Compare commits

...

573 Commits

Author SHA1 Message Date
B. Petersen
a2aa2141d5 make clippy happy 2020-05-16 14:43:00 +02:00
B. Petersen
b0f583d24c return current journal_mode in dc_get_info()
values are either 'delete' or 'wal';
having this info in debug reports is useful for debugging anyway,
if we switch to WAL or not.
2020-05-16 14:23:24 +02:00
bjoern
fff1eaba45 Merge pull request #1485 from deltachat/tweak-delete-docs
clarify delete_server_after option
2020-05-16 11:29:13 +02:00
holger krekel
598dc86ca5 make sure we don't garble output during test runs by more carefully
switching logging on/off globally
2020-05-15 21:19:57 +02:00
Alexander Krotov
3e2bfc35e3 Delete hidden expired messages
The condition remains from the time when expired messages were hidden
instead of being moved into trash chat. As a result, old hidden
messages, such as location messages, were not deleted.
2020-05-15 20:46:41 +03:00
B. Petersen
6658ad8618 clarify delete_server_after option 2020-05-15 17:12:19 +02:00
bjoern
759ccdbee2 Merge pull request #1441 from deltachat/config-from-provider-db
optionally get default-options from provider-db
2020-05-15 12:19:14 +02:00
bjoern
635060a02d Merge pull request #1481 from deltachat/adbenitez-patch-1
Update CHANGELOG.md
2020-05-14 11:16:15 +02:00
Asiel Díaz Benítez
a128e7e7ab Update CHANGELOG.md 2020-05-14 03:22:45 -04:00
Jikstra
3cb69b8035 Merge pull request #1479 from deltachat/prep-1.33
Prep 1.33
2020-05-13 23:17:54 +02:00
B. Petersen
7dc58bb330 bump version 2020-05-13 22:46:04 +02:00
B. Petersen
8bd0a62cb3 changelog 2020-05-13 22:45:01 +02:00
Alexander Krotov
3ee81cbee0 Revert "imap: simplify select_folder() interface"
This reverts commit b614de2f80.
2020-05-13 11:36:33 +02:00
B. Petersen
4b744337fe apply user_defaults after the first successful call to dc_configure() 2020-05-12 23:02:22 +02:00
B. Petersen
8d7d2f7a44 run provider/update.py 2020-05-12 23:02:22 +02:00
B. Petersen
24bf1dbffb fixup 2020-05-12 23:02:22 +02:00
B. Petersen
c1890bb126 adapt provider/update.py so that it generates config_defaults 2020-05-12 23:02:22 +02:00
B. Petersen
a4a570896a add config_defaults array to provider-db 2020-05-12 23:02:22 +02:00
bjoern
0594034ee6 Merge pull request #1477 from deltachat/extract-flags
Extract flags in try_load() to variables
2020-05-12 22:56:00 +02:00
bjoern
396e376f49 Merge pull request #1475 from deltachat/clarify-flags
clarify docs for DC_GCL_FOR_FORWARDING
2020-05-12 22:55:48 +02:00
Hocuri
fca9eae0fd Extract flags in try_load() to variables 2020-05-12 20:28:20 +02:00
B. Petersen
f0d9bdd901 clarify docs for DC_GCL_FOR_FORWARDING 2020-05-12 17:48:03 +02:00
Alexander Krotov
c185d5b0b5 Fix python lint error about unused format 2020-05-12 11:02:27 +02:00
bjoern
6ece2a3449 Merge pull request #1471 from deltachat/tweak-help
add dc_estimate_deletion_cnt() to docs, add some references
2020-05-11 16:01:28 +02:00
B. Petersen
682d52441d add dc_estimate_deletion_cnt() to docs, add some references 2020-05-11 15:31:54 +02:00
Simon Laux
c2c0c81f1c cargo fmt 2020-05-11 11:51:40 +02:00
Simon Laux
fe23907eb3 fix muting dm chats
and rewrite the erroro message so that it makes more sense
2020-05-11 11:51:40 +02:00
bjoern
e2bf8a8a11 Merge pull request #1469 from deltachat/prep-1.32
prep 1.32
2020-05-11 00:15:13 +02:00
B. Petersen
3f0136ae7c bump version to 1.32.0 2020-05-10 23:58:22 +02:00
B. Petersen
9f992409c7 changelog 2020-05-10 23:57:21 +02:00
bjoern
cd53ed16e9 Merge pull request #1468 from deltachat/log_precheck_error
Log precheck_imf errors
2020-05-10 23:30:56 +02:00
Alexander Krotov
cc56edc91d Log precheck_imf errors 2020-05-11 00:19:14 +03:00
bjoern
25eb4b3547 Merge pull request #1463 from deltachat/rfc724_mid_exists-ensure
Do not return "empty rfc724_mid" errors from rfc724_mid_cnt
2020-05-10 23:13:14 +02:00
bjoern
c5eb112f5a Merge pull request #1465 from deltachat/smtp-network-error
Better SMTP ErrorNetwork message
2020-05-10 23:05:20 +02:00
bjoern
8d904f415a Merge pull request #1464 from deltachat/database_busy-warn
sql: do not send DC_EVENT_ERROR on database errors
2020-05-10 22:59:24 +02:00
bjoern
c34173ca6e Merge pull request #1466 from deltachat/fix-prefetch
do normal receive_imf() if message-id is empty or if prefetch failed …
2020-05-10 22:54:16 +02:00
B. Petersen
aa292ac6b8 do normal receive_imf() if message-id is empty or if prefetch failed for other reasons. there are servers not sending a message ids, this and other cases is handled in receive_imf() - but not in prefetch (would be too much to maintain, also we need more information). this normal processing also prevents trying over the same message over and over as the server_uid is updated. 2020-05-10 22:43:04 +02:00
Alexander Krotov
c36227e2fc Better SMTP ErrorNetwork message
It uses stock string, just as for IMAP errors, and is distinguishable
from IMAP errors: protocol is specified in the error message now.
2020-05-10 23:24:03 +03:00
B. Petersen
a406e0416f use new Message-ID parser 2020-05-10 22:58:22 +03:00
B. Petersen
215cc5e71d add function for parsing multiple Message-Ids 2020-05-10 22:58:22 +03:00
B. Petersen
0e72acee10 more tolerant message-id parsing 2020-05-10 22:58:22 +03:00
B. Petersen
000ed3175d add failing test 2020-05-10 22:58:22 +03:00
Alexander Krotov
2f6bae4e2a sql: do not send DC_EVENT_ERROR on database errors
These errors are usually just "database busy" errors, it is enough to
write them to the log instead of displaying to the user.
2020-05-10 17:51:01 +03:00
Alexander Krotov
0fefe11bfd Do not return "empty rfc724_mid" errors from rfc724_mid_cnt
This function should only return temporary errors, e.g. database errors,
as precheck_imf() and dc_receive_imf::add_parts() treat them as such,
retrying the fetch on failure. When permanent errors, like missing
Message-ID, are bubbled up, they cause infinite fetch loop.
2020-05-10 16:43:26 +03:00
Alexander Krotov
2dbb1bbbea Do not reply to hidden messages
Especially with read receipts, it is wrong, because they are never
encrypted and their Message-IDs are not known to other users in a group.
2020-05-09 19:11:23 +03:00
Hocuri
a586a1d525 Fix #1120 Contact requests are not shown when name of sender includes a comma character (#1438)
* First try making get_recipients use MailHeader (nice and functional)

* Get it to compile by using not-so-functional style

* Add "empty-from" test, drop unnecessary check for error; continue using addrparse_header() instead of addrparse()

* Try to use functional style, unfortunately, I can't get the compiler to accept it

* Do it imperative-style: Do not overwrite To with Cc and vice versa

* Use addrparse_header() once more

* Still addrparse_header()

* Clippy

* Fix compile errors in tests

* Fix typo

* Fix tests again ;-)

* Code style

* Code style; try a HashMap<addr: String, display_name: String> as an address list but I am not convinced

* Code style; Use Vec<SingleInfo> as address list

* Clippy

* Add tests

* Add another test

* Remove stale comments
2020-05-07 13:55:09 +02:00
holger krekel
4724101e75 fix upload error?! (#1454)
* use latest setuptools 
* clear indexes also if nothing was uploaded to dc/* branches
2020-05-06 17:31:43 +02:00
bjoern
1b921cd533 Merge pull request #1457 from deltachat/prep-1.31
prep 1.31
2020-05-06 00:15:34 +02:00
B. Petersen
fcf3786fc5 bump version to 1.31.0 2020-05-05 23:53:54 +02:00
B. Petersen
d78f75aa60 changelog 2020-05-05 23:52:53 +02:00
bjoern
56056cf10e Merge pull request #1451 from deltachat/better-errors
Always describe the context of the displayed error
2020-05-05 23:40:35 +02:00
Alexander Krotov
5fd9b20213 Parse attachment filenames from Content-Type "name" attribute
Outlook specifies filename there and omits Content-Disposition.
2020-05-06 00:37:36 +03:00
B. Petersen
076cdae3fd do not show errors during sending as a ephemeral popup or so, just set the message-state to failed, the error can be queried by the user at any time via 'Info' or so 2020-05-05 21:44:03 +02:00
Alexander Krotov
6543c7c26f fetch_single_msg: do not ignore dc_receive_imf errors
If error is ignored, the message will never be fetched again, even if
there was a database write error.

dc_receive_imf itself is modified to ignore unrecoverable errors, to
prevent endless refetching of incorrect messages.
2020-05-04 18:19:32 +03:00
Alexander Krotov
3035c8af30 Always describe the context of the displayed error 2020-05-04 16:35:42 +03:00
bjoern
3cbd647dad Merge pull request #1449 from deltachat/media-quality
support dc_get|set_config("media_quality")
2020-05-04 10:55:35 +02:00
B. Petersen
4efcbee772 support dc_get|set_config("media_quality") 2020-05-03 13:38:39 +02:00
B. Petersen
bb59cf94e9 bump version to 1.30.0 2020-05-02 19:14:35 +03:00
B. Petersen
96436814f5 changelog 2020-05-02 19:14:35 +03:00
Alexander Krotov
e8763e936d imap: simplify select_folder() interface
Accept AsRef<str> instead of Option<impl AsRef<str>>.

There is no need to pass None to force expunge anymore.
2020-04-30 23:48:41 +03:00
Alexander Krotov
c41a6b87b8 imap: always close folder before selecting if expunge is needed 2020-04-30 23:48:41 +03:00
B. Petersen
54395a7252 do not send DC_EVENT_MSGS_CHANGED or DC_EVENT_INCOMING_MSG for hidden messages
these events take the message-id as parameter and might be used
to update an existing list (although to recommended)

if the event is issued for hidden messages,
this might led to "empty" messages flashing up -
the ui tries to get the message from the event,
after a moment, on the next update, the message disappears again
as hidden messages are of course not returned eg. by dc_get_chat_msgs().

the effect was probably always visible for secure-join-messages on ios,
however, become much more visible recently when read-receipts are added
as hidden messages as well (to make them auto-deletable).
2020-04-30 19:44:33 +03:00
bjoern
4322b8b932 Merge pull request #1435 from deltachat/prep-1.29
prepare release 1.29
2020-04-29 16:36:48 +02:00
B. Petersen
f444af825f bump version to 1.29.0 2020-04-29 14:49:40 +02:00
B. Petersen
0d0e7f774e update readme 2020-04-28 23:01:10 +02:00
bjoern
30ed27ae5c Merge pull request #1436 from deltachat/get_info_btreemap
Return BTreeMap instead of HashMap from get_info()
2020-04-28 23:00:28 +02:00
bjoern
b7283487b2 Merge pull request #1426 from deltachat/fix-uptime
fix uptime debug output
2020-04-28 23:00:07 +02:00
jikstra
551f7dc05a Make starts_with_ignore_case() private 2020-04-28 22:52:39 +03:00
jikstra
1b485770b6 cargo fmt 2020-04-28 22:52:39 +03:00
jikstra
50e18f84c2 Also support lowercased dcaccount: uris 2020-04-28 22:52:39 +03:00
jikstra
9eab96090d Cargo fmt 2020-04-28 22:52:39 +03:00
jikstra
737a741a54 Support lowercased openpgp4fpr uri scheme 2020-04-28 22:52:39 +03:00
Alexander Krotov
46253039df Return BTreeMap instead of HashMap from get_info()
BTreeMap is sorted by keys when iterated, making it easier to find the
required line.
2020-04-28 21:26:04 +03:00
B. Petersen
2f5b6a115d update provider database 2020-04-28 21:02:42 +03:00
bjoern
05d63fcbe6 Merge pull request #1430 from deltachat/fix-footer
regard line with ony '--' as footer mark partly
2020-04-28 17:35:15 +02:00
bjoern
ae5bc76123 Merge pull request #1432 from deltachat/Explain-database-path
Update Readme.md: Explain database path, fix #1431
2020-04-27 19:45:55 +02:00
Hocuri
b70e92effb Update Readme.md: Explain database path, fix #1431
Also set the database path to ~/deltachat-db so that nothing has to be changed in the command (it can just be copy-pasted).
2020-04-27 19:18:19 +02:00
B. Petersen
2a9b967d2d remove footer-escape-character from message texts 2020-04-27 16:47:16 +02:00
B. Petersen
459fec56db protect '--' in message from being treated as a footer-beginning 2020-04-27 16:47:16 +02:00
B. Petersen
bfdd6f36e2 regard line with ony '--' as footer mark partly
the footer mark normally used in email-conversations is `-- `,
note the trailing space, see RFC 3676, §4.3

unfortunately, the final space is removed by some providers,
which lead to footers showing up on delta-to-delta-conversations
(on nondc-to-delta, this is not an issue as we cannot be sure anyway
and show a [...] therefore)

this change accepts lines with only `--` as a footer separator
if there is no other footer separator
and if the line before is empty and the line after is not.

as there is still some chance to remove text accidentally,
see tests, some protection against that is needed in another commit.
2020-04-27 16:32:59 +02:00
B. Petersen
432e4b7f0a use std::time::SystemTime for uptime calculation
std::time::Instant does not count eg. doze-time on android
2020-04-26 11:44:27 +02:00
B. Petersen
39cb9c425c fixup 2020-04-26 11:44:27 +02:00
B. Petersen
dff1ae0fb4 move duration-formatting to a separate function and add tests for that 2020-04-26 11:16:59 +02:00
bjoern
2943624439 Merge pull request #1424 from deltachat/show-uptime
add context-uptime to dc_get_info()
2020-04-25 16:27:53 +02:00
bjoern
94064e526a Merge pull request #1423 from deltachat/perform-20-jobs
job: perform no more than 20 IMAP jobs in a row
2020-04-25 01:49:20 +02:00
B. Petersen
95cac4dfb9 add context-uptime to dc_get_info() 2020-04-25 01:37:56 +02:00
Alexander Krotov
46fb6a21ee job: perform no more than 20 IMAP jobs in a row
Let the thread load new messages. This may happen when user switches
the setting to delete messages on the server on and there are a lot of
messages to delete.
2020-04-25 01:43:35 +03:00
bjoern
3d6ca973c4 Merge pull request #1417 from deltachat/dynamic-imap-delete-job-generation
Dynamic imap delete job generation
2020-04-24 21:16:21 +02:00
Alexander Krotov
fc03f4c87a job: generate IMAP deletion jobs dynamically
This prevents generation of a large number of jobs when IMAP deletion
setting is enabled for the first time. Writing jobs to the database
locks it for readers and may cause UI freezing, because chatlist and
messages can't be read until all jobs are written.

Note that on failure job will be written to the database, to make sure
it is postponed instead of being retried immediately.
2020-04-24 21:06:57 +03:00
Alexander Krotov
502ec2a56f job: new API for dynamic job creation
Job::new() can be used to create jobs in-memory

Job.update() is replaced with Job.save() which can create new database
entries and consumes Job to avoid the need to update job ID after saving
it to the database.
2020-04-24 21:06:57 +03:00
Alexander Krotov
66d5c3f620 job: derive PartialEq for Action
This makes it possible to compare action priorities in Rust code.
2020-04-24 21:06:57 +03:00
Floris Bruynooghe
220500efbb Move key loading from deprecated Key struct to DcKey trait
This moves the loading of the keys from the database to the trait and
thus with types differing between public and secret keys.  This
fetches the Config::ConfiguredAddr (configured_addr) directly from the
database in the SQL to simplify the API and consistency instead of
making this the responsiblity of all callers to get this right.

Since anyone invoking these methods also wants to be sure the keys
exist, move key generation here as well.  This already simplifies some
code in contact.rs and will eventually replace all manual checks for
existing keys.

To make errors more manageable this gives EmailAddress it's own error
type and adds some conversions for it.  Otherwise the general error
type leaks to far.  The EmailAddress type also gets its ToSql trait impl
to be able to save it to the database directly.
2020-04-24 01:11:11 +02:00
Hocuri
d29c5eabbb Improve descriptions for the profile image files 2020-04-23 22:47:53 +02:00
Alexander Krotov
979d7c5625 Do not ignore database read errors in precheck_imf
If precheck_imf fails to check if message with the same rfc724_mid
already exists, the same message may be downloaded twice. Instead,
abort the whole operation and retry later.
2020-04-23 15:36:45 +03:00
B. Petersen
4e828199c8 test, that also unrecoded avatars are copied to the blob-directory 2020-04-23 06:49:41 +02:00
Hocuri
61c84c8e01 Log more in tests 2020-04-23 06:48:18 +02:00
bjoern
30be5e33d7 Merge pull request #1410 from deltachat/remove_addremove
remove all member_added/remove_events
2020-04-23 00:59:09 +02:00
bjoern
b1c524d4a3 Merge pull request #1415 from deltachat/py_fresh
expose obtaining list of fresh messages to python
2020-04-21 14:14:24 +02:00
holger krekel
7c33c7f7da expose obtaining list of fresh messages to python 2020-04-21 13:53:56 +02:00
bjoern
7846c23edd Merge pull request #1411 from deltachat/delete-no-hide
Delete expired messages instead of hiding them
2020-04-20 18:04:30 +02:00
Alexander Krotov
6c0dd8543d Delete expired messages instead of hiding them
For hidden messages, blobs are not deleted during housekeeping. To
actually free the space used by media files, messages should be moved to
trash instead of being hidden.
2020-04-20 02:29:55 +03:00
holger krekel
70c082a1b1 remove all member_added/remove_events 2020-04-19 21:15:45 +02:00
holger krekel
02cda1e611 refine member/add remove further, and introduce ac_outgoing_message 2020-04-19 21:00:55 +02:00
holger krekel
a1c82eaea6 refine bot testing and ac_member* handling 2020-04-19 21:00:55 +02:00
holger krekel
9eda710538 refine member-added and member-removed plugin hooks to signal the sender (who added/removed a contact )
add ac_chat_modified hook event
add account.get_contact_by_addr (thanks @r10s)
2020-04-19 21:00:55 +02:00
Hocuri
a87a2d0b71 Change to functional style 2020-04-19 18:28:10 +03:00
Hocuri
6e2f4d85a3 Do not ellipsize non-standard footers in chat messages 2020-04-19 18:28:10 +03:00
Hocuri
511727fdfa Do not ellipsize everything in a message 2020-04-19 18:28:10 +03:00
bjoern
a86c7c767a Merge pull request #1409 from deltachat/ffi-doc-fix
fix doc for dc_lookup_contact_id_by_addr()
2020-04-18 13:17:50 +02:00
bjoern
da88d8f17f Update deltachat-ffi/deltachat.h
Co-Authored-By: holger krekel  <holger@merlinux.eu>
2020-04-18 13:17:33 +02:00
B. Petersen
dbd3705441 fix doc for return value of dc_lookup_contact_id_by_addr() 2020-04-18 01:47:36 +02:00
Alexander Krotov
8d2f526ee7 Fix a typo 2020-04-17 22:29:58 +03:00
B. Petersen
b075a73222 adapt doc wrt events not longer having a return value 2020-04-17 21:26:58 +02:00
bjoern
eb5387ca38 Merge pull request #1403 from deltachat/remove-unused
remove unused api
2020-04-17 13:02:09 +02:00
B. Petersen
1eaef94c71 make clippy happy 2020-04-16 22:49:34 +02:00
B. Petersen
e1903edd04 remove unused dc_array_add_id() api 2020-04-16 22:11:54 +02:00
B. Petersen
0b6b8ced92 remove unused dc_chat_get_subtitle() api 2020-04-16 22:08:17 +02:00
B. Petersen
cc6ce72f6e remove unused dc_get_version_str() api 2020-04-16 21:48:51 +02:00
Alexander Krotov
857a384d8b Switch to nightly-2020-03-12 2020-04-16 08:51:41 +02:00
Alexander Krotov
13dd88b7ad mimeparser: display inline images received from Thunderbird 2020-04-15 11:13:26 +02:00
Alexander Krotov
e85cdc8c9f mimeparser: test parsing of inline images 2020-04-14 07:01:50 +02:00
Alexander Krotov
1760740a4c Parse inline attachments from non-DC email clients
Some mobile email clients, such as apple mail, attach or inline images
after description, just like Delta Chat. It is better to display them
instead of ignoring.
2020-04-14 07:01:50 +02:00
Alexander Krotov
daf40fde82 mimefactory: use .next() instead of .nth(0) 2020-04-13 23:02:57 +03:00
Alexander Krotov
e909b8199b lot: use as_deref() 2020-04-13 23:02:57 +03:00
Alexander Krotov
ec089faf3a configure/auto_mozilla: remove indexing 2020-04-13 23:02:57 +03:00
Alexander Krotov
9fcb30ac33 Remove indexing in chatlist.get_summary() 2020-04-13 23:02:57 +03:00
Alexander Krotov
cb92579461 Use slice patterns in EmailAddress::from_str() 2020-04-13 23:02:57 +03:00
Alexander Krotov
0327000f8d squash_attachment_parts: use slice patterns 2020-04-13 23:02:57 +03:00
Alexander Krotov
3a91c87c73 clippy: use as_deref() 2020-04-13 23:02:57 +03:00
Alexander Krotov
db5b5d321b clippy: remove redundant imports 2020-04-13 23:02:57 +03:00
Alexander Krotov
76b7e7408a chatlist: remove indexing in get_{chat,msg}_id() 2020-04-13 23:02:57 +03:00
Alexander Krotov
324e5d0258 Use slice pattern to parse KML coordinates 2020-04-13 23:02:57 +03:00
Alexander Krotov
d997bbc081 skip_forward_header: get rid of indexing with subslice patterns 2020-04-13 23:02:57 +03:00
Alexander Krotov
1c21d4f356 constants: remove unnecessary parenthesis
Rust warns about them now
2020-04-13 23:02:57 +03:00
Alexander Krotov
5f574cf283 get_next_media: replace indexing with .get() 2020-04-13 23:02:57 +03:00
Alexander Krotov
92f1e6da1e get_next_media: enumerate() instead of indexing 2020-04-13 23:02:57 +03:00
Alexander Krotov
24aa3c781b Remove indexing in Aheader::from_str 2020-04-13 23:02:57 +03:00
Alexander Krotov
32bd6109e3 Update rust-toolchain and proptest
It is required for stabilized subslice patterns.

proptest 0.9.6 fixes compatibility with the latest rustc nightly.
2020-04-13 23:02:57 +03:00
B. Petersen
52442017e2 mark contacts as verified if a secure-join for them was observed on another device 2020-04-13 19:15:16 +02:00
B. Petersen
278454287c make sure, Secure-Join-Fingerprint is not accepted unencrypted 2020-04-13 19:15:16 +02:00
B. Petersen
5ded8fb400 add vc-contact-confirm-received message needed for multi-device-verification 2020-04-13 19:15:16 +02:00
B. Petersen
24730e7ad6 do not delete handshake-messages needed for a multi-device-verification 2020-04-13 19:15:16 +02:00
B. Petersen
016a780632 check that incoming read-receipts do not unarchive chats 2020-04-13 17:51:56 +02:00
Hocuri
6d89638ca4 Get ChatId from Message 2020-04-13 17:43:01 +02:00
Hocuri
fdc091319b Make Clippy happy 2020-04-13 17:43:01 +02:00
Hocuri
960c8745d9 For smtp error 5.5.0, try again. For others, send info message (doesn't work) 2020-04-13 17:43:01 +02:00
Hocuri
134b09dba5 Fix #1373, ignore incorrect html close tags 2020-04-13 17:40:07 +02:00
holger krekel
76b93274e8 use latest sphinx -- seems to work again 2020-04-13 17:39:31 +02:00
holger krekel
ea455323d8 increase timeout while waiting for rsa2048 keygen -- default timeout is 30secs, and it sometimes takes 31 or more on the CI machines 2020-04-13 17:39:31 +02:00
holger krekel
1855f84fe0 fix bug in that remove-contact failed on new groups where we didn't have the peerstate of the removed-contact yet 2020-04-13 17:39:31 +02:00
holger krekel
323d996d5f a few streamlinings 2020-04-13 17:39:31 +02:00
holger krekel
1b858393c5 make create_contact accept email addresses that parse into routable_email and display_name 2020-04-13 17:39:31 +02:00
holger krekel
ca88c5b41c rename hooks to use "ac_" (account) and "dc_" (global) 2020-04-13 17:39:31 +02:00
holger krekel
7e1470ea46 refactor preconfigure handling to not break deltabot's usage of deltachat's test fixtures and
relax timestamp comparisons
2020-04-13 17:39:31 +02:00
holger krekel
f98d0bbc1f fix failing sync test 2020-04-13 17:39:31 +02:00
holger krekel
2a34022619 refine example doc and address https://github.com/deltachat/deltachat-core-rust/pull/1307#pullrequestreview-380876587 2020-04-13 17:39:31 +02:00
holger krekel
57f879a6ba fix buffer handling so that the group-tracking bot example passes 2020-04-13 17:39:31 +02:00
holger krekel
d4ba09c753 refactor bot-setup and testing into a helper function 2020-04-13 17:39:31 +02:00
holger krekel
6c3a8448cf Apply suggestions from code review
some fixes thanks to @adbenitez

Co-Authored-By: Asiel Díaz Benítez <asieldbenitez@gmail.com>
2020-04-13 17:39:31 +02:00
holger krekel
d4e1c1b109 refine handling of accepted contacts in example 2020-04-13 17:39:31 +02:00
holger krekel
a1d5120e58 sipmlify plugins and tests and remove superflous core event 2020-04-13 17:39:31 +02:00
holger krekel
724e1ea97e simplify example 2020-04-13 17:39:31 +02:00
holger krekel
6f8067ffd3 address @adbenitez and @r10s comments 2020-04-13 17:39:31 +02:00
holger krekel
f38386d164 fix member_added/member_removed event with tests and and provide a group-tracking example 2020-04-13 17:39:31 +02:00
holger krekel
d66829702f fix #164 add MEMBER_REMOVED event and member_removed plugin python hook 2020-04-13 17:39:31 +02:00
holger krekel
36b50436d7 add Message.mark_seen shortcut 2020-04-13 17:39:31 +02:00
holger krekel
33dd747ec7 some more test setup refinements and make example testing part of tox runs 2020-04-13 17:39:31 +02:00
holger krekel
d8e14d9993 refine example and make Contact accept Account object 2020-04-13 17:39:31 +02:00
holger krekel
f61b9f7964 add a test echo_and_quit examples 2020-04-13 17:39:31 +02:00
holger krekel
91cdc76414 refactor docs and ffi/high level event handling to pass all tests again 2020-04-13 17:39:31 +02:00
holger krekel
a1379f61da fix up docs 2020-04-13 17:39:31 +02:00
holger krekel
a665d6de59 add chat.is_group() API to help callers avoid having to check with constants
deprecate get_type()
2020-04-13 17:39:31 +02:00
holger krekel
6a6a719ab6 shift pytest support code into deltachat package so deltabot can make use of the test infrastructure 2020-04-13 17:39:31 +02:00
holger krekel
84f17b7539 emit "DC_EVENT_MEMBER_ADDED" and python plugin event "member_added" for securejoin or non-securejoin additions of a contact to a chat. also fixup some docs 2020-04-13 17:39:31 +02:00
holger krekel
57141e478c also add a changelog for plugin things 2020-04-13 17:39:31 +02:00
holger krekel
6213917089 start some docs 2020-04-13 17:39:31 +02:00
holger krekel
fb33c31378 fix a couple of issues wrt to configuring move/mvbox behaviour in tests 2020-04-13 17:39:31 +02:00
holger krekel
79f5e736b0 make eventlogger module a global plugin 2020-04-13 17:39:31 +02:00
holger krekel
0d4b6f5627 move io thread handling into own module 2020-04-13 17:39:31 +02:00
holger krekel
5c8f558f60 - simplify to offer start() and shutdown() as primary account methods, strike start_threads/stop_threads.
- introduce update_config(kwargs) method.
- group APIs a bit better
2020-04-13 17:39:31 +02:00
holger krekel
c851f9d5a3 simplify internal thread handling 2020-04-13 17:39:31 +02:00
holger krekel
2d74514dd0 add some incoming/outgoing message hooks 2020-04-13 17:39:31 +02:00
holger krekel
84012e760e refine low level event handling
slight refactor on printing
2020-04-13 17:39:31 +02:00
holger krekel
6baef49f9d add after_shutdown hook 2020-04-13 17:39:31 +02:00
holger krekel
f55d4fa73a rename process_low_level_event to process_ffi_event 2020-04-13 17:39:31 +02:00
holger krekel
ce00c627d4 don't run Eventlogging by default -- the tests instantiate it, though. 2020-04-13 17:39:31 +02:00
holger krekel
d3c6f530e2 introduce global plugin manager 2020-04-13 17:39:31 +02:00
holger krekel
cf6391d51b move event tracking to new tracker.py file
some api cleanups
2020-04-13 17:39:31 +02:00
holger krekel
57311d731e simplify logging 2020-04-13 17:39:31 +02:00
holger krekel
95d45b386f separate out FFI eventracking to only be used in running tests 2020-04-13 17:39:31 +02:00
holger krekel
bbc8bed39c move temp_plugin to account 2020-04-13 17:39:31 +02:00
holger krekel
ec67b3975c good bye global plugin manager ... we only do per-account object plugin_management for now 2020-04-13 17:39:31 +02:00
Alexander Krotov
e9967c32e6 chat: filter parent messages by state instead of UID
Since introduction of server message deletion, message may not have
server UID even when it is not a draft or pending message. To handle
such messages correctly, we explicitly check message state.
2020-04-13 17:38:42 +02:00
Alexander Krotov
493a213d41 sql: move QueryReturnedNoRows and NULL handling into query_row_optional() 2020-04-13 17:38:42 +02:00
Alexander Krotov
f65dbee74b Add basic test for ChatId.parent_is_encrypted() 2020-04-13 17:38:42 +02:00
Alexander Krotov
f51fd1267f chat: move parent_query() and related methods from Chat to ChatId 2020-04-13 17:38:42 +02:00
Hocuri
711f3f69da Emit an event when SMTP fails (the same is already done for IMAP) 2020-04-12 18:17:24 +03:00
Friedel Ziegelmayer
24f4cbbb27 refactor: replace failure
- failure is deprecated
- thiserror for deriving Error impl
- anyhow for highlevel error handling
2020-04-10 22:39:28 +02:00
Alexander Krotov
d31265895d Update rPGP to 0.5.2 2020-04-03 12:57:13 +03:00
bjoern
6e35a879a3 Merge pull request #1375 from deltachat/robust-hide-device-expired-messages
Make "hide_device_expired_messages" more robust
2020-04-03 11:48:09 +02:00
Alexander Krotov
9c2a3b8a82 Chatlist::try_load: make hide_device_expired_messages errors non-fatal 2020-04-03 12:13:42 +03:00
Alexander Krotov
916fab7d4b hide_device_expired_messages: allow missing self or device chat 2020-04-03 12:12:56 +03:00
Alexander Krotov
3163ef87c6 imap: display errors with {}, not {:?} 2020-04-01 23:27:15 +03:00
Alexander Krotov
1934181b52 Terminate new SQL statement lines with \
This way rustfmt can change indentation. However, care should be taken
to keep a space before each \
2020-04-01 20:06:27 +03:00
Alexander Krotov
1b815a7d96 Display an error if message cannot be trashed 2020-04-01 20:06:27 +03:00
Alexander Krotov
4e0a08106d ChatId.get_param: remove unnecessary type annotation 2020-04-01 20:06:27 +03:00
Alexander Krotov
e8cc739fbd Reload chatlist when "delete_device_after" is set 2020-04-01 20:06:27 +03:00
Alexander Krotov
fc57cbfb49 Refactor hiding of expired messages
Now there is only one function: hide_device_expired_messages().

If any messages are hidden event DC_EVENT_MSGS_CHANGED(0,0) is emitted
now, which is more correct than DC_EVENT_CHAT_MODIFIED and also triggers
chatlist reload.
2020-04-01 20:06:27 +03:00
Alexander Krotov
7522fec044 Do not emit "chat modified" events when loading chatlist
Otherwise chatlist will be loaded twice, once right after hiding expired
messages, and once in response to event emitted by try_load().
2020-04-01 20:06:27 +03:00
Alexander Krotov
237dabb907 Do not hide hidden messages in ChatId.delete_device_expired_messages()
This prevents infinite event loop, when chatlist is reloaded in response
to event, and event is emitted by hiding messages before chatlist reload.
2020-04-01 20:06:27 +03:00
Alexander Krotov
3686048ab6 chatlist: hide all expired messages before loading 2020-04-01 20:06:27 +03:00
Alexander Krotov
2bf4c5d7e7 Emit "chat modified" event when messages expire 2020-04-01 20:06:27 +03:00
Alexander Krotov
051d80b2f3 Move delete_device_expired_messages() to ChatId
This allows to check if chat is a self-talk or device chat.

Messages are deleted only in viewed chats now.
2020-04-01 20:06:27 +03:00
Alexander Krotov
adaa1e856c chat: add ChatId.is_self_talk() and ChatId.is_device_talk() 2020-04-01 20:06:27 +03:00
Alexander Krotov
4daa57c98e delete_device_expired_messages: set text to DELETED for hidden messages
This makes debugging easier: DELETED messages should never be shown.
2020-04-01 20:06:27 +03:00
Alexander Krotov
7e67b2cbb3 Do not delete messages from special chats 2020-04-01 20:06:27 +03:00
Alexander Krotov
270d18a88a Move prune_tombstones() to sql:: and call from housekeeping() 2020-04-01 20:06:27 +03:00
Alexander Krotov
25f8a735a9 get_chat_msgs: remove locally expired messages
Expired messages are hidden right before retrieving messages from the
database, so expired messages are not shown to the user.
2020-04-01 20:06:27 +03:00
Alexander Krotov
9eb672ea17 Move removal of chat message tombstones to a separate function
DELETE operation may be slow compared to UPDATE. It is better to do in
a separate job.
2020-04-01 20:06:27 +03:00
Alexander Krotov
9febc762da Add chat::delete_device_expired_messages() function 2020-04-01 20:06:27 +03:00
Alexander Krotov
4b742c220c Add Context.get_config_delete_device_after() method
In contrast to get_config_delete_server_after(), value 1 does not mean
"delete at once", because it does not make sense to delete messages
immediately after receivning them.
2020-04-01 20:06:27 +03:00
Alexander Krotov
9d03d441e1 Add Config::DeleteDeviceAfter option 2020-04-01 20:06:27 +03:00
B. Petersen
ff8b249cc6 For now, 'Saved messages' are auto-deleted from the server, but not from device 2020-04-01 20:06:27 +03:00
B. Petersen
248e6ea5e7 make clippy happy 2020-04-01 20:06:27 +03:00
B. Petersen
be0afdebfd target comments of @link2xt, fix ci 2020-04-01 20:06:27 +03:00
B. Petersen
9f19d20344 implement message estimating 2020-04-01 20:06:27 +03:00
B. Petersen
aea8a32ba5 add 'estimatedeletion' to repl tool 2020-04-01 20:06:27 +03:00
B. Petersen
d1a4c82937 prototype ffi for ephemeral messages 2020-04-01 20:06:27 +03:00
Alexander Krotov
4f73812673 Prioritize message deletion over message moving
If Delta Chat goes online and gets an expired message is in the Inbox,
it should delete it instead of moving and then deleting.
2020-04-01 20:06:27 +03:00
Alexander Krotov
33150615a1 Improve logging for server UID updates in precheck_imf()
Log all folders and UIDs and warn about UID changes without folder change.
2020-04-01 20:06:27 +03:00
Alexander Krotov
491f83c86d Remove ServerFolder and ServerUid job parameters
They were used by MarkseenMdnOnImap
2020-04-01 20:06:27 +03:00
Alexander Krotov
41f776763b Remove MarkseenMdnOnImap
MarkseenMdnOnImap stored server folder and UID which are never updated
by update_server_uid. Now hidden entries are created for MDNs, so they
should be handled as ordinary messages.
2020-04-01 20:06:27 +03:00
Alexander Krotov
65fdfac866 Remove unused dest_uid argument from Imap.mv()
It was always set to 0 because we don't know the destination UID.
2020-04-01 20:06:27 +03:00
Alexander Krotov
cb0c00bc6d Log rfc724_mid in DeleteMsgOnImap 2020-04-01 20:06:27 +03:00
Alexander Krotov
ad53678c19 Remove msgs.unlinked column
It is not used anymore.

Database version 64 migration introducing this column is also removed.
2020-04-01 20:06:27 +03:00
Alexander Krotov
62097765a6 update_server_uid: set server_uid even for unlinked messages
Sometimes message deletion job marks message as unlinked without
actually deleting it. It is possible if the message was already moved
into another folder, possibly by second device, but not detected there
yet. It should be detected later in the other folder, and the
server_uid in the database should be set.

Since introduction of add_imap_deletion_jobs() any expired message
record will be marked as unlinked eventually, because another message
deletion job will be scheduled even if update_server_uid() resurrects
the message once.
2020-04-01 20:06:27 +03:00
Alexander Krotov
efb7280e99 Do not schedule delayed DeleteMsgOnImap jobs
All IMAP deletion jobs are scheduled either immediately, or later by
job::add_imap_deletion_jobs().
2020-04-01 20:06:27 +03:00
Alexander Krotov
bdb2a47743 Revert "Automatically delete messages in 2 weeks window only"
This may result in messages not being deleted. Optimization and
traffic-saving is postponed for later, one idea is to optimize message
deletion to avoid checking if Message-ID on the server matches
Message-ID in the database.
2020-04-01 20:06:27 +03:00
Alexander Krotov
c4677190be Postpone DeleteMsgOnImap on error
If job returns Status::Finished, it will be deleted. Then
add_imap_deletion_jobs will recreate it immediately if the message is
expired. To actually backoff the job, we should postpone it instead of
removing.
2020-04-01 20:06:27 +03:00
Alexander Krotov
055aba189c Automatically delete messages in 2 weeks window only
This is to avoid creating thousands of jobs when user enables
"delete_server_after" setting for the first time.

If device is offline for more than 2 weeks, some messages may not be
deleted.
2020-04-01 20:06:27 +03:00
Alexander Krotov
314c3d5e78 add_imap_deletion_jobs: check only for DeleteMsgOnImap jobs
Other jobs may have different meaning for foreign_id.

Also removed " \" at the end of lines.
2020-04-01 20:06:27 +03:00
Alexander Krotov
6db03356b5 Return AlreadyDone from Imap.delete_msg if message is gone
This way DeleteMsgOnImap will remove invalid server_uid from the database.
2020-04-01 20:06:27 +03:00
Alexander Krotov
28af919b09 Create DeleteMsgOnImap jobs before performing IMAP jobs
When "delete_server_after" setting is configured, postponed
DeleteMsgOnImap jobs are created for incoming messages.

This commit adds job::add_imap_deletion_jobs function which creates
DeleteMsgOnImap jobs right before performing IMAP jobs. This way even
messages that expired when the setting was disabled are going to be
deleted.

Job creation on message reception is unnecessary now, and even harmful
because it will create jobs with an expiration time which may later be
reduced. It is planned to be removed in following commits.
2020-04-01 20:06:27 +03:00
Alexander Krotov
8f7a456a39 Use 0 value for "delete_server_after" default.
Now 0 means "never delete", 1 means "delete at once" and other values
indicate the number of seconds after which them message should be
deleted from the server.

Configuration value interpretation is moved into
Context.get_config_delete_server_after() function.
2020-04-01 20:06:27 +03:00
Alexander Krotov
5b3bec1aac Create entries in msgs table for MDNs
At least one entry is required for DeleteMsgOnImap job. Additionally,
adding a hidden entry makes it possible to avoid redownloading the
message if it gets a new UID on the server.
2020-04-01 20:06:27 +03:00
Alexander Krotov
f2aa17c9d0 Resultify MsgId.delete_from_db() 2020-04-01 20:06:27 +03:00
Alexander Krotov
bc06b9e051 Do not send BCC-Self copy if we are going to remove it immediately 2020-04-01 20:06:27 +03:00
Alexander Krotov
6d216af507 Delete BCC-self messages after "delete_server_after" time 2020-04-01 20:06:27 +03:00
Alexander Krotov
b2f1d9f376 Do not remove rfc724_mid for unlinked messages
Message-ID is used to send read receipts. Instead, add a separate
"unlinked" column.
2020-04-01 20:06:27 +03:00
Alexander Krotov
a653e469f2 Add user-configurable option "delete_server_after"
The option sets timer in seconds after which all parts of the message
are deleted from the server.
2020-04-01 20:06:27 +03:00
Alexander Krotov
4f4241ba3a dc_receive_imf: delete all message parts if message should be deleted
DeleteMsgOnImap deletes files from the server only when the last part
is deleted. Removing only the first part of the hidden or trashed
message does not result in message deletion.
2020-04-01 20:06:27 +03:00
Alexander Krotov
2cf9c68040 Implement MsgId.unlink() and use it in DeleteMsgOnImap
Currently only trashed or hidden messages are deleted by
DeleteMsgOnImap, so it is safe to remove database records.

It is planned to delete messages on IMAP server after
user-configurable time to cleanup the server even for messages
displayed in chats. For such messages, we unlink them from the
Message-ID, but keep the database record to display them.
2020-04-01 20:06:27 +03:00
Alexander Krotov
cc0f977d6f Document rfc724_mid_cnt 2020-04-01 20:06:27 +03:00
Alexander Krotov
7879952fde Delete MDNs first in MsgId.delete_from_db() 2020-04-01 20:06:27 +03:00
Alexander Krotov
4452cab987 Turn Message::Delete_from_db into MsgId method 2020-04-01 20:06:27 +03:00
Alexander Krotov
98bd64621a Refactor Imap.delete_msg() error handling
Mutable UID reference is removed. Instead, ImapActionResult is
returned immediately.

If message has changed and UID does not correspond to expected
message, ImapActionResult::Failed is returned. Temprorary IMAP errors
result in ImapActionResult::RetryLater.

On the job side, ImapActionResult::RetryLater does not result in
immediate job retry anymore. Instead, job is retried with a backoff.
2020-04-01 20:06:27 +03:00
Alexander Krotov
c1c769ceb0 Add MsgId.trash() and use it to delete messages locally
In addition to moving the message into trash chat, this function
removes message text to make sure the message does not remain in the
database. Only the information necessary to delete message from the
server and avoid redownloading it should be kept, such as Message-Id
and IMAP UID.
2020-04-01 20:06:27 +03:00
Alexander Krotov
d64e55c66f delete_msgs: remove explicit .iter() 2020-04-01 20:06:27 +03:00
Alexander Krotov
76fc84be37 job: document DeleteMsgOnImap 2020-04-01 20:06:27 +03:00
bjoern
8cd5f5990e Merge pull request #1295 from deltachat/draft_group_consistency
draft doc for healing group-inconsistencies
2020-03-31 18:27:57 +02:00
B. Petersen
6ffe54d68f do no longer ignore keypair generation test, due to the ecc-move, it is no longer expensive 2020-03-31 18:20:40 +02:00
Alexander Krotov
d78ea882c8 Update mailparse to 0.12 2020-03-31 18:11:44 +02:00
Alexander Krotov
958802a233 Add system message only if contact was removed successfully 2020-03-31 18:08:19 +02:00
bjoern
00b02efdc2 Merge pull request #1367 from deltachat/no-forward-to-device-chat
hide the device-chat from default forward-to-lists
2020-03-31 16:03:53 +02:00
Floris Bruynooghe
50569f12f5 Remove unsafe CString::yolo from ffi
CString::yolo was still used in the ffi, this was an unsafe
transitional thing.  To remove it there were two choices: 1. make
errors in creating CStrings hard errors or 2. try and be as lenient as
possible.  Given the to_string_lossy() convention adopted in the ffi
this choose the lenient option and simply skips over embedded null
bytes, leaving the rest of the strings intact.

Thus now CString::new_lossy().  It's only used for .strdup() however
so no longer a public trait.

This also cleans up the public visibility of things in the strings.rs
file:

- Rename StrExt/OptStrExt traits to what they actually do: provide
  .strdup() -> Strdup/OptStrdup.

- dc_strdup() should be an implementation detail, replace all usages
  with Strdup.strdup() method.

- Only allow visibility inside the crate for all things.

- Reduce visibility to only the module for things not used in lib.rs.
2020-03-31 09:12:41 +02:00
B. Petersen
8aa4ceb570 hide the device-chat from default forward-to-lists 2020-03-30 15:59:07 +02:00
Alexander Krotov
a7afbf85ad Replace test Alice key with ed25519 key
Autocrypt Setup Message does not contain "==" anymore since key length
has changed.
2020-03-29 19:52:45 +03:00
bjoern
8fdf3dcdb8 Merge pull request #1314 from deltachat/fix-secure-join
ignore handshake messages seen from another device
2020-03-28 10:28:55 +01:00
B. Petersen
818c20e0cb switch to ecc keys
after fixing some issues wrt ecc keys, see #1319,
and waiting some time (three core releases, two ios/android/desktop releases),
it is now the time to switch again to ecc keys again,
after the first attempt was stopped in #1319
2020-03-28 01:05:24 +03:00
bjoern
c1d4996777 Merge pull request #1361 from deltachat/prep-1.28
Prep 1.28
2020-03-25 22:09:34 +01:00
B. Petersen
fd3e6e0ee4 bump version to 1.28 2020-03-25 19:30:52 +01:00
B. Petersen
edc5754c68 changelog 2020-03-25 19:29:44 +01:00
bjoern
bd75dea000 Merge pull request #1359 from deltachat/fix-imap-delimiter
fix imap delimiter
2020-03-25 19:01:58 +01:00
B. Petersen
e09a0a548f using first() instead of [0] 2020-03-25 18:10:02 +01:00
bjoern
15ee8b4362 Merge pull request #1357 from deltachat/ad-hoc-groups
fix classic-email interactions
2020-03-25 17:57:59 +01:00
B. Petersen
ab8d75b192 remove now unused ImapConfig.imap_delimiter 2020-03-25 13:23:29 +01:00
B. Petersen
e135c969c9 figure out and use folder-delimiter provided by LIST command 2020-03-25 13:16:23 +01:00
B. Petersen
36e7090466 force folder-reconfigure on dc_configure() 2020-03-25 02:39:48 +01:00
B. Petersen
f28f177c6b use constant for folders_configured cache 2020-03-24 19:18:18 +01:00
B. Petersen
785973c624 allow ad-hoc-group creation if there is an accepted chat with the contact 2020-03-24 16:47:54 +01:00
B. Petersen
9c06acff72 add tests wrt classic email-interaction 2020-03-24 16:47:54 +01:00
bjoern
4fabddeb47 Merge pull request #1354 from deltachat/fix-bcc-self-group-crash
fix crash in self-only-groups with bcc_self enabled
2020-03-23 12:21:27 +01:00
B. Petersen
17ff1ab372 this corrects the To:-list for group-messages with only SELF as member.
before, the list was empty
and trying to send to groups that only contain SELF lead to a crash.

in theory, this happens for both, bcc_self enabled or not, however,
if bcc_self was disabled (default setting),
things worked as the whole mimerendering was skipped on a higher level.
also, the saved-messages-chat was not affected as this was checked explicitly.

this pr changes the mimerendering so that From: is used as To:
if there is no recipient-list and the messasge will be sent to SELF only.
2020-03-23 01:48:27 +01:00
B. Petersen
3c34096392 add a failing test that crashes when sending a message to a self-only group with bcc_self enabled 2020-03-22 18:23:55 +01:00
bjoern
70e0d3b571 Merge pull request #1353 from deltachat/show-nondc-replies
fix showing non-dc replies
2020-03-22 16:48:49 +01:00
B. Petersen
ae5a2396f3 fix typo 2020-03-22 16:18:53 +01:00
B. Petersen
8f82bf40e0 let function that search for Message-IDs accept widly used square brackets format 2020-03-22 12:20:28 +01:00
B. Petersen
fe398de2fa add test that fail on checking existance of a Message-ID with angle brackets 2020-03-22 12:20:28 +01:00
Alexander Krotov
a770d75e2e Fix condition in normalize_name() 2020-03-22 00:11:41 +00:00
Alexander Krotov
a330104e9b Add tests that crash normalize_name() 2020-03-22 00:11:41 +00:00
bjoern
aae3cae4bb Merge pull request #1342 from deltachat/group-rejoin-bug
Rebuild group member list on group rejoin
2020-03-20 00:39:26 +01:00
Alexander Krotov
e7e4821804 Reset group member list when we are added to the group
This makes test_synchronize_member_list_on_group_rejoin pass.
2020-03-19 17:03:31 +03:00
Alexander Krotov
9654802acc Add failing group rejoin test 2020-03-19 17:03:31 +03:00
Friedel Ziegelmayer
06a24fa4d0 Merge pull request #1345 from deltachat/ci/fix-online-tests
ci: ensure dcc_new_temp_email is propagated
2020-03-19 15:02:30 +01:00
dignifiedquire
62b1b0519a ci: ensure dcc_new_temp_email is propagated
Closes #1344
2020-03-19 11:14:37 +01:00
bjoern
10afdfecdd Merge pull request #1336 from deltachat/forward-to-saved
optionally sort 'saved messages' atop of the chatlist
2020-03-11 22:05:50 +01:00
B. Petersen
c0e08fb927 fix typo 2020-03-11 16:17:29 +01:00
B. Petersen
6d6bc9b050 for forwarding, sort 'saved messages' atop of the chatlist 2020-03-09 23:30:22 +01:00
Alexander Krotov
4714fb6887 Reset server_folder and server_uid in Imap.empty_folder()
This way we avoid trying to delete already deleted messages in the future.
2020-03-09 01:21:12 +03:00
bjoern
5f47810964 Merge pull request #1328 from deltachat/prep-1.27
Prep 1.27
2020-03-04 18:37:31 +01:00
B. Petersen
0f6024e055 bump version to 1.27 2020-03-04 17:04:26 +01:00
B. Petersen
fafc15f80c changelog 2020-03-04 16:55:44 +01:00
bjoern
9a85ea861d Merge pull request #1327 from deltachat/fix/update-rpgp
fix: update to pgp@0.5
2020-03-04 16:49:56 +01:00
dignifiedquire
9541960307 update fixed rpgp 2020-03-04 15:10:19 +01:00
dignifiedquire
95073deb96 fix: update to pgp@0.5 2020-03-04 14:51:58 +01:00
Alexander Krotov
82b4647b95 Update dc_truncate() comment 2020-03-02 23:09:38 +03:00
Alexander Krotov
0c770a8b37 Fix Origin::AddressBook spelling 2020-03-02 22:57:22 +03:00
B. Petersen
0e4031348f remove unwrap-flag from dc_truncate() - it was never really used on-purpose 2020-03-02 14:10:44 +01:00
B. Petersen
5cc26762c2 unwrap lineends in summaries 2020-03-02 14:10:44 +01:00
B. Petersen
b8b4853d1f add failing test for checking linebreak-unwrapping in get_summary 2020-03-02 14:10:44 +01:00
Alexander Krotov
fbabe27fc1 Make add_or_lookup errors non-fatal in add_address_book() 2020-03-02 14:08:20 +01:00
bjoern
4d1554c85b Merge pull request #1323 from deltachat/prep-1.26
prepare 1.26.0
2020-03-01 22:34:37 +01:00
B. Petersen
ab40495d5c update rPGP in .toml, this was missed in #1321 2020-03-01 21:54:11 +01:00
B. Petersen
42ebf49f92 bump version to 1.26.0 2020-03-01 21:46:26 +01:00
B. Petersen
c5ccd88f79 changelog 2020-03-01 21:43:57 +01:00
B. Petersen
dbd1b227d9 revert generating ecc keys for now
the currently released versions fail sometimes in encrypting to ecc keys,
see #1313, the issue is about to be fixed,
however, we should not generate ecc keys until the
fix is rolled out - otherwise new users will get encryption errors every some
messages if their counterpart is not yet using the most recent version.

we can start generating ecc keys a few weeks after the fix is rolled out.
2020-03-01 21:34:04 +03:00
Alexander Krotov
63baac3c61 Update rPGP to 0.4.1
This version fixes incorrect serialization of ECC session keys with
leading zeros.
2020-03-01 20:52:08 +03:00
B. Petersen
7c39bb6659 ignore handshake messages seen from another device
moreover, prepare for marking peers as verified accross devices,
see detailed comment for observe_securejoin_on_other_device()
2020-02-27 15:04:11 +01:00
B. Petersen
4f8c5965ac get fix wrt aol from provider-database
aol is not proken, it just needs some prepararions :)

command for updating:
./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs
2020-02-24 23:39:40 +03:00
holger krekel
900a17fc00 another fix, thanks @adbenitez 2020-02-22 17:11:37 +01:00
holger krekel
78f36aaa0d another bug fix 2020-02-22 17:11:37 +01:00
holger krekel
e064e02794 fix eventlogger 2020-02-22 17:11:37 +01:00
holger krekel
e22e5045f1 add missing file, some streamlining 2020-02-22 17:11:37 +01:00
holger krekel
087f35482b factor out imex tracking 2020-02-22 17:11:37 +01:00
holger krekel
23ff5fea28 move towards pluggy 2020-02-22 17:11:37 +01:00
holger krekel
34347ccaf5 strike get_infostring 2020-02-22 17:11:37 +01:00
holger krekel
e704eb6cef move eventlogging to own module, start distinguishing ll events 2020-02-22 17:11:37 +01:00
holger krekel
bf63423fec strike footer and refine index page 2020-02-22 17:11:37 +01:00
holger krekel
f6d71ed8ef strike one Account parameter, always do eventlogging 2020-02-22 17:11:37 +01:00
Alexander Krotov
3c342339a1 imap: use parse_message_id from mimeparser 2020-02-22 16:41:07 +01:00
Alexander Krotov
33463856c5 Use mailparse::msgidparse to parse Message-IDs 2020-02-22 16:41:07 +01:00
holger krekel
a18f4c9b1b prepare py-0.800.0 2020-02-21 14:09:05 +01:00
bjoern
783c7ee4c5 Merge pull request #1303 from deltachat/prep-core25
prepare 1.25.0
2020-02-21 10:41:07 +01:00
B. Petersen
a0b2a692d0 bump version to 1.25.0 2020-02-20 23:49:49 +01:00
B. Petersen
a59d368101 changelog 2020-02-20 23:47:43 +01:00
B. Petersen
5c36fb29ed update python version examples 2020-02-20 23:47:23 +01:00
Alexander Krotov
508b8ef2e2 Improve documentation, mostly by hiding behind pub(crate) 2020-02-20 23:37:13 +03:00
holger krekel
e94c62e5b3 Update src/contact.rs
fix typo
2020-02-20 02:10:56 +01:00
B. Petersen
b65a6c2829 target comment of @hpk42 2020-02-20 02:10:56 +01:00
B. Petersen
c4a20d0798 fix updating names from incoming mails
- if a manual name was never given, always update names from incoming mails
- if a manual name is cleared, fall back to names from incoming mails
2020-02-20 02:10:56 +01:00
B. Petersen
9cb7ea524e add failing test where a contact-name is not updated as expected 2020-02-20 02:10:56 +01:00
holger krekel
0ac0eeda34 better naming, less code 2020-02-20 01:30:21 +01:00
holger krekel
4d066b4fd2 refine processing of errors and result handling 2020-02-20 01:30:21 +01:00
Alexander Krotov
840e321dd9 Process Permanent and Transient SMTP errors 2020-02-20 01:30:21 +01:00
holger krekel
4b6963122b Update src/smtp/mod.rs
Co-Authored-By: Alexander Krotov <ilabdsf@gmail.com>
2020-02-20 01:30:21 +01:00
holger krekel
d5d662bc41 fix ordering error 2020-02-20 01:30:21 +01:00
holger krekel
0b0ed56901 directly attempt to re-connect if the smtp connection is maybe stale
also refactor performing the job-action into own function
2020-02-20 01:30:21 +01:00
holger krekel
13e361aabc add two variants 2020-02-19 14:21:15 +01:00
holger krekel
d1a26e66a7 update 2020-02-19 13:48:05 +01:00
holger krekel
ffe3c84e7c small refinements 2020-02-19 13:39:33 +01:00
holger krekel
702c7382a7 move 2020-02-19 13:35:07 +01:00
holger krekel
b138d486e4 two ideas to tackle group consistency 2020-02-19 13:29:58 +01:00
B. Petersen
3a25d6b275 target comment of @link2xt 2020-02-18 17:51:28 +01:00
B. Petersen
66e2f51233 adapt spec 2020-02-18 17:51:28 +01:00
B. Petersen
8fdb048b6a alter the memberlist more carefully
up to now, Chat-Group-Member-Added and -Removed commands
result in a complete recreation of the memberlist
by collecting all addresses from the From: and To: headers.

this easily results in missed and accidentally removed members,
esp. when several people at the same time scan a qr code to join a group.

this commit changes the behavior of adding members by
not removing members on the Chat-Group-Member-Added command.
instead the existing memberlist is conjuncted
with the memberlist seen in the message.

only adding the member from the Chat-Group-Member-Added
seems not to be sufficient - imaging a group of Alice and Bob:
- Alice adds Claire
- Bob adds Dave _before_ seeing that Alice added Claire
- Dave would never get the information that Claire is in the group

wrt Chat-Group-Member-Removed: this command
does no longer recreate the memberlist but just remove _exactly_ the member
mentioned in the header. there are situations, where a just removed member
will be readded by out-of-order-messages, however, compared to missed
members, this is seems to be acceptable - also as this is more visible
and easier to fix (just remove the member again).
might be that, in practise, this is not a big issue. while adding members
is typically done in masses on bootstraping a group,
this is typically not true for removing members.
2020-02-18 17:51:28 +01:00
B. Petersen
fa3d98a492 add a function to delete records from the chats_contacts table 2020-02-18 17:51:28 +01:00
Alexander Krotov
d9dda44409 Add integration test for RSA and Ed25519 keys
Test that two chat clients using different key types can communicate
using Autocrypt.
2020-02-18 17:51:06 +01:00
Alexander Krotov
7368c01a8f Add key_gen_type config option 2020-02-18 17:51:06 +01:00
Alexander Krotov
21ac5be7ca Change generated key type to Ed25519
rPGP generates EdDSA and and ECDH keys using Ed25519 only, so there is
no need to specify it explicitly anywhere.
2020-02-18 17:51:06 +01:00
B. Petersen
e14a113277 add testrun to provider-db 2020-02-18 17:50:38 +01:00
Alexander Krotov
66d3440675 Update const.py 2020-02-18 11:58:11 +03:00
Alexander Krotov
6b6be3b03d Fix some "`" code markup 2020-02-18 05:06:11 +03:00
bjoern
cda8158bec Merge pull request #1286 from deltachat/spec-location
add location handling to spec
2020-02-17 19:03:30 +01:00
B. Petersen
332e0dc4a8 update providers from provider-db
command:
./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs
2020-02-17 18:18:14 +01:00
B. Petersen
f7b4c6837b add location handling to spec 2020-02-17 17:58:25 +01:00
Asiel Díaz Benítez
531928bf0b Update proxy.py 2020-02-17 14:43:43 +01:00
Asiel Díaz Benítez
490c8e055b Create proxy.py 2020-02-17 14:43:43 +01:00
Alexander Krotov
bcbf192bbc Remove deprecated Chat-Group-Image header 2020-02-17 13:41:50 +01:00
Alexander Krotov
78d855c5ca Include prefer-encrypt attribute in Autocrypt-Gossip headers 2020-02-17 13:39:32 +01:00
Alexander Krotov
1fa9aa88a8 Search for Flag::Deleted and Flag::Seen with == instead of match 2020-02-17 10:50:34 +01:00
Alexander Krotov
08c77c2668 fetch_single_msg: use if let Some(...) instead of is_empty() 2020-02-17 10:50:34 +01:00
Alexander Krotov
793ebe1b0f imap: move IdleHandle from session.rs to idle.rs 2020-02-17 10:50:34 +01:00
Alexander Krotov
4c42acc7e1 Factor src/imap/session.rs out of src/imap/client.rs 2020-02-17 10:50:34 +01:00
Alexander Krotov
4eb9660bfa Move src/imap_client.rs into src/imap/client.rs 2020-02-17 10:50:34 +01:00
Alexander Krotov
8ed08f701d Do not use grpid to find last inserted row in chats table
Instead, use last_insert_rowid() function to find the row.

There is no race condition in using last_insert_rowid(), because
last_insert_rowid() returns row id last inserted in this connection. As
we hold the connection during the whole transaction, it is impossible
that some other thread will execute INSERT statement in parallel.

This commit is part of the effort to get rid of sql::get_rowid hack and
use transactions more for related SQL statements.
2020-02-17 01:35:56 +03:00
Alexander Krotov
784964efad Make sql::with_conn and sql::start_stmt public
Existing public methods that use these functions, like sql::execute, are
only suitable for executing a single statement.

Sometimes it is useful to execute multiple statements within one
connection, for example to begin a transaction, execute mutliple SELECT
and INSERT queries and commit or rollback the whole transaction.
2020-02-17 01:35:56 +03:00
Alexander Krotov
adb96e72b9 Pass mutable Connection reference to with_conn callback
This makes it possible to call .transaction() method on connection.
2020-02-17 01:35:56 +03:00
Alexander Krotov
439c6f7296 Fix a typo 2020-02-17 01:35:56 +03:00
Floris Bruynooghe
e2f1ea1444 Add .strdup() method to Option<AsRef<str>>
We already have a .strdup() method on AsRef<str>, this adds this
method also to an option of this.  In case the option is None a NULL
pointer is returned.

This is done by using a new trait, as the type system otherwise
considers such an implementation conflicting with the existing one.
2020-02-16 23:15:53 +01:00
Alexander Krotov
2977ceb459 Turn deltachat::configure functions into Context methods
Now configure module is no longer public. Users should call
Context.configure() and Context.is_configured() methods.

Configure module is completely hidden from documentation unless
--document-private-items option is specified.
2020-02-16 21:17:03 +01:00
Alexander Krotov
e00d4e0ed8 Remove AvatarAction::None
Where needed, Option<AvatarAction> can be used to extend it.
2020-02-16 15:03:37 +03:00
Alexander Krotov
772127d9d8 Remove outdated comment
delete_msg no longer returns an integer
2020-02-16 03:43:25 +03:00
bjoern
6ba45c88ec Merge pull request #1281 from deltachat/fix-pin-test
fix pinning test by forcing a reliable order
2020-02-16 01:32:37 +01:00
Alexander Krotov
5a4040cf0b cargo fmt 2020-02-16 03:22:30 +03:00
Alexander Krotov
b54f580e66 mimeparser: allow "inline" attachments
RFC 2183 specifically allows filenames to be specified for MIME parts
with "inline" Content-Disposition.

Previously we treated such attachments as an error, causing the whole
message parsing to fail. Now it is only an error if Content-Disposition
cannot be parsed.

MIME parts with filename are now considered to be attachments
regardless of their Content-Disposition type. However, "attachment"
Content-Disposition is still processed differently: we generate a
filename for it even if it is not specified.
2020-02-16 03:22:30 +03:00
B. Petersen
a9ac69fe9c fix pinning test by forcing a reliable order 2020-02-16 00:55:13 +01:00
B. Petersen
5c52b5e404 fix order of shared chats, move pinned up
we forgot to respect the new pinned-state for the list of shared chats
as it is shown eg. in the profile of a contact.
2020-02-15 21:15:01 +01:00
Alexander Krotov
b80360b7da Pass avatar file path to build_selfavatar_file as &str 2020-02-15 22:20:05 +03:00
Alexander Krotov
2753883687 Rename add_selfavatar into attach_selfavatar to match field name 2020-02-15 21:22:25 +03:00
bjoern
ced73ffb14 Merge pull request #1248 from deltachat/pinned_chats
feat: pin chats
2020-02-15 00:15:33 +01:00
Alexander Krotov
672fe2dfd7 Parse KML without converting to UTF-8 and back to bytes 2020-02-14 22:34:41 +01:00
Alexander Krotov
04bb6997a2 Remove unused imap::idle::Error::ImapError variant 2020-02-14 22:33:36 +01:00
B. Petersen
c8a8dbbbae adapt python bindings 2020-02-14 13:43:49 +01:00
B. Petersen
1f9520dc78 target comments from @flub 2020-02-14 13:43:48 +01:00
B. Petersen
84f8627890 fix repl tool 2020-02-14 13:43:48 +01:00
B. Petersen
a177df32b7 omit values in ChatVisibility enum as suggested by @dignifiedquire and @flub 2020-02-14 13:43:48 +01:00
B. Petersen
f25d5dd123 do not unpin chats on sending/receiving messages 2020-02-14 13:43:48 +01:00
B. Petersen
4cfa9e6165 send event as before, uis depend on that 2020-02-14 13:43:48 +01:00
B. Petersen
0303ea7f57 rename to ChatVisibility, simplify ffi 2020-02-14 13:43:48 +01:00
B. Petersen
2813e01e61 remove unneeded sql-roundtrip on getting archived state 2020-02-14 11:28:55 +01:00
B. Petersen
e3420da60f reword ffi from 'archived' to 'visibility' 2020-02-14 11:28:55 +01:00
B. Petersen
60493d30f6 target comment from @dignifiedquire, use ArchiveState inside core 2020-02-14 11:28:55 +01:00
Simon Laux
6efe8e7d7c change ChatInfo.archived to tri-state
and add to the changelog
2020-02-14 11:28:55 +01:00
Simon Laux
2e8409f146 address some of flubs comments 2020-02-14 11:28:55 +01:00
Simon Laux
ac4b2b9dfe python bindings for archive and py tests 2020-02-14 11:28:55 +01:00
Simon Laux
23b6178e78 add rust test for pin chat 2020-02-14 11:28:55 +01:00
Simon Laux
5e5d45fb0a better fallbacks 2020-02-14 11:28:54 +01:00
Simon Laux
1765b8f2cf show pinned chats again and order them to the top 2020-02-14 11:28:54 +01:00
Simon Laux
5678562ce2 represent archivestate as enum
before it was a boolean, even though it is a 3 state
2020-02-14 11:28:54 +01:00
Floris Bruynooghe
7274197da0 Remove comment about method being deprecated
I had a look for two mintues at converting things to .try_inner() and
I don't think this is currently worth the effort or would increase
readability a lot.  So let's leave this for now.
2020-02-14 01:02:28 +01:00
Floris Bruynooghe
c79fcb380b Clean up result traits
There is no need to have both a .log_warn() and .log_err(), even their
names are confusing by now.  Let's just have the most liberal one and
reduce one more thing we need to know about.

Also, these traits don't need to be pub.
2020-02-14 01:02:28 +01:00
Floris Bruynooghe
6a98eade07 Remove the error method from the ffi
No ffi method should be reporting errors to the user.  That's
exclusively the domain of core.  All errors in the ffi are programing
errors.
2020-02-14 01:02:28 +01:00
Alexander Krotov
9008a65c14 Remove DC_IMAP_SEEN constant
Replace "flags" integer with a "seen" boolean.
2020-02-13 13:55:06 +01:00
Alexander Krotov
4e07e4c7f3 Fix a typo (bbc-self instead of bcc-self) 2020-02-12 23:15:04 +03:00
dignifiedquire
e440d8503a fixup 2020-02-12 19:12:22 +01:00
dignifiedquire
e9bacff830 fixup 2020-02-12 19:12:22 +01:00
dignifiedquire
9cc99ffcd6 add more ser and de impls 2020-02-12 19:12:22 +01:00
Alexander Krotov
beb91271de Unlock session before calling add_flag_finalized
add_flag_finalized tries to lock session again and IMAP thread deadlocks
if session is not unlocked.
2020-02-12 11:38:18 +00:00
Alexander Krotov
7e9585ebc5 cargo fmt 2020-02-12 11:38:18 +00:00
Alexander Krotov
0c4b3f71e5 Check for MOVE capability before using MOVE command 2020-02-12 11:38:18 +00:00
holger krekel
5c17ec5f01 use new URL format and service provided by mailadm 2020-02-11 22:48:24 +01:00
B. Petersen
8b4edc46a7 implement dc_set_config_from_qr() 2020-02-11 21:04:18 +01:00
B. Petersen
2b7a0a4585 prototype dc_set_config_from_qr() 2020-02-11 21:04:18 +01:00
B. Petersen
1882176489 let dc_check_qr() accept DCACCOUNT-schemes 2020-02-11 21:04:18 +01:00
holger krekel
875e89e71a better event information for moved messages 2020-02-11 20:38:14 +01:00
Alexander Krotov
52520635ea Fix a typo ("requests") 2020-02-11 22:32:35 +03:00
Friedel Ziegelmayer
aa50a9ba83 Merge pull request #1258 from deltachat/cargo-check-ci
GitHub actions: check all packages, examples, tests and features
2020-02-11 11:50:26 +01:00
Alexander Krotov
489e5111ac GitHub actions: check all packages, examples, tests and features 2020-02-11 02:09:47 +03:00
Alexander Krotov
2d4c20af35 Revert "Make dc_receive_imf non-public"
This reverts commit cabb58a9aa.

dc_receive_imf() is used in the "repl" example.
This was not caught by CI because it does not build examples.
2020-02-11 02:03:36 +03:00
Alexander Krotov
66fdb447f7 Rename get_headerdef into get_header_value 2020-02-11 01:59:41 +03:00
Alexander Krotov
c801775a39 Implement get_headerdef method for MailHeader slices 2020-02-11 01:59:41 +03:00
Alexander Krotov
f5bb57d6a6 Fix a typo ("Messag") 2020-02-11 00:07:37 +03:00
Alexander Krotov
1071ab05db Move created_db_entries into cleanup closure instead of borrowing 2020-02-10 23:31:46 +03:00
Simon Laux
8461cf6443 Merge pull request #1143 from deltachat/feat_mute_chat
feat: mute chat
2020-02-10 01:39:50 +01:00
Alexander Krotov
bb10501f56 Improve test_one_account_send_bcc_setting
Clone the first account and check that second device actually sees the
message copied via BCC-to-self.
2020-02-10 01:17:28 +01:00
Alexander Krotov
c4d5f657da python tests: remove peek_online_config()
Using peek_online_config() results in a message being sent to some
other account, used by previous test. If tests are run in parallel,
for example with pytest-xdist, we could be sending a message to a still
online DC client.
2020-02-10 01:17:28 +01:00
Alexander Krotov
cabb58a9aa Make dc_receive_imf non-public 2020-02-09 23:30:06 +00:00
Alexander Krotov
e64ce5bb4f Reduce is_msgrmsg_rfc724_mid_in_list scope to pub(crate) 2020-02-09 23:28:29 +00:00
Alexander Krotov
bc750d61d2 Remove outdated comment 2020-02-09 23:28:29 +00:00
Alexander Krotov
3150901b6e Improve logs for prefetch checks 2020-02-09 23:28:29 +00:00
Alexander Krotov
4f6745b742 Check if contact is blocked or accepted before downloading it 2020-02-09 23:28:29 +00:00
Alexander Krotov
bcdc323a97 Factor from_field_to_contact_id out of dc_receive_imf 2020-02-09 23:28:29 +00:00
Alexander Krotov
4fd4cc709d Prefetch FROM field 2020-02-09 23:28:29 +00:00
Alexander Krotov
0fac463144 Prefetch Message-ID header instead of envelope
Envelope has the same Message-ID, but using other fields from the
envelope, such as From field, is error-prone. They may contain values
different from the message body. As we don't parse the envelope later on,
it is better not to fetch it during prefetch too.
2020-02-09 23:28:29 +00:00
Alexander Krotov
d809dfac65 Do not download messages that are not displayed 2020-02-09 23:28:29 +00:00
Alexander Krotov
983fd70260 Update imap-proto to 0.10.2
It adds support for parsing of BODY[HEADER.FIELDS (...)] fetches.
2020-02-09 23:28:29 +00:00
Alexander Krotov
ea11a5274e Remove unwrap() in prefetch_get_message_id 2020-02-09 23:28:29 +00:00
Alexander Krotov
42356c2a67 Fix a typo in prefetch_get_message_id 2020-02-09 23:28:29 +00:00
Alexander Krotov
5a84ab2011 Fix a typo (s/ideling/idling/) 2020-02-09 23:28:29 +00:00
Alexander Krotov
b4573e341f Return &'static str from HeaderDef.get_headername() 2020-02-09 19:17:27 +01:00
Alexander Krotov
df252c4704 Do not check for XLIST capability
This capability is not used by Delta Chat. Moreover, XLIST is deprecated
in favor of https://tools.ietf.org/html/rfc6154, which should be
supported instead. Delta Chat already looks for \Sent attribute in
get_folder_meaning().
2020-02-09 19:15:20 +01:00
Alexander Krotov
d1912f873b Resultify fetch_single_msg 2020-02-09 19:14:36 +01:00
Floris Bruynooghe
e525c42c7d Unmute should also raise exceptions
And tests should not care about return values.
2020-02-09 19:12:21 +01:00
Floris Bruynooghe
0242322d24 Tweak error halding a little
- Python should raise exceptions on error.  Not return False.
- Negative durations are nonense.
- ChatID validity is already checked by the Rust API, let's not duplicate.
2020-02-09 18:01:42 +01:00
Simon Laux
ded6fafc8a Merge pull request #1250 from deltachat/flub-feat-mute-chat
Use SystemTime instead of i64
2020-02-09 15:46:19 +01:00
Floris Bruynooghe
4aebd678c3 Use SystemTime instead of i64
This clarifies some behaviour with negative times and propagates
errors a bit better around places.
2020-02-09 13:35:37 +01:00
Simon Laux
afc9ed2274 fix python formatting 2020-02-09 12:36:37 +01:00
Simon Laux
d73d021e3c cargo fmt 2020-02-09 12:25:47 +01:00
bjoern
c8fe81e21d Merge pull request #1243 from Ampli-fier/master
Minor changes in comments
2020-02-09 12:01:31 +01:00
Simon Laux
621f1df913 rename MutedUntilTimestamp to Until 2020-02-09 12:01:09 +01:00
Simon Laux
5f4274b449 fix naming 2020-02-09 10:42:32 +01:00
Simon Laux
4acb37156f fix building of ffi 2020-02-09 10:39:21 +01:00
Alexander Krotov
1ca23a7479 Update link to C core issue 2020-02-09 03:28:59 +03:00
Alexander Krotov
61daf7218d Preconfigure the key in clone_online_account()
This avoids generating the key in test_ac_setup_message
2020-02-08 22:39:34 +00:00
Floris Bruynooghe
dc6671fc4e Add an integration tests which generates a key
Configuring an online account generates a key, we would like this
code-path tested too.  So add some functionality to the AccountManager
to not use the pre-generated keys.

Because this slows down interactively running the tests by hand add an
ignored marker which only runs if --ignored is used.  This name was
chosen because this matches the naming used by rust/cargo #[ignored].
The difference however is that --ignored on cargo *only* runs ignored
tests while here it runs *all* tests.

To ensure the ignored/slow tests are run on CI we add it as an
argument to the tox configuration, which is used by the CI to run the
tests.
2020-02-08 22:39:34 +00:00
Alexander Krotov
f34237ebc8 Run python tests on CI in debug mode
This should speed up compilation and enable additional integer overflow checks.
2020-02-08 16:58:55 +01:00
Alexander Krotov
aadeb3b87e job: do not render messages without recipients 2020-02-08 16:58:55 +01:00
Simon Laux
1fb75c1af3 rename dc_chat_get_mute_duration
to dc_chat_get_remaining_mute_duration
2020-02-08 13:29:24 +01:00
Simon Laux
e04d28c885 use from- and to-sql traits 2020-02-08 13:29:24 +01:00
Simon Laux
6d80b3675a dtransform mute chat to use relative durations
instead of absolute timestamps
2020-02-08 13:26:48 +01:00
Simon Laux
07d698f8dc add python tests 2020-02-08 13:26:21 +01:00
Simon Laux
ef158504e7 remove success variable
(replace with else statement)
2020-02-08 13:26:21 +01:00
Simon Laux
63be1ae5a9 change muted forever to -1 2020-02-08 13:25:22 +01:00
Simon Laux
b9ba1a4f69 implement ffi part 2020-02-08 13:25:22 +01:00
Simon Laux
e006d9b033 rustfmt 2020-02-08 13:25:22 +01:00
Simon Laux
1538684c6c simplify test code 2020-02-08 13:23:55 +01:00
Simon Laux
b37e83caab add possibility to mute chat in core 2020-02-08 13:23:55 +01:00
Simon Laux
d11d3ab08b fix typo 2020-02-08 13:11:22 +01:00
Floris Bruynooghe
1144a536a5 Pre-generate more keys for use in integration tests 2020-02-08 08:28:41 +00:00
Floris Bruynooghe
515c753d11 Use pre-generated keys for python integration tests
This changes the AccountMaker to use pre-generated keys when
available, speeding up test runs.

As a side-effect we no longer need to compile the integration tests in
release mode with debug symbols.  Losing debug symbols (-g) means
cargo no longer wants to recompile everything all the time too.

Tested locally and seems to works.
2020-02-08 08:28:41 +00:00
Alexander Krotov
0864e640ed Simplify Peerstate.peek_key() 2020-02-08 00:02:42 +01:00
Ampli-fier
b876e49393 Minor changes in comments 2020-02-07 20:01:17 +01:00
Floris Bruynooghe
fc0292bf8a Rename save_self_keypair
For the ffi rename to dc_preconfigure_keypair.  For the internal API
to store_self_keypair.
2020-02-06 22:00:29 +01:00
Floris Bruynooghe
a903805cd9 Rename MessageChain to MessageWithCause 2020-02-06 22:00:29 +01:00
Floris Bruynooghe
abab34573e Remove unused method
Forgot to do this earlier.
2020-02-06 22:00:29 +01:00
Floris Bruynooghe
fa1b94af60 Simplify returning None from a Result 2020-02-06 22:00:29 +01:00
Floris Bruynooghe
81ff5d1224 Let save_self_keypair() also remove a previously used key
The imex code was deleting the key if it was already used.  Turns out
none of the callers would not want this behaviour, so let's just
handle this case generally.
2020-02-06 22:00:29 +01:00
Floris Bruynooghe
5c3a8819a4 Avoid pointless conversions, just return the result 2020-02-06 22:00:29 +01:00
Floris Bruynooghe
4d0a08d858 DcKey::to_base64 can not fail
As this can't fail, we don't have to expose Result on this and can
keep the API simpler.
2020-02-06 22:00:29 +01:00
Floris Bruynooghe
c41bdaa2b7 Avoid for-else loop
And here I thought this pythonic
2020-02-06 22:00:29 +01:00
Floris Bruynooghe
4bf2fc18e5 Write with_inner() in function of try_inner()
I guess this is a little neater.  Note that this does change all the
existing error logging to warning logging.  I believe this is correct
as these error log messages are really programming errors rather than
errors which need to be show to the user.

Also chose to make entire with_inner unsafe, there is little to be
gained from hiding the unsafe as this is only used from unsafe
functions in the first place.
2020-02-06 22:00:29 +01:00
Floris Bruynooghe
145fd8657f Update deltachat-ffi/src/lib.rs
Co-Authored-By: Alexander Krotov <ilabdsf@gmail.com>
2020-02-06 22:00:29 +01:00
Floris Bruynooghe
01b55d1d29 Update deltachat-ffi/src/lib.rs
Co-Authored-By: Alexander Krotov <ilabdsf@gmail.com>
2020-02-06 22:00:29 +01:00
Floris Bruynooghe
98b3151c5f Refactor keypair handling and expose storing keypairs on ffi
The user-visible change here is that it allows the FFI API to save
keys in the database for a context.  This is primarily intended for
testing purposes as it allows you to get a key without having to
generate it.

Internally the most important change is to start using the
SignedPublicKey and SignedPrivateKey types from rpgp instead of
wrapping them into a single Key object.  This allows APIs to be
specific about which they want instead of having to do runtime checks
like .is_public() or so.  This means some of the functionality of the
Key impl now needs to be a trait.

A thid API change is to introduce the KeyPair struct, which binds
together the email address, public and private key for a keypair.

All these changes result in a bunch of cleanups, though more more
should be done to completely replace the Key type with the
SignedPublicKye/SignedPrivateKey + traits.  But this change is large
enough already.

Testing-wise this adds two new keys which can be loaded from disk and
and avoids a few more key-generating tests.  The encrypt/decrypt tests
are moved from the stress tests into the pgp tests and split up.
2020-02-06 22:00:29 +01:00
Alexander Krotov
c7eca8deb3 chat: resultify parent_is_encrypted and don't ignore the error
This should prevent accidental sending of unencrypted messages when
parent_is_encrypted returns an error. For example, if the database is
busy due to other thread activity, parent_is_encrypted should not return
false. Instead, message sending job should retry later.
2020-02-04 02:54:56 +03:00
Alexander Krotov
627b54f712 sql: add resultified version of query_get_value 2020-02-04 02:54:56 +03:00
Alexander Krotov
8ef7b6fc54 mimeparser: pass the Context around explicitly 2020-02-03 23:48:55 +03:00
Alexander Krotov
d83652b0fc Add standard reference for UID FETCH quirk 2020-02-02 12:46:31 +00:00
B. Petersen
cce32229e0 do not bubble up errors on dc_has_import()
error!() will show the error directly to the user,
this is not useful in this case,
and also the message with the function-name is not for the end-user.

instead, the ui shall (and already does)
show some explaining text if dc_has_import() returns false;
the error!() is disturbing here as this results in two hints shown to the user.
2020-02-01 22:33:18 +00:00
bjoern
24edd83c8a Merge pull request #1207 from deltachat/provider-db
streamline provider-db
2020-01-30 23:33:04 +01:00
B. Petersen
268c5b6482 update provider-db 2020-01-30 23:31:44 +01:00
B. Petersen
a66a754126 use EmailAddress object correctly 2020-01-30 17:47:43 +01:00
B. Petersen
18059734ce make 'cargo fmt' happy with generated code: avoid two lineends at end of file 2020-01-30 17:47:43 +01:00
B. Petersen
c883e709c3 make sure, a domain is not used twice 2020-01-30 17:47:43 +01:00
B. Petersen
7be0bd3583 skip providers without data, cleanup 2020-01-30 17:47:43 +01:00
B. Petersen
d9ab37ea58 check for missing before_login_hint 2020-01-30 17:47:43 +01:00
B. Petersen
ff075ba612 update provider-db 2020-01-30 17:47:42 +01:00
B. Petersen
9c9294a730 use P_DOMAIN_NAME instead of P_123 to make a diff easier, make sure, domains are lowercase 2020-01-30 17:47:42 +01:00
B. Petersen
2a0842b8ae rough check for valid domains 2020-01-30 17:47:42 +01:00
B. Petersen
3cfe45ffc2 disable a clippy warning, add a comment on that 2020-01-30 17:47:42 +01:00
B. Petersen
80dc7bfc52 make 'cargo fmt' happy 2020-01-30 17:47:42 +01:00
B. Petersen
10f26f17ba replace provider/data.rs by auto-generated file 2020-01-30 17:47:42 +01:00
B. Petersen
4ba7402f28 tweak yaml->rust script, generated code compiles and works now :) 2020-01-30 17:47:42 +01:00
B. Petersen
d4da2e0d9c add update.py that takes a folder with yaml-md-files as argument 2020-01-30 17:47:41 +01:00
B. Petersen
b180d004ba use structure as suggested by @dignifiedquire 2020-01-30 17:47:41 +01:00
B. Petersen
de5bd96f08 add some more tests 2020-01-30 17:47:41 +01:00
B. Petersen
3a05b5dacc target comment of @Simon-Laux, use a subdirectory 2020-01-30 17:47:41 +01:00
B. Petersen
b3c4e32b68 split provider-database from code to allow easy generation 2020-01-30 17:47:41 +01:00
B. Petersen
375a48f135 add before_login_hint to device-chat on successfull logins 2020-01-30 17:47:41 +01:00
B. Petersen
1750ab92e6 actually use server/port/etc. from provider-db, remove hardcoded nauta-settings 2020-01-30 17:47:41 +01:00
B. Petersen
8364ddd10b get and test concrete server-data 2020-01-30 17:47:41 +01:00
B. Petersen
63043cb45d add after_login_hint, refine ffi 2020-01-30 17:47:40 +01:00
B. Petersen
aaa6497659 add server data for nauta to provider-db 2020-01-30 17:47:40 +01:00
dignifiedquire
4fc6fa9c8a lazy_static: make it compile 2020-01-30 17:47:40 +01:00
B. Petersen
7d0dcfb3a5 refine example 2020-01-30 17:47:40 +01:00
B. Petersen
a97ea0ad63 adapt python tests 2020-01-30 17:47:40 +01:00
B. Petersen
da66a4d22f make clippy happy 2020-01-30 17:47:40 +01:00
B. Petersen
748e54d4c2 add basic provider-functions 2020-01-30 17:47:40 +01:00
B. Petersen
0f172595d7 we need the provider-db also in the core, not only ffi. the idea is to add it directly to the core and to avoid an extra crate. this also avoids pulling in yaml-rust and some other dependencies. 2020-01-30 17:47:39 +01:00
B. Petersen
5ffdbd99e8 adapt python bindings, remove tests until we really have data 2020-01-30 17:47:39 +01:00
B. Petersen
fbe57c4c71 adapt provider-db api to real need 2020-01-30 17:47:39 +01:00
Alexander Krotov
61726168d6 examples/simple.rs: fix clippy warnings 2020-01-29 23:29:34 +03:00
B. Petersen
a80632ab36 use RECOMMENDED_FILE_SIZE also for sys.msgsize_max_recommended 2020-01-28 23:32:38 +03:00
Alexander Krotov
893eb8b73b Move message size limits to constants
Corresponding C code:
d31c82478c/src/dc_context.h (L165)
2020-01-28 23:32:38 +03:00
Alexander Krotov
07a5ee7d2c mimeparser: construct MimeMessage at the end of from_bytes()
This reduces the scope of code dealing with mutable structures.
2020-01-28 17:46:16 +03:00
Alexander Krotov
c7a300be2f mimeparser: split parse_headers() into multiple functions 2020-01-28 17:46:16 +03:00
Alexander Krotov
ef842fca89 Increase python test timeout to 90 seconds 2020-01-28 17:44:34 +03:00
Alexander Krotov
bdbe9e1ca5 fix(python): add more checks for _thread_quitflag 2020-01-28 17:44:34 +03:00
Alexander Krotov
0c7f65222c fix(python): add workaround for interrupt_idle race condition 2020-01-28 17:44:34 +03:00
dignifiedquire
a8fa644d25 feat: update to latest async-imap 2020-01-28 17:44:34 +03:00
Alexander Krotov
cf7ccb5b8c configure: do not format! an empty string 2020-01-26 16:51:51 +01:00
B. Petersen
49fdd6fc5b clarify documentation 2020-01-26 16:23:30 +01:00
B. Petersen
5e91c74304 instant success on simple qr-setup-contact
this changes the behavior when scanning a setup-contact qr-code.
instead of waiting, until the whole protocol is finished,
which may take something between 10 seconds and several minutes,
the dc_join_secure_join() returns instantly;
the uis typically show the created chat then.

the returned chat-id is the same than if we wait for the protocol to finish,
it is the opportunistic one-to-one chat-id, so no changes there.

all this works even when both devices are offline.

after dc_join_secure_join() returns, however,
the usual setup-contact continues. ux-wise, once the protocol finishes,
a system-info-messages is added to the chat (also unchanged),
this is directly visible, in the chat as well as in the chatlist.
while the prococol runs, the user can alredy send message to the chat,
or do other things in the app.

if the user scans a new qr-code while an existing protocol is not finished yet,
the old join will be aborted (however, of course, created chats are kept).
we could also allow multiple joins at the same time,
however, this would be much more effort that this little change
and i am not sure if it is worth the effort.

finally, if a verified-group shall be joined,
this is not possible instantly, this is not affected by this pr.
same for unverified-groups, however, this could maybe be improved,
but also here, not sure if it is worth the effort
(i think most scans are setup-contact scans)
2020-01-26 16:23:30 +01:00
Alexander Krotov
b83e6f6e7c Simplify MsgId.is_special() 2020-01-26 16:19:01 +01:00
Alexander Krotov
2687777a82 Remove unused EmailAddress::new() 2020-01-26 16:18:34 +01:00
bjoern
918b8036ea Merge pull request #1223 from deltachat/prep-beta24
prepare 1.0.0-beta24
2020-01-26 14:22:57 +01:00
B. Petersen
7ea0e4d4db bump version to 1.0.0-beta.24 2020-01-26 14:06:33 +01:00
B. Petersen
622c1705aa changelog 2020-01-26 13:49:10 +01:00
Alexander Krotov
3a08929b05 Print "email" JSON-value if it has a wrong type 2020-01-26 12:28:42 +00:00
B. Petersen
99e30d561d fix oauth2 login by fixing a Serde call
oauth2 crashes in beta23
because we did not let Serde remove the quotes from a parsed JSON email-address.

for autoconfig, we try to get a well-known URL
containing the domain of the used address - with the quote that is
https://autoconfig.gmail.com"/mail/config-v1.1.xml?...

unfortunately, instead of just returning an error,
(the url does not exist anyway)
reqwest crashes on the attempt to get this URL.

the Serde-thing is not obvious to me:
while serde_json::Value.as_str() removes the quotes,
serde_json::Value.to_string() does not.
2020-01-26 12:28:42 +00:00
Alexander Krotov
033a44580c Turn get_[fresh_]msg_cnt() into ChatId members 2020-01-25 22:11:13 +00:00
Alexander Krotov
4734bcfbb4 Update Chat.unarchive() comment 2020-01-25 22:11:13 +00:00
Alexander Krotov
099fe6f477 Rename Chat.set_blocking() into set_blocked() 2020-01-25 22:11:13 +00:00
Alexander Krotov
889327b5f6 Rename Chat.archive() into Chat.set_archived() 2020-01-25 22:11:13 +00:00
Alexander Krotov
a2845f44ab Turn ChatId-related functions into methods 2020-01-25 22:11:13 +00:00
Alexander Krotov
a7477516d1 mimefactory: factor out get_location_kml_part 2020-01-25 17:44:19 +00:00
bjoern
938d5828fc Merge pull request #1212 from deltachat/handshake-ignore
do not delete handshake messages possibly belonging to secure-joins on other devices
2020-01-25 15:41:20 +01:00
Alexander Krotov
bf3eab453c Avoid panic in sanitise_name()
.truncate() is not safe because it panics if string length does not
lie on character boundary. We convert the string to characters and take
first n character instead.
2020-01-25 13:04:56 +01:00
Alexander Krotov
ebab893330 dc_tools: remove unused dc_derive_safe_stem_ext()
It had the same .truncate() bug as BlobObject::sanitise_name()
2020-01-25 13:04:56 +01:00
Alexander Krotov
4c67b3a118 Add failing test_sanitise_name() test 2020-01-25 13:04:56 +01:00
B. Petersen
d74b06f8bf target comment of @flub 2020-01-24 21:22:12 +01:00
B. Petersen
c54e211147 do not delete handshake messages maybe belonging to secure-joins on other devices 2020-01-24 12:50:26 +01:00
B. Petersen
8817cf5116 don't make me think 2020-01-24 12:32:45 +01:00
Floris Bruynooghe
fb568513b2 When handling vc-contact-confirm the message is Done
The HandshaeMessage::Propagate return should only occur for
vg-member-added messages since it is for those that we still need to
handle the group add.
2020-01-24 11:26:27 +01:00
Floris Bruynooghe
b4ebfdb84a Fix out of sync cargo.lock
This should have been updated when the version of deltachat_derive was
changed.  Oops.
2020-01-24 11:25:08 +01:00
Floris Bruynooghe
7040bd804a Change to normal sematic version number for this sub-crate
Having a number that resembles the core version number is confusing.
This doesn't matter at all, it just needs to be a version number.
2020-01-22 23:49:01 +01:00
holger krekel
2f2fc17bd8 another try 2020-01-22 15:12:48 +01:00
holger krekel
d5d4b49aaf try to get on-tag-builds to work 2020-01-22 15:11:19 +01:00
holger krekel
042c4efddf update Python README 2020-01-22 14:00:55 +01:00
holger krekel
01251d162c fix wheel upload target, and change python versioning 2020-01-22 14:00:55 +01:00
Alexander Krotov
64026fde7c Store main part of the message outside of "parts" vector 2020-01-22 04:28:35 +00:00
Alexander Krotov
adcdae4abe Factor get_message_kml_part out of render_message 2020-01-22 04:28:35 +00:00
holger krekel
88d138b925 only upload manylinux1 packages as others are rejected by pypi anyway 2020-01-21 23:28:11 +01:00
130 changed files with 10373 additions and 5286 deletions

View File

@@ -7,6 +7,10 @@ executors:
doxygen:
docker:
- image: hrektts/doxygen
python:
docker:
- image: 3.7.7-stretch
restore-workspace: &restore-workspace
attach_workspace:
@@ -174,19 +178,31 @@ workflows:
jobs:
# - cargo_fetch
- remote_tests_rust
- remote_tests_rust:
filters:
tags:
only: /.*/
- remote_tests_python
- remote_tests_python:
filters:
tags:
only: /.*/
- remote_python_packaging:
requires:
- remote_tests_python
- remote_tests_rust
filters:
tags:
only: /.*/
- upload_docs_wheels:
requires:
- remote_python_packaging
- build_doxygen
filters:
tags:
only: /.*/
# - rustfmt:
# requires:
# - cargo_fetch
@@ -194,7 +210,10 @@ workflows:
# requires:
# - cargo_fetch
- build_doxygen
- build_doxygen:
filters:
tags:
only: /.*/
# Linux Desktop 64bit
# - test_x86_64-unknown-linux-gnu:

View File

@@ -10,11 +10,12 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2019-11-06
toolchain: nightly-2020-03-12
override: true
- uses: actions-rs/cargo@v1
with:
command: check
args: --workspace --examples --tests --all-features
fmt:
name: Rustfmt
@@ -24,7 +25,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2019-11-06
toolchain: nightly-2020-03-12
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
@@ -38,10 +39,10 @@ jobs:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly-2019-11-06
toolchain: nightly-2020-03-12
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features
args: --all-features

View File

@@ -1,5 +1,169 @@
# Changelog
## 1.33.0
- let `dc_set_muted()` also mute one-to-one chats #1470
- fix a bug that led to load and traffic if the server does not use sent-folder
#1472
## 1.32.0
- fix endless loop when trying to download messages with bad RFC Message-ID,
also be more reliable on similar errors #1463 #1466 #1462
- fix bug with comma in contact request #1438
- do not refer to hidden messages on replies #1459
- improve error handling #1468 #1465 #1464
## 1.31.0
- always describe the context of the displayed error #1451
- do not emit `DC_EVENT_ERROR` when message sending fails;
`dc_msg_get_state()` and `dc_get_msg_info()` are sufficient #1451
- new config-option `media_quality` #1449
- try over if writing message to database fails #1447
## 1.30.0
- expunge deleted messages #1440
- do not send `DC_EVENT_MSGS_CHANGED|INCOMING_MSG` on hidden messages #1439
## 1.29.0
- new config options `delete_device_after` and `delete_server_after`,
each taking an amount of seconds after which messages
are deleted from the device and/or the server #1310 #1335 #1411 #1417 #1423
- new api `dc_estimate_deletion_cnt()` to estimate the effect
of `delete_device_after` and `delete_server_after`
- use Ed25519 keys by default, these keys are much shorter
than RSA keys, which results in saving traffic and speed improvements #1362
- improve message ellipsizing #1397 #1430
- emit `DC_EVENT_ERROR_NETWORK` also on smtp-errors #1378
- do not show badly formatted non-delta-messages as empty #1384
- try over SMTP on potentially recoverable error 5.5.0 #1379
- remove device-chat from forward-to-chat-list #1367
- improve group-handling #1368
- `dc_get_info()` returns uptime (how long the context is in use)
- python improvements and adaptions #1408 #1415
- log to the stdout and stderr in tests #1416
- refactoring, code improvements #1363 #1365 #1366 #1370 #1375 #1389 #1390 #1418 #1419
- removed api: `dc_chat_get_subtitle()`, `dc_get_version_str()`, `dc_array_add_id()`
- removed events: `DC_EVENT_MEMBER_ADDED`, `DC_EVENT_MEMBER_REMOVED`
## 1.28.0
- new flag DC_GCL_FOR_FORWARDING for dc_get_chatlist()
that will sort the "saved messages" chat to the top of the chatlist #1336
- mark mails as being deleted from server in dc_empty_server() #1333
- fix interaction with servers that do not allow folder creation on root-level;
use path separator as defined by the email server #1359
- fix group creation if group was created by non-delta clients #1357
- fix showing replies from non-delta clients #1353
- fix member list on rejoining left groups #1343
- fix crash when using empty groups #1354
- fix potential crash on special names #1350
## 1.27.0
- handle keys reliably on armv7 #1327
## 1.26.0
- change generated key type back to RSA as shipped versions
have problems to encrypt to Ed25519 keys
- update rPGP to encrypt reliably to Ed25519 keys;
one of the next versions can finally use Ed25519 keys then
## 1.25.0
- save traffic by downloading only messages that are really displayed #1236
- change generated key type to Ed25519, these keys are much shorter
than RSA keys, which results in saving traffic and speed improvements #1287
- improve key handling #1237 #1240 #1242 #1247
- mute handling, apis are dc_set_chat_mute_duration()
dc_chat_is_muted() and dc_chat_get_remaining_mute_duration() #1143
- pinning chats, new apis are dc_set_chat_visibility() and
dc_chat_get_visibility() #1248
- add dc_provider_new_from_email() api that queries the new, integrated
provider-database #1207
- account creation by scanning a qr code
in the DCACCOUNT scheme (https://mailadm.readthedocs.io),
new api is dc_set_config_from_qr() #1249
- if possible, dc_join_securejoin(), returns the new chat-id immediately
and does the handshake in background #1225
- update imap and smtp dependencies #1115
- check for MOVE capability before using MOVE command #1263
- allow inline attachments from RFC 2183 #1280
- fix updating names from incoming mails #1298
- fix error messages shown on import #1234
- directly attempt to re-connect if the smtp connection is maybe stale #1296
- improve adding group members #1291
- improve rust-api #1261
- cleanup #1302 #1283 #1282 #1276 #1270-#1274 #1267 #1258-#1260
#1257 #1239 #1231 #1224
- update spec #1286 #1291
## 1.0.0-beta.24
- fix oauth2/gmail bug introduced in beta23 (not used in releases) #1219
- fix panic when receiving eg. cyrillic filenames #1216
- delete all consumed secure-join handshake messagess #1209 #1212
- rust-level cleanups #1218 #1217 #1210 #1205
- python-level cleanups #1204 #1202 #1201
## 1.0.0-beta.23
- #1197 fix imap-deletion of messages

1413
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.0.0-beta.23"
version = "1.33.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -12,7 +12,7 @@ lto = true
deltachat_derive = { path = "./deltachat_derive" }
libc = "0.2.51"
pgp = { version = "0.4.0", default-features = false }
pgp = { version = "0.5.1", default-features = false }
hex = "0.4.0"
sha2 = "0.8.0"
rand = "0.7.0"
@@ -20,26 +20,19 @@ smallvec = "1.0.0"
reqwest = { version = "0.10.0", features = ["blocking", "json"] }
num-derive = "0.3.0"
num-traits = "0.2.6"
async-smtp = { git = "https://github.com/async-email/async-smtp" }
async-smtp = "0.2"
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
# XXX newer commits of async-imap lead to import-export tests hanging
async-imap = { git = "https://github.com/async-email/async-imap", branch = "dcc-stable" }
async-native-tls = "0.1.1"
async-std = { version = "1.0", features = ["unstable"] }
async-imap = "0.2"
async-native-tls = "0.3.1"
async-std = { version = "1.4", features = ["unstable"] }
base64 = "0.11"
charset = "0.1"
percent-encoding = "2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4.6"
failure = "0.1.5"
failure_derive = "0.1.5"
indexmap = "1.3.0"
# TODO: make optional
rustyline = "4.1.0"
lazy_static = "1.4.0"
regex = "1.1.6"
rusqlite = { version = "0.21", features = ["bundled"] }
@@ -58,10 +51,15 @@ bitflags = "1.1.0"
debug_stub_derive = "0.3.0"
sanitize-filename = "0.2.1"
stop-token = { version = "0.1.1", features = ["unstable"] }
mailparse = "0.10.2"
mailparse = "0.12.0"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
native-tls = "0.2.3"
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
pretty_env_logger = "0.3.1"
rustyline = { version = "4.1.0", optional = true }
thiserror = "1.0.14"
anyhow = "1.0.28"
[dev-dependencies]
tempfile = "3.0"
@@ -82,10 +80,11 @@ path = "examples/simple.rs"
[[example]]
name = "repl"
path = "examples/repl/main.rs"
required-features = ["rustyline"]
[features]
default = ["nightly", "ringbuf"]
vendored = ["native-tls/vendored", "reqwest/native-tls-vendored"]
default = ["nightly"]
vendored = ["async-native-tls/vendored", "reqwest/native-tls-vendored", "async-smtp/native-tls-vendored"]
nightly = ["pgp/nightly"]
ringbuf = ["pgp/ringbuf"]

View File

@@ -17,8 +17,9 @@ curl https://sh.rustup.rs -sSf | sh
Compile and run Delta Chat Core command line utility, using `cargo`:
```
cargo run --example repl -- /path/to/db
cargo run --example repl -- ~/deltachat-db
```
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
Configure your account (if not already configured):
@@ -108,7 +109,6 @@ $ cargo test -- --ignored
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
- `nightly`: Enable nightly only performance and security related features.
- `ringbuf`: Enable the use of [`slice_deque`](https://github.com/gnzlbg/slice_deque) in pgp.
[circle-shield]: https://img.shields.io/circleci/project/github/deltachat/deltachat-core-rust/master.svg?style=flat-square
[circle]: https://circleci.com/gh/deltachat/deltachat-core-rust/

View File

@@ -4,7 +4,7 @@ environment:
install:
- appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
- rustup-init -yv --default-toolchain nightly-2019-07-10
- rustup-init -yv --default-toolchain nightly-2020-03-12
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin
- rustc -vV
- cargo -vV

View File

@@ -37,7 +37,7 @@ echo -----------------------
# Bundle external shared libraries into the wheels
pushd $WHEELHOUSEDIR
pip3 install -U pip
pip3 install -U pip setuptools
pip3 install devpi-client
devpi use https://m.devpi.net
devpi login dc --password $DEVPI_LOGIN

View File

@@ -48,7 +48,7 @@ def run():
projectnames = get_projectnames(baseurl, username, indexname)
if indexname == "master" or not indexname:
continue
assert projectnames == ["deltachat"]
clear_index = not projectnames
for projectname in projectnames:
dates = get_release_dates(baseurl, username, indexname, projectname)
if not dates:
@@ -60,8 +60,11 @@ def run():
date = datetime.datetime(*max(dates))
if (datetime.datetime.now() - date) > datetime.timedelta(days=MAXDAYS):
assert username and indexname
url = baseurl + username + "/" + indexname
subprocess.check_call(["devpi", "index", "-y", "--delete", url])
clear_index = True
break
if clear_index:
url = baseurl + username + "/" + indexname
subprocess.check_call(["devpi", "index", "-y", "--delete", url])

View File

@@ -3,9 +3,9 @@
set -e -x
# Install Rust
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-11-06 -y
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2020-03-12 -y
export PATH=/root/.cargo/bin:$PATH
rustc --version
# remove some 300-400 MB that we don't need for automated builds
rm -rf /root/.rustup/toolchains/nightly-2019-11-06-x86_64-unknown-linux-gnu/share/
rm -rf /root/.rustup/toolchains/nightly-2020-03-12-x86_64-unknown-linux-gnu/share/

View File

@@ -46,6 +46,7 @@ if [ -n "$TESTS" ]; then
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "not qr"
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "qr"
unset DCC_PY_LIVECONFIG
unset DCC_NEW_TMP_EMAIL
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
tox --workdir "$TOXWORKDIR" -e auditwheels
popd

View File

@@ -32,11 +32,11 @@ ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
set +x -e
cd $BUILDDIR
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
set -x
# run everything else inside docker
docker run -e DCC_PY_LIVECONFIG \
docker run -e DCC_NEW_TMP_EMAIL -e DCC_PY_LIVECONFIG \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh
@@ -46,6 +46,6 @@ echo "--- Running $CIRCLE_JOB remotely"
ssh -t $SSHTARGET bash "$BUILDDIR/exec_docker_run"
mkdir -p workspace
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse" workspace/
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse/*manylinux1*" workspace/wheelhouse/
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/dist/*" workspace/wheelhouse/
rsync -avz "$SSHTARGET:$BUILDDIR/python/doc/_build/" workspace/py-docs

View File

@@ -30,6 +30,7 @@ ssh $SSHTARGET <<_HERE
export CARGO_TARGET_DIR=\`pwd\`/../target
export TARGET=release
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
#we rely on tox/virtualenv being available in the host
#rm -rf virtualenv venv

View File

@@ -6,7 +6,7 @@
set -e -x
# for core-building and python install step
export DCC_RS_TARGET=release
export DCC_RS_TARGET=debug
export DCC_RS_DEV=`pwd`
cd python

View File

@@ -37,7 +37,8 @@ mkdir -p $TOXWORKDIR
# XXX we may switch on some live-tests on for better ensurances
# Note that the independent remote_tests_python step does all kinds of
# live-testing already.
unset DCC_PY_LIVECONFIG
unset DCC_PY_LIVECONFIG
unset DCC_NEW_TMP_EMAIL
tox --workdir "$TOXWORKDIR" -e py35,py36,py37,py38,auditwheels
popd

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.0.0-beta.23"
version = "1.33.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -16,15 +16,14 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
deltachat = { path = "../", default-features = false }
deltachat-provider-database = "0.2.1"
libc = "0.2"
human-panic = "1.0.1"
num-traits = "0.2.6"
failure = "0.1.6"
serde_json = "1.0"
anyhow = "1.0.28"
thiserror = "1.0.14"
[features]
default = ["vendored", "nightly", "ringbuf"]
default = ["vendored", "nightly"]
vendored = ["deltachat/vendored"]
nightly = ["deltachat/nightly"]
ringbuf = ["deltachat/ringbuf"]

View File

@@ -41,7 +41,7 @@ typedef struct _dc_provider dc_provider_t;
* uintptr_t event_handler_func(dc_context_t* context, int event,
* uintptr_t data1, uintptr_t data2)
* {
* return 0; // for unhandled events, it is always safe to return 0
* return 0;
* }
*
* dc_context_t* context = dc_context_new(event_handler_func, NULL, NULL);
@@ -208,7 +208,7 @@ typedef struct _dc_provider dc_provider_t;
* @param event one of the @ref DC_EVENT constants
* @param data1 depends on the event parameter
* @param data2 depends on the event parameter
* @return return 0 unless stated otherwise in the event parameter documentation
* @return events do not expect a return value, just always return 0
*/
typedef uintptr_t (*dc_callback_t) (dc_context_t* context, int event, uintptr_t data1, uintptr_t data2);
@@ -229,7 +229,7 @@ typedef uintptr_t (*dc_callback_t) (dc_context_t* context, int event, uintptr_t
* otherwise!
* - The callback SHOULD return _fast_, for GUI updates etc. you should
* post yourself an asynchronous message to your GUI thread, if needed.
* - If not mentioned otherweise, the callback should return 0.
* - events do not expect a return value, just always return 0.
* @param userdata can be used by the client for any purpuse. He finds it
* later in dc_get_userdata().
* @param os_name is only for decorative use
@@ -342,7 +342,8 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `smtp_certificate_checks` = how to check SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way eg. using CC, defaults to empty
* - `selfstatus` = Own status to display eg. in email footers, defaults to a standard text
* - `selfavatar` = File containing avatar. Will be copied to blob directory.
* - `selfavatar` = File containing avatar. Will immediately be copied to the
* `blobdir`; the original image will not be needed anymore.
* NULL to remove the avatar.
* It is planned for future versions
* to send this image together with the next messages.
@@ -364,9 +365,34 @@ char* dc_get_blobdir (const dc_context_t* context);
* also show all mails of confirmed contacts,
* DC_SHOW_EMAILS_ALL (2)=
* also show mails of unconfirmed contacts in the deaddrop.
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
* generate recommended key type (default),
* DC_KEY_GEN_RSA2048 (1)=
* generate RSA 2048 keypair
* DC_KEY_GEN_ED25519 (2)=
* generate Ed25519 keypair
* - `save_mime_headers` = 1=save mime headers
* and make dc_get_mime_headers() work for subsequent calls,
* 0=do not save mime headers (default)
* - `delete_device_after` = 0=do not delete messages from device automatically (default),
* >=1=seconds, after which messages are deleted automatically from the device.
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
* Messages are deleted whether they were seen or not, the UI should clearly point that out.
* See also dc_estimate_deletion_cnt().
* - `delete_server_after` = 0=do not delete messages from server automatically (default),
* 1=delete messages directly after receiving from server, mvbox is skipped.
* >1=seconds, after which messages are deleted automatically from the server, mvbox is used as defined.
* "Saved messages" are deleted from the server as well as
* emails matching the `show_emails` settings above, the UI should clearly point that out.
* See also dc_estimate_deletion_cnt().
* - `media_quality` = DC_MEDIA_QUALITY_BALANCED (0) =
* good outgoing images/videos/voice quality at reasonable sizes (default)
* DC_MEDIA_QUALITY_WORSE (1)
* allow worse images/videos/voice quality to gain smaller sizes,
* suitable for providers or areas known to have a bad connection.
* In contrast to other options, the implementation of this option is currently up to the UIs;
* this may change in future, however,
* having the option in the core allows provider-specific-defaults already today.
*
* If you want to retrieve a value, use dc_get_config().
*
@@ -403,6 +429,7 @@ int dc_set_config (dc_context_t* context, const char*
*/
char* dc_get_config (dc_context_t* context, const char* key);
/**
* Set stock string translation.
*
@@ -417,6 +444,22 @@ char* dc_get_config (dc_context_t* context, const char*
int dc_set_stock_translation(dc_context_t* context, uint32_t stock_id, const char* stock_msg);
/**
* Set configuration values from a QR code containing an account.
* Before this function is called, dc_check_qr() should confirm the type of the
* QR code is DC_QR_ACCOUNT.
*
* Internally, the function will call dc_set_config()
* at least with the keys `addr` and `mail_pw`.
*
* @memberof dc_context_t
* @param context The context object
* @param qr scanned QR code
* @return int (==0 on error, 1 on success)
*/
int dc_set_config_from_qr (dc_context_t* context, const char* qr);
/**
* Get information about the context.
*
@@ -852,11 +895,33 @@ void dc_interrupt_smtp_idle (dc_context_t* context);
void dc_maybe_network (dc_context_t* context);
/**
* Save a keypair as the default keys for the user.
*
* This API is only for testing purposes and should not be used as part of a
* normal application, use the import-export APIs instead.
*
* This saves a public/private keypair as the default keypair in the context.
* It allows avoiding having to generate a secret key for unittests which need
* one.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @param addr The email address of the user. This must match the
* configured_addr setting of the context as well as the UID of the key.
* @param public_data The public key as base64.
* @param secret_data The secret key as base64.
* @return 1 on success, 0 on failure.
*/
int dc_preconfigure_keypair (dc_context_t* context, const char *addr, const char *public_data, const char *secret_data);
// handle chatlists
#define DC_GCL_ARCHIVED_ONLY 0x01
#define DC_GCL_NO_SPECIALS 0x02
#define DC_GCL_ADD_ALLDONE_HINT 0x04
#define DC_GCL_FOR_FORWARDING 0x08
/**
@@ -880,7 +945,7 @@ void dc_maybe_network (dc_context_t* context);
* or "Not now".
* The UI can also offer a "Close" button that calls dc_marknoticed_contact() then.
* - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
* archived _any_ chat using dc_archive_chat(). The UI should show a link as
* archived _any_ chat using dc_set_chat_visibility(). The UI should show a link as
* "Show archived chats", if the user clicks this item, the UI should show a
* list of all archived chats that can be created by this function hen using
* the DC_GCL_ARCHIVED_ONLY flag.
@@ -895,6 +960,10 @@ void dc_maybe_network (dc_context_t* context);
* if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
* the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are _any_ archived
* chats
* - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
* and hides the "Device chat" and the deaddrop.
* typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
* to also hide the archive link.
* - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
* to the list (may be used eg. for selecting chats on forwarding, the flag is
* not needed when DC_GCL_ARCHIVED_ONLY is already set)
@@ -1248,6 +1317,22 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch
int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t chat_id);
/**
* Estimate the number of messages that will be deleted
* by the dc_set_config()-options `delete_device_after` or `delete_server_after`.
* This is typically used to show the estimated impact to the user before actually enabling ephemeral messages.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param from_server 1=Estimate deletion count for server, 0=Estimate deletion count for device
* @param seconds Count messages older than the given number of seconds.
* @return Number of messages that are older than the given number of seconds.
* This includes emails downloaded due to the `show_emails` option.
* Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
*/
int dc_estimate_deletion_cnt (dc_context_t* context, int from_server, int64_t seconds);
/**
* Returns the message IDs of all _fresh_ messages of any chat.
* Typically used for implementing notification summaries.
@@ -1334,25 +1419,18 @@ uint32_t dc_get_next_media (dc_context_t* context, uint32_t ms
/**
* Archive or unarchive a chat.
* Set chat visibility to pinned, archived or normal.
*
* Archived chats are not included in the default chatlist returned
* by dc_get_chatlist(). Instead, if there are _any_ archived chats,
* the pseudo-chat with the chat_id DC_CHAT_ID_ARCHIVED_LINK will be added the the
* end of the chatlist.
*
* - To get a list of archived chats, use dc_get_chatlist() with the flag DC_GCL_ARCHIVED_ONLY.
* - To find out the archived state of a given chat, use dc_chat_get_archived()
* - Messages in archived chats are marked as being noticed, so they do not count as "fresh"
* - Calling this function usually results in the event #DC_EVENT_MSGS_CHANGED
* Calling this function usually results in the event #DC_EVENT_MSGS_CHANGED
* See @ref DC_CHAT_VISIBILITY for detailed information about the visibilities.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat to archive or unarchive.
* @param archive 1=archive chat, 0=unarchive chat, all other values are reserved for future use
* @param chat_id The ID of the chat to change the visibility for.
* @param visibility one of @ref DC_CHAT_VISIBILITY
* @return None.
*/
void dc_archive_chat (dc_context_t* context, uint32_t chat_id, int archive);
void dc_set_chat_visibility (dc_context_t* context, uint32_t chat_id, int visibility);
/**
@@ -1549,13 +1627,31 @@ int dc_set_chat_name (dc_context_t* context, uint32_t ch
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @param chat_id The chat ID to set the image for.
* @param image Full path of the image to use as the group image. If you pass NULL here,
* the group image is deleted (for promoted groups, all members are informed about this change anyway).
* @param image Full path of the image to use as the group image. The image will immediately be copied to the
* `blobdir`; the original image will not be needed anymore.
* If you pass NULL here, the group image is deleted (for promoted groups, all members are informed about
* this change anyway).
* @return 1=success, 0=error
*/
int dc_set_chat_profile_image (dc_context_t* context, uint32_t chat_id, const char* image);
/**
* Set mute duration of a chat.
*
* This value can be checked by the ui upon receiving a new message to decide whether it should trigger an notification.
*
* Sends out #DC_EVENT_CHAT_MODIFIED.
*
* @memberof dc_context_t
* @param chat_id The chat ID to set the mute duration.
* @param duration The duration (0 for no mute, -1 for forever mute, everything else is is the relative mute duration from now in seconds)
* @param context The context as created by dc_context_new().
* @return 1=success, 0=error
*/
int dc_set_chat_mute_duration (dc_context_t* context, uint32_t chat_id, int64_t duration);
// handle messages
/**
@@ -1602,8 +1698,9 @@ char* dc_get_mime_headers (dc_context_t* context, uint32_t ms
*/
void dc_delete_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
/**
/*
* Empty IMAP server folder: delete all messages.
* Deprecated, use dc_set_config() with the key "delete_server_after" instead.
*
* @memberof dc_context_t
* @param context The context object as created by dc_context_new()
@@ -1706,7 +1803,7 @@ int dc_may_be_valid_addr (const char* addr);
/**
* Check if an e-mail address belongs to a known and unblocked contact.
* Known and unblocked contacts will be returned by dc_get_contacts().
* To get a list of all known and unblocked contacts, use dc_get_contacts().
*
* To validate an e-mail address independently of the contact database
* use dc_may_be_valid_addr().
@@ -1714,7 +1811,8 @@ int dc_may_be_valid_addr (const char* addr);
* @memberof dc_context_t
* @param context The context object as created by dc_context_new().
* @param addr The e-mail-address to check.
* @return 1=address is a contact in use, 0=address is not a contact in use.
* @return Contact ID of the contact belonging to the e-mail-address
* or 0 if there is no contact that is or was introduced by an accepted contact.
*/
uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char* addr);
@@ -1756,7 +1854,7 @@ uint32_t dc_create_contact (dc_context_t* context, const char*
* Trying to add email-addresses that are already in the contact list,
* results in updating the name unless the name was changed manually by the user.
* If any email-address or any name is really updated,
* the event DC_EVENT_CONTACTS_CHANGED is sent.
* the event #DC_EVENT_CONTACTS_CHANGED is sent.
*
* To add a single contact entered by the user, you should prefer dc_create_contact(),
* however, for adding a bunch of addresses, this function is _much_ faster.
@@ -2074,6 +2172,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_FPR_OK 210 // id=contact
#define DC_QR_FPR_MISMATCH 220 // id=contact
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
#define DC_QR_ACCOUNT 250 // text1=domain
#define DC_QR_ADDR 320 // id=contact
#define DC_QR_TEXT 330 // text1=text
#define DC_QR_URL 332 // text1=URL
@@ -2091,12 +2190,12 @@ void dc_stop_ongoing_process (dc_context_t* context);
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::test1=Formatted fingerprint
* - DC_QR_ACCOUNT allows creation of an account, dc_lot_t::text1=domain
* - DC_QR_ADDR with dc_lot_t::id=Contact ID
* - DC_QR_TEXT with dc_lot_t::text1=Text
* - DC_QR_URL with dc_lot_t::text1=URL
* - DC_QR_ERROR with dc_lot_t::text1=Error string
*
*
* @memberof dc_context_t
* @param context The context object.
* @param qr The text of the scanned QR code.
@@ -2107,20 +2206,22 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
/**
* Get QR code text that will offer an secure-join verification.
* Get QR code text that will offer an Setup-Contact or Verified-Group invitation.
* The QR code is compatible to the OPENPGP4FPR format
* so that a basic fingerprint comparison also works eg. with OpenKeychain.
*
* The scanning device will pass the scanned content to dc_check_qr() then;
* if this function returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP
* if dc_check_qr() returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP
* an out-of-band-verification can be joined using dc_join_securejoin()
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id If set to a group-chat-id,
* the group-join-protocol is offered in the QR code;
* the Verified-Group-Invite protocol is offered in the QR code;
* works for verified groups as well as for normal groups.
* If set to 0, the setup-Verified-contact-protocol is offered in the QR code.
* If set to 0, the Setup-Contact protocol is offered in the QR code.
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
* @return Text that should go to the QR code,
* On errors, an empty QR code is returned, NULL is never returned.
* The returned string must be released using dc_str_unref() after usage.
@@ -2129,13 +2230,29 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
/**
* Join an out-of-band-verification initiated on another device with dc_get_securejoin_qr().
* Continue a Setup-Contact or Verified-Group-Invite protocol
* started on another device with dc_get_securejoin_qr().
* This function is typically called when dc_check_qr() returns
* lot.state=DC_QR_ASK_VERIFYCONTACT or lot.state=DC_QR_ASK_VERIFYGROUP.
*
* This function takes some time and sends and receives several messages.
* You should call it in a separate thread; if you want to abort it, you should
* call dc_stop_ongoing_process().
* Depending on the given QR code,
* this function may takes some time and sends and receives several messages.
* Therefore, you should call it always in a separate thread;
* if you want to abort it, you should call dc_stop_ongoing_process().
*
* - If the given QR code starts the Setup-Contact protocol,
* the function typically returns immediately
* and the handshake runs in background.
* Subsequent calls of dc_join_securejoin() will abort unfinished tasks.
* The returned chat is the one-to-one opportunistic chat.
* When the protocol has finished, an info-message is added to that chat.
* - If the given QR code starts the Verified-Group-Invite protocol,
* the function waits until the protocol has finished.
* This is because the verified group is not opportunistic
* and can be created only when the contacts have verified each other.
*
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
*
* @memberof dc_context_t
* @param context The context object
@@ -2143,6 +2260,9 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
* to dc_check_qr().
* @return Chat-id of the joined chat, the UI may redirect to the this chat.
* If the out-of-band verification failed or was aborted, 0 is returned.
* A returned chat-id does not guarantee that the chat or the belonging contact is verified.
* If needed, this be checked with dc_chat_is_verified() and dc_contact_is_verified(),
* however, in practise, the UI will just listen to #DC_EVENT_CONTACTS_CHANGED unconditionally.
*/
uint32_t dc_join_securejoin (dc_context_t* context, const char* qr);
@@ -2725,19 +2845,6 @@ int dc_chat_get_type (const dc_chat_t* chat);
char* dc_chat_get_name (const dc_chat_t* chat);
/*
* Get a subtitle for a chat. The subtitle is eg. the email-address or the
* number of group members.
*
* Deprecated function. Subtitles should be created in the ui
* where plural forms and other specials can be handled more gracefully.
*
* @param chat The chat object to calulate the subtitle for.
* @return Subtitle as a string. Must be released using dc_str_unref() after usage. Never NULL.
*/
char* dc_chat_get_subtitle (const dc_chat_t* chat);
/**
* Get the chat's profile image.
* For groups, this is the image set by any group member
@@ -2770,21 +2877,14 @@ uint32_t dc_chat_get_color (const dc_chat_t* chat);
/**
* Get archived state.
*
* - 0 = normal chat, not archived, not sticky.
* - 1 = chat archived
* - 2 = chat sticky (reserved for future use, if you do not support this value, just treat the chat as a normal one)
*
* To archive or unarchive chats, use dc_archive_chat().
* If chats are archived, this should be shown in the UI by a little icon or text,
* eg. the search will also return archived chats.
* Get visibility of chat.
* See @ref DC_CHAT_VISIBILITY for detailed information about the visibilities.
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return Archived state.
* @return One of @ref DC_CHAT_VISIBILITY
*/
int dc_chat_get_archived (const dc_chat_t* chat);
int dc_chat_get_visibility (const dc_chat_t* chat);
/**
@@ -2877,6 +2977,26 @@ int dc_chat_is_verified (const dc_chat_t* chat);
int dc_chat_is_sending_locations (const dc_chat_t* chat);
/**
* Check whether the chat is currently muted
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=muted, 0=not muted
*/
int dc_chat_is_muted (const dc_chat_t* chat);
/**
* Get the exact state of the mute of a chat
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return 0=not muted, -1=forever muted, (x>0)=remaining seconds until the mute is lifted
*/
int64_t dc_chat_get_remaining_mute_duration (const dc_chat_t* chat);
/**
* @class dc_msg_t
*
@@ -3665,30 +3785,19 @@ int dc_contact_is_verified (dc_contact_t* contact);
*/
/**
* Create a provider struct for the given domain.
*
* @memberof dc_provider_t
* @param domain The domain to get provider info for.
* @return a dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
*/
dc_provider_t* dc_provider_new_from_domain (const char* domain);
/**
* Create a provider struct for the given email address.
*
* The provider is extracted from the email address and it's information is returned.
*
* @memberof dc_provider_t
* @param context The context object as created by dc_context_new().
* @param email The user's email address to extract the provider info form.
* @return a dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
*/
dc_provider_t* dc_provider_new_from_email (const char* email);
dc_provider_t* dc_provider_new_from_email (const dc_context_t* context, const char* email);
/**
@@ -3698,53 +3807,35 @@ dc_provider_t* dc_provider_new_from_email (const char* email);
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string which must be released using dc_str_unref().
* @return String with a fully-qualified URL,
* if there is no such URL, an empty string is returned, NULL is never returned.
* The returned value must be released using dc_str_unref().
*/
char* dc_provider_get_overview_page (const dc_provider_t* provider);
/**
* The provider's name.
* Get hints to be shown to the user on the login screen.
* Depending on the @ref DC_PROVIDER_STATUS returned by dc_provider_get_status(),
* the ui may want to highlight the hint.
*
* The name of the provider, e.g. "POSTEO".
* Moreover, the ui should display a "More information" link
* that forwards to the url returned by dc_provider_get_overview_page().
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string which must be released using dc_str_unref().
* @return A string with the hint to show to the user, may contain multiple lines,
* if there is no such hint, an empty string is returned, NULL is never returned.
* The returned value must be released using dc_str_unref().
*/
char* dc_provider_get_name (const dc_provider_t* provider);
/**
* The markdown content of the providers page.
*
* This contains the preparation steps or additional information if the status
* is @ref DC_PROVIDER_STATUS_BROKEN.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string which must be released using dc_str_unref().
*/
char* dc_provider_get_markdown (const dc_provider_t* provider);
/**
* Date of when the state was last checked/updated.
*
* This is returned as a string.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string which must be released using dc_str_unref().
*/
char* dc_provider_get_status_date (const dc_provider_t* provider);
char* dc_provider_get_before_login_hint (const dc_provider_t* provider);
/**
* Whether DC works with this provider.
*
* Can be one of @ref DC_PROVIDER_STATUS_OK, @ref
* DC_PROVIDER_STATUS_PREPARATION and @ref DC_PROVIDER_STATUS_BROKEN.
* Can be one of #DC_PROVIDER_STATUS_OK,
* #DC_PROVIDER_STATUS_PREPARATION or #DC_PROVIDER_STATUS_BROKEN.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
@@ -3759,7 +3850,7 @@ int dc_provider_get_status (const dc_provider_t* prov
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
*/
void dc_provider_unref (const dc_provider_t* provider);
void dc_provider_unref (dc_provider_t* provider);
/**
@@ -4065,28 +4156,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
/**
* @defgroup DC_EMPTY DC_EMPTY
*
* These constants configure emptying imap folders with dc_empty_server()
*
* @addtogroup DC_EMPTY
* @{
*/
/**
* Clear all mvbox messages.
*/
#define DC_EMPTY_MVBOX 0x01
/**
* Clear all INBOX messages.
*/
#define DC_EMPTY_INBOX 0x02
/**
* @}
*/
#define DC_EMPTY_MVBOX 0x01 // Deprecated, flag for dc_empty_server(): Clear all mvbox messages
#define DC_EMPTY_INBOX 0x02 // Deprecated, flag for dc_empty_server(): Clear all INBOX messages
/**
@@ -4094,8 +4165,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* These constants are used as events
* reported to the callback given to dc_context_new().
* If you do not want to handle an event, it is always safe to return 0,
* so there is no need to add a "case" for every event.
*
* @addtogroup DC_EVENT
* @{
@@ -4110,7 +4179,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_INFO 100
@@ -4121,7 +4189,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_SMTP_CONNECTED 101
@@ -4132,7 +4199,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_IMAP_CONNECTED 102
@@ -4142,7 +4208,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_SMTP_MESSAGE_SENT 103
@@ -4152,7 +4217,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_IMAP_MESSAGE_DELETED 104
@@ -4162,7 +4226,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_IMAP_MESSAGE_MOVED 105
@@ -4172,7 +4235,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) folder name.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_IMAP_FOLDER_EMPTIED 106
@@ -4182,7 +4244,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) path name
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_NEW_BLOB_FILE 150
@@ -4192,7 +4253,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) path name
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_DELETED_BLOB_FILE 151
@@ -4205,7 +4265,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) Warning string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_WARNING 300
@@ -4228,7 +4287,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* Some error strings are taken from dc_set_stock_translation(),
* however, most error strings will be in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_ERROR 400
@@ -4252,7 +4310,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* 0=subsequent network error, should be logged only
* @param data2 (const char*) Error string, always set, never NULL.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_ERROR_NETWORK 401
@@ -4268,7 +4325,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data2 (const char*) Info string in english language.
* Must not be unref'd or modified
* and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_ERROR_SELF_NOT_IN_GROUP 410
@@ -4282,7 +4338,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) chat_id for single added messages
* @param data2 (int) msg_id for single added messages
* @return 0
*/
#define DC_EVENT_MSGS_CHANGED 2000
@@ -4295,7 +4350,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
* @return 0
*/
#define DC_EVENT_INCOMING_MSG 2005
@@ -4306,7 +4360,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
* @return 0
*/
#define DC_EVENT_MSG_DELIVERED 2010
@@ -4317,7 +4370,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
* @return 0
*/
#define DC_EVENT_MSG_FAILED 2012
@@ -4328,7 +4380,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
* @return 0
*/
#define DC_EVENT_MSG_READ 2015
@@ -4341,17 +4392,15 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) chat_id
* @param data2 0
* @return 0
*/
#define DC_EVENT_CHAT_MODIFIED 2020
/**
* Contact(s) created, renamed, blocked or deleted.
* Contact(s) created, renamed, verified, blocked or deleted.
*
* @param data1 (int) If not 0, this is the contact_id of an added contact that should be selected.
* @param data2 0
* @return 0
*/
#define DC_EVENT_CONTACTS_CHANGED 2030
@@ -4364,7 +4413,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* If the locations of several contacts have been changed,
* eg. after calling dc_delete_all_locations(), this parameter is set to 0.
* @param data2 0
* @return 0
*/
#define DC_EVENT_LOCATION_CHANGED 2035
@@ -4374,7 +4422,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) 0=error, 1-999=progress in permille, 1000=success and done
* @param data2 0
* @return 0
*/
#define DC_EVENT_CONFIGURE_PROGRESS 2041
@@ -4384,7 +4431,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) 0=error, 1-999=progress in permille, 1000=success and done
* @param data2 0
* @return 0
*/
#define DC_EVENT_IMEX_PROGRESS 2051
@@ -4399,7 +4445,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 (const char*) Path and file name.
* Must not be unref'd or modified and is valid only until the callback returns.
* @param data2 0
* @return 0
*/
#define DC_EVENT_IMEX_FILE_WRITTEN 2052
@@ -4417,7 +4462,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
* 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
* 1000=Protocol finished for this contact.
* @return 0
*/
#define DC_EVENT_SECUREJOIN_INVITER_PROGRESS 2060
@@ -4433,21 +4477,9 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data2 (int) Progress as:
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
* (Bob has verified alice and waits until Alice does the same for him)
* @return 0
*/
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
/**
* This event is sent out to the inviter when a joiner successfully joined a group.
*
* @param data1 (int) chat_id
* @param data2 (int) contact_id
* @return 0
*/
#define DC_EVENT_SECUREJOIN_MEMBER_ADDED 2062
/**
* @}
*/
@@ -4463,8 +4495,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_EVENT_DATA2_IS_STRING(e) ((e)>=100 && (e)<=499)
#define DC_EVENT_RETURNS_INT(e) ((e)==DC_EVENT_IS_OFFLINE) // not used anymore
#define DC_EVENT_RETURNS_STRING(e) ((e)==DC_EVENT_GET_STRING) // not used anymore
char* dc_get_version_str (void); // deprecated
void dc_array_add_id (dc_array_t*, uint32_t); // deprecated
#define dc_archive_chat(a,b,c) dc_set_chat_visibility((a), (b), (c)? 1 : 0) // not used anymore
#define dc_chat_get_archived(a) (dc_chat_get_visibility((a))==1? 1 : 0) // not used anymore
/*
@@ -4475,6 +4507,21 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
#define DC_SHOW_EMAILS_ALL 2
/*
* Values for dc_get|set_config("media_quality")
*/
#define DC_MEDIA_QUALITY_BALANCED 0
#define DC_MEDIA_QUALITY_WORSE 1
/*
* Values for dc_get|set_config("key_gen_type")
*/
#define DC_KEY_GEN_DEFAULT 0
#define DC_KEY_GEN_RSA2048 1
#define DC_KEY_GEN_ED25519 2
/**
* @defgroup DC_PROVIDER_STATUS DC_PROVIDER_STATUS
*
@@ -4485,23 +4532,43 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
*/
/**
* Provider status returned by dc_provider_get_status().
* Prover works out-of-the-box.
* This provider status is returned for provider where the login
* works by just entering the name or the email-address.
*
* Works right out of the box without any preperation steps needed
* - There is no need for the user to do any special things
* (enable IMAP or so) in the provider's webinterface or at other places.
* - There is no need for the user to enter advanced settings;
* server, port etc. are known by the core.
*
* The status is returned by dc_provider_get_status().
*/
#define DC_PROVIDER_STATUS_OK 1
/**
* Provider status returned by dc_provider_get_status().
* Provider works, but there are preparations needed.
*
* Works, but preparation steps are needed
* - The user has to do some special things as "Enable IMAP in the Webinterface",
* what exactly, is described in the string returnd by dc_provider_get_before_login_hints()
* and, typically more detailed, in the page linked by dc_provider_get_overview_page().
* - There is no need for the user to enter advanced settings;
* server, port etc. should be known by the core.
*
* The status is returned by dc_provider_get_status().
*/
#define DC_PROVIDER_STATUS_PREPARATION 2
/**
* Provider status returned by dc_provider_get_status().
* Provider is not working.
* This provider status is returned for providers
* that are known to not work with Delta Chat.
* The ui should block logging in with this provider.
*
* Doesn't work (too unstable to use falls also in this category)
* More information about that is typically provided
* in the string returned by dc_provider_get_before_login_hints()
* and in the page linked by dc_provider_get_overview_page().
*
* The status is returned by dc_provider_get_status().
*/
#define DC_PROVIDER_STATUS_BROKEN 3
@@ -4510,6 +4577,48 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
*/
/**
* @defgroup DC_CHAT_VISIBILITY DC_CHAT_VISIBILITY
*
* These constants describe the visibility of a chat.
* The chat visibiliry can be get using dc_chat_get_visibility()
* and set using dc_set_chat_visibility().
*
* @addtogroup DC_CHAT_VISIBILITY
* @{
*/
/**
* Chats with normal visibility are not archived and are shown below all pinned chats.
* Archived chats, that receive new messages automatically become normal chats.
*/
#define DC_CHAT_VISIBILITY_NORMAL 0
/**
* Archived chats are not included in the default chatlist returned by dc_get_chatlist().
* Instead, if there are _any_ archived chats, the pseudo-chat
* with the chat_id DC_CHAT_ID_ARCHIVED_LINK will be added the the end of the chatlist.
*
* The UI typically shows a little icon or chats beside archived chats in the chatlist,
* this is needed as eg. the search will also return archived chats.
*
* If archived chats receive new messages, they become normal chats again.
*
* To get a list of archived chats, use dc_get_chatlist() with the flag DC_GCL_ARCHIVED_ONLY.
*/
#define DC_CHAT_VISIBILITY_ARCHIVED 1
/**
* Pinned chats are included in the default chatlist. moreover,
* they are always the first items, whether they have fresh messages or not.
*/
#define DC_CHAT_VISIBILITY_PINNED 2
/**
* @}
*/
/*
* TODO: Strings need some doumentation about used placeholders.
*
@@ -4523,8 +4632,6 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
#define DC_STR_NOMESSAGES 1
#define DC_STR_SELF 2
#define DC_STR_DRAFT 3
#define DC_STR_MEMBER 4
#define DC_STR_CONTACT 6
#define DC_STR_VOICEMESSAGE 7
#define DC_STR_DEADDROP 8
#define DC_STR_IMAGE 9
@@ -4556,7 +4663,6 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
#define DC_STR_STARREDMSGS 41
#define DC_STR_AC_SETUP_MSG_SUBJECT 42
#define DC_STR_AC_SETUP_MSG_BODY 43
#define DC_STR_SELFTALK_SUBTITLE 50
#define DC_STR_CANNOT_LOGIN 60
#define DC_STR_SERVER_RESPONSE 61
#define DC_STR_MSGACTIONBYUSER 62

View File

@@ -13,21 +13,23 @@ extern crate human_panic;
extern crate num_traits;
extern crate serde_json;
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::convert::TryInto;
use std::ffi::CString;
use std::fmt::Write;
use std::ptr;
use std::str::FromStr;
use std::sync::RwLock;
use std::time::{Duration, SystemTime};
use libc::uintptr_t;
use num_traits::{FromPrimitive, ToPrimitive};
use deltachat::chat::ChatId;
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::Contact;
use deltachat::contact::{Contact, Origin};
use deltachat::context::Context;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::stock::StockMessage;
use deltachat::*;
@@ -83,15 +85,12 @@ pub type dc_callback_t =
pub type dc_context_t = ContextWrapper;
impl ContextWrapper {
/// Log an error on the FFI context.
/// Log a warning on the FFI context.
///
/// As soon as a [ContextWrapper] exist it can be used to log an
/// error using the callback, even before [dc_context_open] is
/// called and an actual [Context] exists.
///
/// This function makes it easy to log an error.
unsafe fn error(&self, msg: &str) {
self.translate_cb(Event::Error(msg.to_string()));
/// Like [error] but logs as a warning which only goes to the
/// logfile rather than being shown directly to the user.
unsafe fn warning(&self, msg: &str) {
self.translate_cb(Event::Warning(msg.to_string()));
}
/// Unlock the context and execute a closure with it.
@@ -109,17 +108,29 @@ impl ContextWrapper {
/// the appropriate return value for an error return since this
/// differs for various functions on the FFI API: sometimes 0,
/// NULL, an empty string etc.
fn with_inner<T, F>(&self, ctxfn: F) -> Result<T, ()>
unsafe fn with_inner<T, F>(&self, ctxfn: F) -> Result<T, ()>
where
F: FnOnce(&Context) -> T,
{
self.try_inner(|ctx| Ok(ctxfn(ctx))).map_err(|err| {
self.warning(&err.to_string());
})
}
/// Unlock the context and execute a closure with it.
///
/// This is like [ContextWrapper::with_inner] but uses
/// [anyhow::Error] as error type. This allows you to write a
/// closure which could produce many errors, use the `?` operator
/// to return them and handle them all as the return of this call.
fn try_inner<T, F>(&self, ctxfn: F) -> Result<T, anyhow::Error>
where
F: FnOnce(&Context) -> Result<T, anyhow::Error>,
{
let guard = self.inner.read().unwrap();
match guard.as_ref() {
Some(ref ctx) => Ok(ctxfn(ctx)),
None => {
unsafe { self.error("context not open") };
Err(())
}
Some(ref ctx) => ctxfn(ctx),
None => Err(anyhow::format_err!("context not open")),
}
}
@@ -185,17 +196,6 @@ impl ContextWrapper {
progress as uintptr_t,
);
}
Event::SecurejoinMemberAdded {
chat_id,
contact_id,
} => {
ffi_cb(
self,
event_id,
chat_id.to_u32() as uintptr_t,
contact_id as uintptr_t,
);
}
}
}
}
@@ -340,7 +340,7 @@ pub unsafe extern "C" fn dc_set_config(
})
.unwrap_or(0),
Err(_) => {
ffi_context.error("dc_set_config(): invalid key");
ffi_context.warning("dc_set_config(): invalid key");
0
}
}
@@ -361,7 +361,7 @@ pub unsafe extern "C" fn dc_get_config(
.with_inner(|ctx| ctx.get_config(key).unwrap_or_default().strdup())
.unwrap_or_else(|_| "".strdup()),
Err(_) => {
ffi_context.error("dc_get_config(): invalid key");
ffi_context.warning("dc_get_config(): invalid key");
"".strdup()
}
}
@@ -396,11 +396,33 @@ pub unsafe extern "C" fn dc_set_stock_translation(
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_config_from_qr(
context: *mut dc_context_t,
qr: *mut libc::c_char,
) -> libc::c_int {
if context.is_null() || qr.is_null() {
eprintln!("ignoring careless call to dc_set_config_from_qr");
return 0;
}
let qr = to_string_lossy(qr);
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| match qr::set_config_from_qr(ctx, &qr) {
Ok(()) => 1,
Err(err) => {
error!(ctx, "Failed to create account from QR code: {}", err);
0
}
})
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_info(context: *mut dc_context_t) -> *mut libc::c_char {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_info()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_context = &*context;
let guard = ffi_context.inner.read().unwrap();
@@ -412,7 +434,7 @@ pub unsafe extern "C" fn dc_get_info(context: *mut dc_context_t) -> *mut libc::c
}
fn render_info(
info: HashMap<&'static str, String>,
info: BTreeMap<&'static str, String>,
) -> std::result::Result<String, std::fmt::Error> {
let mut res = String::new();
for (key, value) in &info {
@@ -443,11 +465,6 @@ pub unsafe extern "C" fn dc_get_oauth2_url(
.unwrap_or_else(|_| ptr::null_mut())
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_version_str() -> *mut libc::c_char {
context::get_version_str().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_configure(context: *mut dc_context_t) {
if context.is_null() {
@@ -455,9 +472,7 @@ pub unsafe extern "C" fn dc_configure(context: *mut dc_context_t) {
return;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| configure::configure(ctx))
.unwrap_or(())
ffi_context.with_inner(|ctx| ctx.configure()).unwrap_or(())
}
#[no_mangle]
@@ -468,7 +483,7 @@ pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| configure::dc_is_configured(ctx) as libc::c_int)
.with_inner(|ctx| ctx.is_configured() as libc::c_int)
.unwrap_or(0)
}
@@ -665,6 +680,35 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_preconfigure_keypair(
context: *mut dc_context_t,
addr: *const libc::c_char,
public_data: *const libc::c_char,
secret_data: *const libc::c_char,
) -> i32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_preconfigure_keypair()");
return 0;
}
let ffi_context = &*context;
ffi_context
.try_inner(|ctx| {
let addr = dc_tools::EmailAddress::new(&to_string_lossy(addr))?;
let public = key::SignedPublicKey::from_base64(&to_string_lossy(public_data))?;
let secret = key::SignedSecretKey::from_base64(&to_string_lossy(secret_data))?;
let keypair = key::KeyPair {
addr,
public,
secret,
};
key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default)?;
Ok(1)
})
.log_err(ffi_context, "Failed to save keypair")
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_chatlist(
context: *mut dc_context_t,
@@ -708,7 +752,7 @@ pub unsafe extern "C" fn dc_create_chat_by_msg_id(context: *mut dc_context_t, ms
ffi_context
.with_inner(|ctx| {
chat::create_by_msg_id(ctx, MsgId::new(msg_id))
.log_err(ctx, "Failed to create chat from msg_id")
.log_err(ffi_context, "Failed to create chat from msg_id")
.map(|id| id.to_u32())
.unwrap_or(0)
})
@@ -728,7 +772,7 @@ pub unsafe extern "C" fn dc_create_chat_by_contact_id(
ffi_context
.with_inner(|ctx| {
chat::create_by_contact_id(ctx, contact_id)
.log_err(ctx, "Failed to create chat from contact_id")
.log_err(ffi_context, "Failed to create chat from contact_id")
.map(|id| id.to_u32())
.unwrap_or(0)
})
@@ -748,7 +792,7 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
ffi_context
.with_inner(|ctx| {
chat::get_by_contact_id(ctx, contact_id)
.log_err(ctx, "Failed to get chat for contact_id")
.log_err(ffi_context, "Failed to get chat for contact_id")
.map(|id| id.to_u32())
.unwrap_or(0)
})
@@ -836,7 +880,7 @@ pub unsafe extern "C" fn dc_set_draft(
Some(&mut ffi_msg.message)
};
ffi_context
.with_inner(|ctx| chat::set_draft(ctx, ChatId::new(chat_id), msg))
.with_inner(|ctx| ChatId::new(chat_id).set_draft(ctx, msg))
.unwrap_or(())
}
@@ -911,7 +955,7 @@ pub unsafe extern "C" fn dc_get_draft(context: *mut dc_context_t, chat_id: u32)
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| match chat::get_draft(ctx, ChatId::new(chat_id)) {
.with_inner(|ctx| match ChatId::new(chat_id).get_draft(ctx) {
Ok(Some(draft)) => {
let ffi_msg = MessageWrapper {
context,
@@ -966,7 +1010,7 @@ pub unsafe extern "C" fn dc_get_msg_cnt(context: *mut dc_context_t, chat_id: u32
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| chat::get_msg_cnt(ctx, ChatId::new(chat_id)) as libc::c_int)
.with_inner(|ctx| ChatId::new(chat_id).get_msg_cnt(ctx) as libc::c_int)
.unwrap_or(0)
}
@@ -981,7 +1025,26 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| chat::get_fresh_msg_cnt(ctx, ChatId::new(chat_id)) as libc::c_int)
.with_inner(|ctx| ChatId::new(chat_id).get_fresh_msg_cnt(ctx) as libc::c_int)
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_estimate_deletion_cnt(
context: *mut dc_context_t,
from_server: libc::c_int,
seconds: i64,
) -> libc::c_int {
if context.is_null() || seconds < 0 {
eprintln!("ignoring careless call to dc_estimate_deletion_cnt()");
return 0;
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
message::estimate_deletion_cnt(ctx, from_server != 0, seconds).unwrap_or(0)
as libc::c_int
})
.unwrap_or(0)
}
@@ -1017,7 +1080,7 @@ pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id
ffi_context
.with_inner(|ctx| {
chat::marknoticed_chat(ctx, ChatId::new(chat_id))
.log_err(ctx, "Failed marknoticed chat")
.log_err(ffi_context, "Failed marknoticed chat")
.unwrap_or(())
})
.unwrap_or(())
@@ -1033,7 +1096,7 @@ pub unsafe extern "C" fn dc_marknoticed_all_chats(context: *mut dc_context_t) {
ffi_context
.with_inner(|ctx| {
chat::marknoticed_all_chats(ctx)
.log_err(ctx, "Failed marknoticed all chats")
.log_err(ffi_context, "Failed marknoticed all chats")
.unwrap_or(())
})
.unwrap_or(())
@@ -1126,27 +1189,32 @@ pub unsafe extern "C" fn dc_get_next_media(
}
#[no_mangle]
pub unsafe extern "C" fn dc_archive_chat(
pub unsafe extern "C" fn dc_set_chat_visibility(
context: *mut dc_context_t,
chat_id: u32,
archive: libc::c_int,
) {
if context.is_null() {
eprintln!("ignoring careless call to dc_archive_chat()");
eprintln!("ignoring careless call to dc_set_chat_visibility()");
return;
}
let ffi_context = &*context;
let archive = if archive == 0 {
false
} else if archive == 1 {
true
} else {
return;
let visibility = match archive {
0 => ChatVisibility::Normal,
1 => ChatVisibility::Archived,
2 => ChatVisibility::Pinned,
_ => {
ffi_context.warning(
"ignoring careless call to dc_set_chat_visibility(): unknown archived state",
);
return;
}
};
ffi_context
.with_inner(|ctx| {
chat::archive(ctx, ChatId::new(chat_id), archive)
.log_err(ctx, "Failed archive chat")
ChatId::new(chat_id)
.set_visibility(ctx, visibility)
.log_err(ffi_context, "Failed setting chat visibility")
.unwrap_or(())
})
.unwrap_or(())
@@ -1161,8 +1229,9 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
chat::delete(ctx, ChatId::new(chat_id))
.log_err(ctx, "Failed chat delete")
ChatId::new(chat_id)
.delete(ctx)
.log_err(ffi_context, "Failed chat delete")
.unwrap_or(())
})
.unwrap_or(())
@@ -1249,7 +1318,7 @@ pub unsafe extern "C" fn dc_create_group_chat(
ffi_context
.with_inner(|ctx| {
chat::create_group_chat(ctx, verified, to_string_lossy(name))
.log_err(ctx, "Failed to create group chat")
.log_err(ffi_context, "Failed to create group chat")
.map(|id| id.to_u32())
.unwrap_or(0)
})
@@ -1351,6 +1420,37 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_mute_duration(
context: *mut dc_context_t,
chat_id: u32,
duration: i64,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_set_chat_mute_duration()");
return 0;
}
let ffi_context = &*context;
let muteDuration = match duration {
0 => MuteDuration::NotMuted,
-1 => MuteDuration::Forever,
n if n > 0 => MuteDuration::Until(SystemTime::now() + Duration::from_secs(duration as u64)),
_ => {
ffi_context.warning(
"dc_chat_set_mute_duration(): Can not use negative duration other than -1",
);
return 0;
}
};
ffi_context
.with_inner(|ctx| {
chat::set_muted(ctx, ChatId::new(chat_id), muteDuration)
.map(|_| 1)
.unwrap_or_log_default(ctx, "Failed to set mute duration")
})
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_msg_info(
context: *mut dc_context_t,
@@ -1358,7 +1458,7 @@ pub unsafe extern "C" fn dc_get_msg_info(
) -> *mut libc::c_char {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_msg_info()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_context = &*context;
ffi_context
@@ -1541,7 +1641,9 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr(
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| Contact::lookup_id_by_addr(ctx, to_string_lossy(addr)))
.with_inner(|ctx| {
Contact::lookup_id_by_addr(ctx, to_string_lossy(addr), Origin::IncomingReplyTo)
})
.unwrap_or(0)
}
@@ -1753,7 +1855,9 @@ pub unsafe extern "C" fn dc_imex_has_backup(
.with_inner(|ctx| match imex::has_backup(ctx, to_string_lossy(dir)) {
Ok(res) => res.strdup(),
Err(err) => {
error!(ctx, "dc_imex_has_backup: {}", err);
// do not bubble up error to the user,
// the ui will expect that the file does not exist or cannot be accessed
warn!(ctx, "dc_imex_has_backup: {}", err);
ptr::null_mut()
}
})
@@ -1955,7 +2059,9 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| location::delete_all(ctx).log_err(ctx, "Failed to delete locations"))
.with_inner(|ctx| {
location::delete_all(ctx).log_err(ffi_context, "Failed to delete locations")
})
.ok();
}
@@ -1974,16 +2080,6 @@ pub unsafe extern "C" fn dc_array_unref(a: *mut dc_array::dc_array_t) {
Box::from_raw(a);
}
#[no_mangle]
pub unsafe extern "C" fn dc_array_add_id(array: *mut dc_array_t, item: libc::c_uint) {
if array.is_null() {
eprintln!("ignoring careless call to dc_array_add_id()");
return;
}
(*array).add_id(item);
}
#[no_mangle]
pub unsafe extern "C" fn dc_array_get_cnt(array: *const dc_array_t) -> libc::size_t {
if array.is_null() {
@@ -2305,23 +2401,10 @@ pub unsafe extern "C" fn dc_chat_get_type(chat: *mut dc_chat_t) -> libc::c_int {
pub unsafe extern "C" fn dc_chat_get_name(chat: *mut dc_chat_t) -> *mut libc::c_char {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_get_name()");
return dc_strdup(ptr::null());
}
let ffi_chat = &*chat;
ffi_chat.chat.get_name().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_get_subtitle(chat: *mut dc_chat_t) -> *mut libc::c_char {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_get_subtitle()");
return "".strdup();
}
let ffi_chat = &*chat;
let ffi_context: &ContextWrapper = &*ffi_chat.context;
ffi_context
.with_inner(|ctx| ffi_chat.chat.get_subtitle(ctx).strdup())
.unwrap_or_else(|_| "".strdup())
ffi_chat.chat.get_name().strdup()
}
#[no_mangle]
@@ -2354,13 +2437,17 @@ pub unsafe extern "C" fn dc_chat_get_color(chat: *mut dc_chat_t) -> u32 {
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_get_archived(chat: *mut dc_chat_t) -> libc::c_int {
pub unsafe extern "C" fn dc_chat_get_visibility(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_get_archived()");
eprintln!("ignoring careless call to dc_chat_get_visibility()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_archived() as libc::c_int
match ffi_chat.chat.visibility {
ChatVisibility::Normal => 0,
ChatVisibility::Archived => 1,
ChatVisibility::Pinned => 2,
}
}
#[no_mangle]
@@ -2423,6 +2510,37 @@ pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> l
ffi_chat.chat.is_sending_locations() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_muted(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_muted()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_muted() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_get_remaining_mute_duration(chat: *mut dc_chat_t) -> i64 {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_get_remaining_mute_duration()");
return 0;
}
let ffi_chat = &*chat;
if !ffi_chat.chat.is_muted() {
return 0;
}
// If the chat was muted to before the epoch, it is not muted.
match ffi_chat.chat.mute_duration {
MuteDuration::NotMuted => 0,
MuteDuration::Forever => -1,
MuteDuration::Until(when) => when
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_get_info_json(
context: *mut dc_context_t,
@@ -2592,7 +2710,7 @@ pub unsafe extern "C" fn dc_msg_get_sort_timestamp(msg: *mut dc_msg_t) -> i64 {
pub unsafe extern "C" fn dc_msg_get_text(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_text()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_msg = &*msg;
ffi_msg.message.get_text().unwrap_or_default().strdup()
@@ -2611,8 +2729,7 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha
ffi_msg
.message
.get_file(ctx)
.and_then(|p| p.to_c_string().ok())
.map(|cs| dc_strdup(cs.as_ptr()))
.map(|p| p.strdup())
.unwrap_or_else(|| "".strdup())
})
.unwrap_or_else(|_| "".strdup())
@@ -2622,7 +2739,7 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha
pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_filename()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_msg = &*msg;
ffi_msg.message.get_filename().unwrap_or_default().strdup()
@@ -2632,13 +2749,13 @@ pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c
pub unsafe extern "C" fn dc_msg_get_filemime(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_filemime()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_msg = &*msg;
if let Some(x) = ffi_msg.message.get_filemime() {
x.strdup()
} else {
dc_strdup(ptr::null())
"".strdup()
}
}
@@ -2962,7 +3079,7 @@ pub unsafe extern "C" fn dc_contact_get_id(contact: *mut dc_contact_t) -> u32 {
pub unsafe extern "C" fn dc_contact_get_addr(contact: *mut dc_contact_t) -> *mut libc::c_char {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_get_addr()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_contact = &*contact;
ffi_contact.contact.get_addr().strdup()
@@ -2972,7 +3089,7 @@ pub unsafe extern "C" fn dc_contact_get_addr(contact: *mut dc_contact_t) -> *mut
pub unsafe extern "C" fn dc_contact_get_name(contact: *mut dc_contact_t) -> *mut libc::c_char {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_get_name()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_contact = &*contact;
ffi_contact.contact.get_name().strdup()
@@ -2984,7 +3101,7 @@ pub unsafe extern "C" fn dc_contact_get_display_name(
) -> *mut libc::c_char {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_get_display_name()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_contact = &*contact;
ffi_contact.contact.get_display_name().strdup()
@@ -2996,7 +3113,7 @@ pub unsafe extern "C" fn dc_contact_get_name_n_addr(
) -> *mut libc::c_char {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_get_name_n_addr()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_contact = &*contact;
ffi_contact.contact.get_name_n_addr().strdup()
@@ -3008,7 +3125,7 @@ pub unsafe extern "C" fn dc_contact_get_first_name(
) -> *mut libc::c_char {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_get_first_name()");
return dc_strdup(ptr::null());
return "".strdup();
}
let ffi_contact = &*contact;
ffi_contact.contact.get_first_name().strdup()
@@ -3091,7 +3208,7 @@ pub unsafe extern "C" fn dc_lot_get_text1(lot: *mut dc_lot_t) -> *mut libc::c_ch
}
let lot = &*lot;
strdup_opt(lot.get_text1())
lot.get_text1().strdup()
}
#[no_mangle]
@@ -3102,7 +3219,7 @@ pub unsafe extern "C" fn dc_lot_get_text2(lot: *mut dc_lot_t) -> *mut libc::c_ch
}
let lot = &*lot;
strdup_opt(lot.get_text2())
lot.get_text2().strdup()
}
#[no_mangle]
@@ -3154,11 +3271,16 @@ pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
libc::free(s as *mut _)
}
pub mod providers;
pub trait ResultExt<T, E> {
trait ResultExt<T, E> {
fn unwrap_or_log_default(self, context: &context::Context, message: &str) -> T;
fn log_err(self, context: &context::Context, message: &str) -> Result<T, E>;
/// Log a warning to a [ContextWrapper] for an [Err] result.
///
/// Does nothing for an [Ok].
///
/// You can do this as soon as the wrapper exists, it does not
/// have to be open (which is required for the `warn!()` macro).
fn log_err(self, wrapper: &ContextWrapper, message: &str) -> Result<T, E>;
}
impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
@@ -3172,22 +3294,17 @@ impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
}
}
fn log_err(self, context: &context::Context, message: &str) -> Result<T, E> {
fn log_err(self, wrapper: &ContextWrapper, message: &str) -> Result<T, E> {
self.map_err(|err| {
warn!(context, "{}: {}", message, err);
unsafe {
wrapper.warning(&format!("{}: {}", message, err));
}
err
})
}
}
unsafe fn strdup_opt(s: Option<impl AsRef<str>>) -> *mut libc::c_char {
match s {
Some(s) => s.as_ref().strdup(),
None => ptr::null_mut(),
}
}
pub trait ResultNullableExt<T> {
trait ResultNullableExt<T> {
fn into_raw(self) -> *mut T;
}
@@ -3210,3 +3327,68 @@ fn convert_and_prune_message_ids(msg_ids: *const u32, msg_cnt: libc::c_int) -> V
msg_ids
}
// dc_provider_t
#[no_mangle]
pub type dc_provider_t = provider::Provider;
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_email(
context: *const dc_context_t,
addr: *const libc::c_char,
) -> *const dc_provider_t {
if context.is_null() || addr.is_null() {
eprintln!("ignoring careless call to dc_provider_new_from_email()");
return ptr::null();
}
let addr = to_string_lossy(addr);
match provider::get_provider_info(addr.as_str()) {
Some(provider) => provider,
None => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_overview_page(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_get_overview_page()");
return "".strdup();
}
let provider = &*provider;
provider.overview_page.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_before_login_hint(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_get_before_login_hint()");
return "".strdup();
}
let provider = &*provider;
provider.before_login_hint.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t) -> libc::c_int {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_get_status()");
return 0;
}
let provider = &*provider;
provider.status as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_unref()");
return;
}
// currently, there is nothing to free, the provider info is a static object.
// this may change once we start localizing string.
}

View File

@@ -1,91 +0,0 @@
extern crate deltachat_provider_database;
use std::ptr;
use crate::string::{to_string_lossy, StrExt};
use deltachat_provider_database::StatusState;
#[no_mangle]
pub type dc_provider_t = deltachat_provider_database::Provider;
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_domain(
domain: *const libc::c_char,
) -> *const dc_provider_t {
match deltachat_provider_database::get_provider_info(&to_string_lossy(domain)) {
Some(provider) => provider,
None => ptr::null(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_email(
email: *const libc::c_char,
) -> *const dc_provider_t {
let email = to_string_lossy(email);
let domain = deltachat_provider_database::get_domain_from_email(&email);
match deltachat_provider_database::get_provider_info(domain) {
Some(provider) => provider,
None => ptr::null(),
}
}
macro_rules! null_guard {
($context:tt) => {
if $context.is_null() {
return ptr::null_mut() as *mut libc::c_char;
}
};
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_overview_page(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
null_guard!(provider);
format!(
"{}/{}",
deltachat_provider_database::PROVIDER_OVERVIEW_URL,
(*provider).overview_page
)
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_name(provider: *const dc_provider_t) -> *mut libc::c_char {
null_guard!(provider);
(*provider).name.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_markdown(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
null_guard!(provider);
(*provider).markdown.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_status_date(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
null_guard!(provider);
(*provider).status.date.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t) -> u32 {
if provider.is_null() {
return 0;
}
match (*provider).status.state {
StatusState::OK => 1,
StatusState::PREPARATION => 2,
StatusState::BROKEN => 3,
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_unref(_provider: *const dc_provider_t) {}
// TODO expose general provider overview url?

View File

@@ -1,5 +1,5 @@
use failure::Fail;
use std::ffi::{CStr, CString};
use std::ptr;
/// Duplicates a string
///
@@ -8,7 +8,7 @@ use std::ffi::{CStr, CString};
/// # Examples
///
/// ```rust,norun
/// use deltachat::dc_tools::{dc_strdup, to_string_lossy};
/// use crate::string::{dc_strdup, to_string_lossy};
/// unsafe {
/// let str_a = b"foobar\x00" as *const u8 as *const libc::c_char;
/// let str_a_copy = dc_strdup(str_a);
@@ -16,7 +16,7 @@ use std::ffi::{CStr, CString};
/// assert_ne!(str_a, str_a_copy);
/// }
/// ```
pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
let ret: *mut libc::c_char;
if !s.is_null() {
ret = libc::strdup(s);
@@ -30,13 +30,13 @@ pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
}
/// Error type for the [OsStrExt] trait
#[derive(Debug, Fail, PartialEq)]
pub enum CStringError {
#[derive(Debug, PartialEq, thiserror::Error)]
pub(crate) enum CStringError {
/// The string contains an interior null byte
#[fail(display = "String contains an interior null byte")]
#[error("String contains an interior null byte")]
InteriorNullByte,
/// The string is not valid Unicode
#[fail(display = "String is not valid unicode")]
#[error("String is not valid unicode")]
NotUnicode,
}
@@ -65,7 +65,7 @@ pub enum CStringError {
/// let mut c_ptr: *mut libc::c_char = dc_strdup(path_c.as_ptr());
/// }
/// ```
pub trait OsStrExt {
pub(crate) trait OsStrExt {
/// Convert a [std::ffi::OsStr] to an [std::ffi::CString]
///
/// This is useful to convert e.g. a [std::path::Path] to
@@ -130,27 +130,28 @@ fn os_str_to_c_string_unicode(
}
/// Convenience methods/associated functions for working with [CString]
///
/// This is helps transitioning from unsafe code.
pub trait CStringExt {
/// Create a new [CString], yolo style
trait CStringExt {
/// Create a new [CString], best effort
///
/// This unwrap the result, panicking when there are embedded NULL
/// bytes.
fn yolo<T: Into<Vec<u8>>>(t: T) -> CString {
CString::new(t).expect("String contains null byte, can not be CString")
/// Like the [to_string_lossy] this doesn't give up in the face of
/// bad input (embedded null bytes in this case) instead it does
/// the best it can by stripping the embedded null bytes.
fn new_lossy<T: Into<Vec<u8>>>(t: T) -> CString {
let mut s = t.into();
s.retain(|&c| c != 0);
CString::new(s).unwrap_or_default()
}
}
impl CStringExt for CString {}
/// Convenience methods to make transitioning from raw C strings easier.
/// Convenience methods to turn strings into C strings.
///
/// To interact with (legacy) C APIs we often need to convert from
/// Rust strings to raw C strings. This can be clumsy to do correctly
/// and the compiler sometimes allows it in an unsafe way. These
/// methods make it more succinct and help you get it right.
pub trait StrExt {
pub(crate) trait Strdup {
/// Allocate a new raw C `*char` version of this string.
///
/// This allocates a new raw C string which must be freed using
@@ -167,14 +168,52 @@ pub trait StrExt {
unsafe fn strdup(&self) -> *mut libc::c_char;
}
impl<T: AsRef<str>> StrExt for T {
impl<T: AsRef<str>> Strdup for T {
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = CString::yolo(self.as_ref());
let tmp = CString::new_lossy(self.as_ref());
dc_strdup(tmp.as_ptr())
}
}
pub fn to_string_lossy(s: *const libc::c_char) -> String {
// We can not implement for AsRef<OsStr> because we already implement
// AsRev<str> and this conflicts. So implement for Path directly.
impl Strdup for std::path::Path {
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
dc_strdup(tmp.as_ptr())
}
}
/// Convenience methods to turn optional strings into C strings.
///
/// This is the same as the [Strdup] trait but a different trait name
/// to work around the type system not allowing to implement [Strdup]
/// for `Option<impl Strdup>` When we already have an [Strdup] impl
/// for `AsRef<&str>`.
///
/// When the [Option] is [Option::Some] this behaves just like
/// [Strdup::strdup], when it is [Option::None] a null pointer is
/// returned.
pub(crate) trait OptStrdup {
/// Allocate a new raw C `*char` version of this string, or NULL.
///
/// See [Strdup::strdup] for details.
unsafe fn strdup(&self) -> *mut libc::c_char;
}
impl<T: AsRef<str>> OptStrdup for Option<T> {
unsafe fn strdup(&self) -> *mut libc::c_char {
match self {
Some(s) => {
let tmp = CString::new_lossy(s.as_ref());
dc_strdup(tmp.as_ptr())
}
None => ptr::null_mut(),
}
}
}
pub(crate) fn to_string_lossy(s: *const libc::c_char) -> String {
if s.is_null() {
return "".into();
}
@@ -184,7 +223,7 @@ pub fn to_string_lossy(s: *const libc::c_char) -> String {
cstr.to_string_lossy().to_string()
}
pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
if s.is_null() {
return None;
}
@@ -205,7 +244,7 @@ pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
///
/// [Path]: std::path::Path
#[cfg(not(target_os = "windows"))]
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
assert!(!s.is_null(), "cannot be used on null pointers");
use std::os::unix::ffi::OsStrExt;
unsafe {
@@ -217,7 +256,7 @@ pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
// as_path() implementation for windows, documented above.
#[cfg(target_os = "windows")]
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
as_path_unicode(s)
}
@@ -324,8 +363,14 @@ mod tests {
}
#[test]
fn test_cstring_yolo() {
assert_eq!(CString::new("hello").unwrap(), CString::yolo("hello"));
fn test_cstring_new_lossy() {
assert!(CString::new("hel\x00lo").is_err());
assert!(CString::new(String::from("hel\x00o")).is_err());
let r = CString::new("hello").unwrap();
assert_eq!(CString::new_lossy("hello"), r);
assert_eq!(CString::new_lossy("hel\x00lo"), r);
assert_eq!(CString::new_lossy(String::from("hello")), r);
assert_eq!(CString::new_lossy(String::from("hel\x00lo")), r);
}
#[test]
@@ -347,4 +392,19 @@ mod tests {
assert_eq!(cmp, 0);
}
}
#[test]
fn test_strdup_opt_string() {
unsafe {
let s = Some("hello");
let c = s.strdup();
let cmp = strcmp(c, b"hello\x00" as *const u8 as *const libc::c_char);
free(c as *mut libc::c_void);
assert_eq!(cmp, 0);
let s: Option<&str> = None;
let c = s.strdup();
assert_eq!(c, ptr::null_mut());
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_derive"
version = "1.0.0-beta.22"
version = "2.0.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"

View File

@@ -3,7 +3,6 @@ extern crate proc_macro;
use crate::proc_macro::TokenStream;
use quote::quote;
use syn;
// For now, assume (not check) that these macroses are applied to enum without
// data. If this assumption is violated, compiler error will point to

126
draft/group-sync.rst Normal file
View File

@@ -0,0 +1,126 @@
Problem: missing eventual group consistency
--------------------------------------------
If group members are concurrently adding new members,
the new members will miss each other's additions, example:
- Alice and Bob are in a two-member group
- Alice adds Carol, concurrently Bob adds Doris
- Carol will see a three-member group (Alice, Bob, Carol),
Doris will see a different three-member group (Alice, Bob, Doris),
and only Alice and Bob will have all four members.
Note that for verified groups any mitigation mechanism likely
needs to make all clients to know who originally added a member.
solution: memorize+attach (possible encrypted) chat-meta mime messages
----------------------------------------------------------------------
For reference, please see https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#add-and-remove-members how MemberAdded/Removed messages are shaped.
- All Chat-Group-Member-Added/Removed messages are recorded in their
full raw (signed and encrypted) mime-format in the DB
- If an incoming member-add/member-delete messages has a member list
which is, apart from the added/removed member, not consistent
with our own view, broadcast a "Chat-Group-Member-Correction" message to
all members, attaching the original added/removed mime-message for all mismatching
contacts. If we have no relevant add/del information, don't send a
correction message out.
- Upong receiving added/removed attachments we don't do the
check_consistency+correction message dance.
This avoids recursion problems and hard-to-reason-about chatter.
Notes:
- mechanism works for both encrypted and unencrypted add/del messages
- we already have a "mime_headers" column in the DB for each incoming message.
We could extend it to also include the payload and store mime unconditionally
for member-added/removed messages.
- multiple member-added/removed messages can be attached in a single
correction message
- it is minimal on the number of overall messages to reach group consistency
(best-case: no extra messages, the ABCD case above: max two extra messages)
- somewhat backward compatible: older clients will probably ignore
messages which are signed by someone who is not the outer From-address.
- the correction-protocol also helps with dropped messages. If a member
did not see a member-added/removed message, the next member add/removed
message in the group will likely heal group consistency for this member.
- we can quite easily extend the mechanism to also provide the group-avatar or
other meta-information.
Discussions of variants
++++++++++++++++++++++++
- instead of acting on MemberAdded/Removed message we could send
corrections for any received message that addresses inconsistent group members but
a) this would delay group-membership healing
b) could lead to a lot of members sending corrections
- instead of broadcasting correction messages we could only send it to
the sender of the inconsistent member-added/removed message.
A receiver of such a correction message would then need to forward
the message to the members it thinks also have an inconsistent view.
This sounds complicated and error-prone. Concretely, if Alice
receives Bob's "Member-added: Doris" message, then Alice
broadcasting the correction message with "Member-added: Carol"
would reach all four members, healing group consistency in one step.
If Bob meanwhile receives Alice's "Member-Added: Carol" message,
Bob would broadcast a correction message to all four members as well.
(Imagine a situation where Alice/Bob added Carol/Doris
while both being in an offline or bad-connection situation).
solution2: repeat member-added/removed messages
---------------------------------------------------
Introduce a new Chat-Group-Member-Changed header and deprecate Chat-Group-Member-Added/Removed
but keep sending out the old headers until the new protocol is sufficiently deployed.
The new Chat-Group-Member-Changed header contains a Time-to-Live number (TTL)
which controls repetition of the signed "add/del e-mail address" payload.
Example::
Chat-Group-Member-Changed: TTL add "somedisplayname" someone@example.org
owEBYQGe/pANAwACAY47A6J5t3LWAcsxYgBeTQypYWRkICJzb21lZGlzcGxheW5h
bWUiIHNvbWVvbmVAZXhhbXBsZS5vcmcgCokBHAQAAQIABgUCXk0MqQAKCRCOOwOi
ebdy1hfRB/wJ74tgFQulicthcv9n+ZsqzwOtBKMEVIHqJCzzDB/Hg/2z8ogYoZNR
iUKKrv3Y1XuFvdKyOC+wC/unXAWKFHYzY6Tv6qDp6r+amt+ad+8Z02q53h9E55IP
FUBdq2rbS8hLGjQB+mVRowYrUACrOqGgNbXMZjQfuV7fSc7y813OsCQgi3tjstup
b+uduVzxCp3PChGhcZPs3iOGCnQvSB8VAaLGMWE2d7nTo/yMQ0Jx69x5qwfXogTk
mTt5rOJyrosbtf09TMKFzGgtqBcEqHLp3+mQpZQ+WHUKAbsRa8Jc9DOUOSKJ8SNM
clKdskprY+4LY0EBwLD3SQ7dPkTITCRD
=P6GG
TTL is set to "2" on an initial Chat-Group-Member-Changed add/del message.
Receivers will apply the add/del change to the group-membership,
decrease the TTL by 1, and if TTL>0 re-sent the header.
The "add|del e-mail address" payload is pgp-signed and repeated verbatim.
This allows to propagate, in a cryptographically secured way,
who added a member. This is particularly important for allowing
to show in verified groups who added a member (planned).
Disadvantage to solution 1:
- requires to specify encoding and precise rules for what/how is signed.
- causes O(N^2) extra messages
- Not easily extendable for other things (without introducing a new
header / encoding)

View File

@@ -1,9 +1,9 @@
use std::path::Path;
use std::str::FromStr;
use deltachat::chat::{self, Chat, ChatId};
use anyhow::{bail, ensure};
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::constants::*;
use deltachat::contact::*;
use deltachat::context::*;
@@ -19,6 +19,7 @@ use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
use deltachat::Event;
use deltachat::{config, provider};
/// Reset database tables.
/// Argument is a bitmask, executing single or multiple actions in one call.
@@ -91,10 +92,10 @@ fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
1
}
fn dc_poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), Error> {
fn dc_poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), anyhow::Error> {
let data = dc_read_file(context, filename)?;
if let Err(err) = dc_receive_imf(context, &data, "import", 0, 0) {
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false) {
println!("dc_receive_imf errored: {:?}", err);
}
Ok(())
@@ -297,7 +298,7 @@ fn chat_prefix(chat: &Chat) -> &'static str {
chat.typ.into()
}
pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), Error> {
let chat_id = *context.cmdline_sel_chat_id.read().unwrap();
let mut sel_chat = if !chat_id.is_unset() {
Chat::load_from_db(context, chat_id).ok()
@@ -371,6 +372,8 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
listmedia\n\
archive <chat-id>\n\
unarchive <chat-id>\n\
pin <chat-id>\n\
unpin <chat-id>\n\
delchat <chat-id>\n\
===========================Message commands==\n\
listmsgs <query>\n\
@@ -392,8 +395,10 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
getqr [<chat-id>]\n\
getbadqr\n\
checkqr <qr-content>\n\
providerinfo <addr>\n\
event <event-id to test>\n\
fileinfo <file>\n\
estimatedeletion <seconds>\n\
emptyserver <flags> (1=MVBOX 2=INBOX)\n\
clear -- clear screen\n\
exit or quit\n\
@@ -510,14 +515,19 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?;
println!(
"{}#{}: {} [{} fresh]",
"{}#{}: {} [{} fresh] {}",
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
chat::get_fresh_msg_cnt(context, chat.get_id()),
chat.get_id().get_fresh_msg_cnt(context),
match chat.visibility {
ChatVisibility::Normal => "",
ChatVisibility::Archived => "📦",
ChatVisibility::Pinned => "📌",
},
);
let lot = chatlist.get_summary(context, i, Some(&chat));
let statestr = if chat.is_archived() {
let statestr = if chat.visibility == ChatVisibility::Archived {
" [Archived]"
} else {
match lot.get_state() {
@@ -598,14 +608,11 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
},
);
log_msglist(context, &msglist)?;
if let Some(draft) = chat::get_draft(context, sel_chat.get_id())? {
if let Some(draft) = sel_chat.get_id().get_draft(context)? {
log_msg(context, "Draft", &draft);
}
println!(
"{} messages.",
chat::get_msg_cnt(context, sel_chat.get_id())
);
println!("{} messages.", sel_chat.get_id().get_msg_cnt(context));
chat::marknoticed_chat(context, sel_chat.get_id())?;
}
"createchat" => {
@@ -801,14 +808,14 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
if !arg1.is_empty() {
let mut draft = Message::new(Viewtype::Text);
draft.set_text(Some(arg1.to_string()));
chat::set_draft(
context,
sel_chat.as_ref().unwrap().get_id(),
Some(&mut draft),
);
sel_chat
.as_ref()
.unwrap()
.get_id()
.set_draft(context, Some(&mut draft));
println!("Draft saved.");
} else {
chat::set_draft(context, sel_chat.as_ref().unwrap().get_id(), None);
sel_chat.as_ref().unwrap().get_id().set_draft(context, None);
println!("Draft deleted.");
}
}
@@ -844,15 +851,23 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
}
print!("\n");
}
"archive" | "unarchive" => {
"archive" | "unarchive" | "pin" | "unpin" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat::archive(context, chat_id, arg0 == "archive")?;
chat_id.set_visibility(
context,
match arg0 {
"archive" => ChatVisibility::Archived,
"unarchive" | "unpin" => ChatVisibility::Normal,
"pin" => ChatVisibility::Pinned,
_ => panic!("Unexpected command (This should never happen)"),
},
)?;
}
"delchat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat::delete(context, chat_id)?;
chat_id.delete(context)?;
}
"msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
@@ -969,6 +984,31 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
res.get_text2()
);
}
"setqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
match set_config_from_qr(context, arg1) {
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
Err(err) => println!("Cannot set config from QR code: {:?}", err),
}
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
match provider::get_provider_info(arg1) {
Some(info) => {
println!("Information for provider belonging to {}:", arg1);
println!("status: {}", info.status as u32);
println!("before_login_hint: {}", info.before_login_hint);
println!("after_login_hint: {}", info.after_login_hint);
println!("overview_page: {}", info.overview_page);
for server in info.server.iter() {
println!("server: {}:{}", server.hostname, server.port,);
}
}
None => {
println!("No information for provider belonging to {} found.", arg1);
}
}
}
// TODO: implement this again, unclear how to match this through though, without writing a parser.
// "event" => {
// ensure!(!arg1.is_empty(), "Argument <id> missing.");
@@ -990,6 +1030,16 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
bail!("Command failed.");
}
}
"estimatedeletion" => {
ensure!(!arg1.is_empty(), "Argument <seconds> missing");
let seconds = arg1.parse()?;
let device_cnt = message::estimate_deletion_cnt(context, false, seconds)?;
let server_cnt = message::estimate_deletion_cnt(context, true, seconds)?;
println!(
"estimated count of messages older than {} seconds:\non device: {}\non server: {}",
seconds, device_cnt, server_cnt
);
}
"emptyserver" => {
ensure!(!arg1.is_empty(), "Argument <flags> missing");

View File

@@ -7,8 +7,6 @@
#[macro_use]
extern crate deltachat;
#[macro_use]
extern crate failure;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate rusqlite;
@@ -20,9 +18,9 @@ use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use anyhow::{bail, Error};
use deltachat::chat::ChatId;
use deltachat::config;
use deltachat::configure::*;
use deltachat::context::*;
use deltachat::job::*;
use deltachat::oauth2::*;
@@ -263,7 +261,7 @@ const DB_COMMANDS: [&str; 11] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 24] = [
const CHAT_COMMANDS: [&str; 26] = [
"listchats",
"listarchived",
"chat",
@@ -287,6 +285,8 @@ const CHAT_COMMANDS: [&str; 24] = [
"listmedia",
"archive",
"unarchive",
"pin",
"unpin",
"delchat",
];
const MESSAGE_COMMANDS: [&str; 8] = [
@@ -307,8 +307,17 @@ const CONTACT_COMMANDS: [&str; 6] = [
"delcontact",
"cleanupcontacts",
];
const MISC_COMMANDS: [&str; 9] = [
"getqr", "getbadqr", "checkqr", "event", "fileinfo", "clear", "exit", "quit", "help",
const MISC_COMMANDS: [&str; 10] = [
"getqr",
"getbadqr",
"checkqr",
"event",
"fileinfo",
"clear",
"exit",
"quit",
"help",
"estimatedeletion",
];
impl Hinter for DcHelper {
@@ -360,10 +369,10 @@ impl Highlighter for DcHelper {
impl Helper for DcHelper {}
fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
fn main_0(args: Vec<String>) -> Result<(), Error> {
if args.len() < 2 {
println!("Error: Bad arguments, expected [db-name].");
return Err(format_err!("No db-name specified"));
bail!("No db-name specified");
}
let context = Context::new(
Box::new(receive_event),
@@ -433,7 +442,7 @@ enum ExitResult {
Exit,
}
fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failure::Error> {
fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, Error> {
let mut args = line.splitn(2, ' ');
let arg0 = args.next().unwrap_or_default();
let arg1 = args.next().unwrap_or_default();
@@ -461,7 +470,7 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
}
"configure" => {
start_threads(ctx.clone());
configure(&ctx.read().unwrap());
ctx.read().unwrap().configure();
}
"oauth2" => {
if let Some(addr) = ctx.read().unwrap().get_config(config::Config::Addr) {
@@ -516,7 +525,7 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
Ok(ExitResult::Continue)
}
pub fn main() -> Result<(), failure::Error> {
pub fn main() -> Result<(), Error> {
let _ = pretty_env_logger::try_init();
let args: Vec<String> = std::env::args().collect();

View File

@@ -7,7 +7,6 @@ use tempfile::tempdir;
use deltachat::chat;
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::configure::*;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::job::{
@@ -21,13 +20,13 @@ fn cb(_ctx: &Context, event: Event) {
match event {
Event::ConfigureProgress(progress) => {
print!(" progress: {}\n", progress);
println!(" progress: {}", progress);
}
Event::Info(msg) | Event::Warning(msg) | Event::Error(msg) | Event::ErrorNetwork(msg) => {
print!(" {}\n", msg);
println!(" {}", msg);
}
_ => {
print!("\n");
println!();
}
}
}
@@ -77,7 +76,7 @@ fn main() {
ctx.set_config(config::Config::Addr, Some("d@testrun.org"))
.unwrap();
ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap();
configure(&ctx);
ctx.configure();
thread::sleep(duration);

View File

@@ -1,6 +1,29 @@
0.600.1
0.900.0 (DRAFT)
---------------
- refactored internals to use plugin-approach
- introduced PerAccount and Global hooks that plugins can implement
- introduced `ac_member_added()` and `ac_member_removed()` plugin events.
- introduced two documented examples for an echo and a group-membership
tracking plugin.
0.800.0
-------
- use latest core 1.25.0
- refine tests and some internal changes to core bindings
0.700.0
---------
- lots of new Python APIs
- use rust core-beta23
- introduce automatic versioning via setuptools_scm,
based on py-X.Y.Z tags

View File

@@ -3,75 +3,70 @@ deltachat python bindings
=========================
This package provides bindings to the deltachat-core_ Rust -library
which provides imap/smtp/crypto handling as well as chat/group/messages
handling to Android, Desktop and IO user interfaces.
which implements IMAP/SMTP/MIME/PGP e-mail standards and offers
a low-level Chat/Contact/Message API to user interfaces and bots.
Installing bindings from source (Updated: 21-Dec-2019)
Installing bindings from source (Updated: 20-Jan-2020)
=========================================================
Install Rust and Cargo first. Deltachat needs a specific nightly
version, the easiest is probably to first install Rust stable from
rustup and then use this to install the correct nightly version.
Install rustup using::
Bootstrap Rust and Cargo by using rustup::
curl https://sh.rustup.rs -sSf | sh
GIT clone the repo and use rustup to check the correct nightly version
is available, if you do not have the right nightly version rustup will
download and install it::
Then GIT clone the deltachat-core-rust repo and get the actual
rust- and cargo-toolchain needed by deltachat::
git clone https://github.com/deltachat/deltachat-core-rust
cd deltachat-core-rust
rustup show
To install the python bindings make sure you have python installed, a
recent 3.x version will also come with the required venv module.
To install the Delta Chat Python bindings make sure you have Python3 installed.
E.g. on Debian-based systems `apt install python3 python3-pip
python3-venv` should give you a usable python installation. If you
prefer you can also
`Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_
as an alternative to `venv`.
python3-venv` should give you a usable python installation.
Ensure you are in the deltachat-core-rust/python directory, create the
vivrtual environment and activate it in your shell::
virtual environment and activate it in your shell::
cd python
python3 -m venv venv # or: virtualenv venv
source venv/bin/activate
You should now be able to build the python bindings using the supplied
script::
You should now be able to build the python bindings using the supplied script::
./install_python_bindings.py
The installation might take a while, depending on your machine.
The bindings will be installed in release mode but with debug symbols.
The release mode is necessary because some tests generate RSA keys
which is prohibitively slow in debug mode.
The release mode is currently necessary because some tests generate RSA keys
which is prohibitively slow in non-release mode.
After successful binding installation you can install a few more
python packages before finally running the tests::
Python packages before running the tests::
python -m pip install pytest pytest-timeout pytest-rerunfailures requests
pytest -v tests
running "live" tests (experimental)
-----------------------------------
running "live" tests with temporary accounts
---------------------------------------------
If you want to run "liveconfig" functional tests you can set
``DCC_PY_LIVECONFIG`` to:
``DCC_NEW_TMP_EMAIL`` to:
- a particular https-url that you can ask for from the delta
chat devs.
chat devs. This is implemented on the server side via
the [mailadm](https://github.com/deltachat/mailadm) command line tool.
- or the path of a file that contains two lines, each describing
via "addr=... mail_pw=..." a test account login that will
be used for the live tests.
With ``DCC_PY_LIVECONFIG`` set pytest invocations will use real
With ``DCC_NEW_TMP_EMAIL`` set pytest invocations will use real
e-mail accounts and run through all functional "liveconfig" tests.
@@ -81,15 +76,15 @@ Installing pre-built packages (Linux-only)
If you have a Linux system you may try to install the ``deltachat`` binary "wheel" packages
without any "build-from-source" steps.
First of all we suggest to `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
then create a fresh python environment and activate it in your shell::
We suggest to `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
then create a fresh Python virtual environment and activate it in your shell::
virtualenv venv # or: python -m venv
source venv/bin/activate
Afterwards, invoking ``python`` or ``pip install`` will only
modify files in your ``venv`` directory and leave your system installation
alone.
Afterwards, invoking ``python`` or ``pip install`` only
modifies files in your ``venv`` directory and leaves
your system installation alone.
For Linux, we automatically build wheels for all github PR branches
and push them to a python package index. To install the latest
@@ -135,7 +130,7 @@ organization::
This docker image can be used to run tests and build Python wheels for all interpreters::
$ docker run -e DCC_PY_LIVECONFIG \
$ docker run -e DCC_NEW_TMP_EMAIL \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh

View File

@@ -15,3 +15,7 @@ div.globaltoc {
img.logo {
height: 120px;
}
div.footer {
display: none;
}

View File

@@ -2,10 +2,6 @@
high level API reference
========================
.. note::
This API is work in progress and may change in versions prior to 1.0.
- :class:`deltachat.account.Account` (your main entry point, creates the
other classes)
- :class:`deltachat.contact.Contact`

View File

@@ -1,7 +0,0 @@
C deltachat interface
=====================
See :doc:`lapi` for accessing many of the below functions
through the ``deltachat.capi.lib`` namespace.

View File

@@ -55,7 +55,7 @@ master_doc = 'index'
# General information about the project.
project = u'deltachat'
copyright = u'2018, holger krekel and contributors'
copyright = u'2020, holger krekel and contributors'
# The language for content autogenerated by Sphinx. Refer to documentation

View File

@@ -1,37 +1,60 @@
examples
========
Playing around on the commandline
----------------------------------
Once you have :doc:`installed deltachat bindings <install>`
you can start playing from the python interpreter commandline.
For example you can type ``python`` and then::
you need email/password credentials for an IMAP/SMTP account.
Delta Chat developers and the CI system use a special URL to create
temporary e-mail accounts on [testrun.org](https://testrun.org) for testing.
# instantiate and configure deltachat account
import deltachat
ac = deltachat.Account("/tmp/db")
Receiving a Chat message from the command line
----------------------------------------------
# start configuration activity and smtp/imap threads
ac.start_threads()
ac.configure(addr="test2@hq5.merlinux.eu", mail_pw="********")
Here is a simple bot that:
# create a contact and send a message
contact = ac.create_contact("someother@email.address")
chat = ac.create_chat_by_contact(contact)
chat.send_text("hi from the python interpreter command line")
- receives a message and sends back ("echoes") a message
Checkout our :doc:`api` for the various high-level things you can do
to send/receive messages, create contacts and chats.
- terminates the bot if the message `/quit` is sent
.. include:: ../examples/echo_and_quit.py
:literal:
Looking at a real example
With this file in your working directory you can run the bot
by specifying a database path, an e-mail address and password of
a SMTP-IMAP account::
$ cd examples
$ python echo_and_quit.py /tmp/db --email ADDRESS --password PASSWORD
While this process is running you can start sending chat messages
to `ADDRESS`.
Track member additions and removals in a group
----------------------------------------------
Here is a simple bot that:
- echoes messages sent to it
- tracks if configuration completed
- tracks member additions and removals for all chat groups
.. include:: ../examples/group_tracking.py
:literal:
With this file in your working directory you can run the bot
by specifying a database path, an e-mail address and password of
a SMTP-IMAP account::
python group_tracking.py --email ADDRESS --password PASSWORD /tmp/db
When this process is running you can start sending chat messages
to `ADDRESS`.
Writing bots for real
-------------------------
The `deltabot repository <https://github.com/deltachat/deltabot#deltachat-example-bot>`_
contains a real-life example of Python bindings usage.
contains a little framework for writing deltachat bots in Python.

View File

@@ -1,15 +1,15 @@
deltachat python bindings
=========================
The ``deltachat`` Python package provides two bindings for the core Rust-library
of the https://delta.chat messaging ecosystem:
The ``deltachat`` Python package provides two layers of bindings for the
core Rust-library of the https://delta.chat messaging ecosystem:
- :doc:`api` is a high level interface to deltachat-core which aims
to be memory safe and thoroughly tested through continous tox/pytest runs.
- :doc:`api` is a high level interface to deltachat-core.
- :doc:`capi` is a lowlevel CFFI-binding to the previous
`deltachat-core C-API <https://c.delta.chat>`_ (so far the Rust library
replicates exactly the same C-level API).
- :doc:`plugins` is a brief introduction into implementing plugin hooks.
- :doc:`lapi` is a lowlevel CFFI-binding to the `Rust Core
<https://github.com/deltachat/deltachat-core-rust>`_.
@@ -28,8 +28,8 @@ getting started
links
changelog
api
capi
lapi
plugins
..
Indices and tables

38
python/doc/plugins.rst Normal file
View File

@@ -0,0 +1,38 @@
Implementing Plugin Hooks
==========================
The Delta Chat Python bindings use `pluggy <https://pluggy.readthedocs.io>`_
for managing global and per-account plugin registration, and performing
hook calls. There are two kinds of plugins:
- Global plugins that are active for all accounts; they can implement
hooks at account-creation and account-shutdown time.
- Account plugins that are only active during the lifetime of a
single Account instance.
Registering a plugin
--------------------
.. autofunction:: deltachat.register_global_plugin
:noindex:
.. automethod:: deltachat.account.Account.add_account_plugin
:noindex:
Per-Account Hook specifications
-------------------------------
.. autoclass:: deltachat.hookspec.PerAccount
:members:
Global Hook specifications
--------------------------
.. autoclass:: deltachat.hookspec.Global
:members:

View File

@@ -0,0 +1,30 @@
# content of echo_and_quit.py
from deltachat import account_hookimpl, run_cmdline
class EchoPlugin:
@account_hookimpl
def ac_incoming_message(self, message):
print("process_incoming message", message)
if message.text.strip() == "/quit":
message.account.shutdown()
else:
# unconditionally accept the chat
message.accept_sender_contact()
addr = message.get_sender_contact().addr
text = message.text
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
@account_hookimpl
def ac_message_delivered(self, message):
print("ac_message_delivered", message)
def main(argv=None):
run_cmdline(argv=argv, account_plugins=[EchoPlugin()])
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,52 @@
# content of group_tracking.py
from deltachat import account_hookimpl, run_cmdline
class GroupTrackingPlugin:
@account_hookimpl
def ac_incoming_message(self, message):
print("process_incoming message", message)
if message.text.strip() == "/quit":
message.account.shutdown()
else:
# unconditionally accept the chat
message.accept_sender_contact()
addr = message.get_sender_contact().addr
text = message.text
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
@account_hookimpl
def ac_outgoing_message(self, message):
print("ac_outgoing_message:", message)
@account_hookimpl
def ac_configure_completed(self, success):
print("ac_configure_completed:", success)
@account_hookimpl
def ac_chat_modified(self, chat):
print("ac_chat_modified:", chat.id, chat.get_name())
for member in chat.get_contacts():
print("chat member: {}".format(member.addr))
@account_hookimpl
def ac_member_added(self, chat, contact, message):
print("ac_member_added {} to chat {} from {}".format(
contact.addr, chat.id, message.get_sender_contact().addr))
for member in chat.get_contacts():
print("chat member: {}".format(member.addr))
@account_hookimpl
def ac_member_removed(self, chat, contact, message):
print("ac_member_removed {} from chat {} by {}".format(
contact.addr, chat.id, message.get_sender_contact().addr))
def main(argv=None):
run_cmdline(argv=argv, account_plugins=[GroupTrackingPlugin()])
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,72 @@
import pytest
import py
import echo_and_quit
import group_tracking
from deltachat.eventlogger import FFIEventLogger
@pytest.fixture(scope='session')
def datadir():
"""The py.path.local object of the test-data/ directory."""
for path in reversed(py.path.local(__file__).parts()):
datadir = path.join('test-data')
if datadir.isdir():
return datadir
else:
pytest.skip('test-data directory not found')
def test_echo_quit_plugin(acfactory):
botproc = acfactory.run_bot_process(echo_and_quit)
ac1 = acfactory.get_one_online_account()
bot_contact = ac1.create_contact(botproc.addr)
ch1 = ac1.create_chat_by_contact(bot_contact)
ch1.send_text("hello")
reply = ac1._evtracker.wait_next_incoming_message()
assert "hello" in reply.text
assert reply.chat == ch1
ch1.send_text("/quit")
botproc.wait()
def test_group_tracking_plugin(acfactory, lp):
lp.sec("creating one group-tracking bot and two temp accounts")
botproc = acfactory.run_bot_process(group_tracking, ffi=False)
ac1, ac2 = acfactory.get_two_online_accounts(quiet=True)
botproc.fnmatch_lines("""
*ac_configure_completed*
""")
ac1.add_account_plugin(FFIEventLogger(ac1, "ac1"))
ac2.add_account_plugin(FFIEventLogger(ac2, "ac2"))
lp.sec("creating bot test group with bot")
bot_contact = ac1.create_contact(botproc.addr)
ch = ac1.create_group_chat("bot test group")
ch.add_contact(bot_contact)
ch.send_text("hello")
botproc.fnmatch_lines("""
*ac_chat_modified*bot test group*
""")
lp.sec("adding third member {}".format(ac2.get_config("addr")))
contact3 = ac1.create_contact(ac2.get_config("addr"))
ch.add_contact(contact3)
reply = ac1._evtracker.wait_next_incoming_message()
assert "hello" in reply.text
lp.sec("now looking at what the bot received")
botproc.fnmatch_lines("""
*ac_member_added {}*
""".format(contact3.addr))
lp.sec("contact successfully added, now removing")
ch.remove_contact(contact3)
botproc.fnmatch_lines("""
*ac_member_removed {}*
""".format(contact3.addr))

View File

@@ -11,18 +11,15 @@ import sys
if __name__ == "__main__":
target = os.environ.get("DCC_RS_TARGET")
if target is None:
os.environ["DCC_RS_TARGET"] = target = "release"
os.environ["DCC_RS_TARGET"] = target = "debug"
if "DCC_RS_DEV" not in os.environ:
dn = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.environ["DCC_RS_DEV"] = dn
# build the core library in release + debug mode because
# as of Nov 2019 rPGP generates RSA keys which take
# prohibitively long for non-release installs
os.environ["RUSTFLAGS"] = "-g"
subprocess.check_call([
"cargo", "build", "-p", "deltachat_ffi", "--" + target
])
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
if target == 'release':
cmd.append("--release")
subprocess.check_call(cmd)
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":

View File

@@ -13,14 +13,20 @@ def main():
"root": "..",
"relative_to": __file__,
'tag_regex': r'^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
'git_describe_command': "git describe --dirty --tags --long --match py-*.*",
},
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
long_description=long_description,
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
install_requires=['cffi>=1.0.0', 'six'],
install_requires=['cffi>=1.0.0', 'pluggy'],
packages=setuptools.find_packages('src'),
package_dir={'': 'src'},
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
entry_points = {
'pytest11': [
'deltachat.testplugin = deltachat.testplugin',
],
},
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',

View File

@@ -1,6 +1,13 @@
from deltachat import capi, const
from deltachat.capi import ffi
from deltachat.account import Account # noqa
import sys
from . import capi, const, hookspec
from .capi import ffi
from .account import Account # noqa
from .message import Message # noqa
from .contact import Contact # noqa
from .chat import Chat # noqa
from .hookspec import account_hookimpl, global_hookimpl # noqa
from . import eventlogger
from pkg_resources import get_distribution, DistributionNotFound
try:
@@ -74,3 +81,62 @@ def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
if name.startswith("DC_EVENT_"):
_DC_EVENTNAME_MAP[val] = name
return _DC_EVENTNAME_MAP[integer]
def register_global_plugin(plugin):
""" Register a global plugin which implements one or more
of the :class:`deltachat.hookspec.Global` hooks.
"""
gm = hookspec.Global._get_plugin_manager()
gm.register(plugin)
gm.check_pending()
def unregister_global_plugin(plugin):
gm = hookspec.Global._get_plugin_manager()
gm.unregister(plugin)
register_global_plugin(eventlogger)
def run_cmdline(argv=None, account_plugins=None):
""" Run a simple default command line app, registering the specified
account plugins. """
import argparse
if argv is None:
argv = sys.argv
parser = argparse.ArgumentParser(prog=argv[0] if argv else None)
parser.add_argument("db", action="store", help="database file")
parser.add_argument("--show-ffi", action="store_true", help="show low level ffi events")
parser.add_argument("--email", action="store", help="email address")
parser.add_argument("--password", action="store", help="password")
args = parser.parse_args(argv[1:])
ac = Account(args.db)
if args.show_ffi:
log = eventlogger.FFIEventLogger(ac, "bot")
ac.add_account_plugin(log)
if not ac.is_configured():
assert args.email and args.password, (
"you must specify --email and --password once to configure this database/account"
)
ac.set_config("addr", args.email)
ac.set_config("mail_pw", args.password)
ac.set_config("mvbox_move", "0")
ac.set_config("mvbox_watch", "0")
ac.set_config("sentbox_watch", "0")
for plugin in account_plugins or []:
ac.add_account_plugin(plugin)
# start IO threads and configure if neccessary
ac.start()
print("{}: waiting for message".format(ac.get_config("addr")))
ac.wait_shutdown()

View File

@@ -2,23 +2,25 @@
from __future__ import print_function
import atexit
import threading
from contextlib import contextmanager
from email.utils import parseaddr
import queue
from threading import Event
import os
import re
import time
from array import array
try:
from queue import Queue, Empty
except ImportError:
from Queue import Queue, Empty
import deltachat
from . import const
from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
from .chat import Chat
from .message import Message
from .message import Message, map_system_message
from .contact import Contact
from .tracker import ImexTracker
from . import hookspec, iothreads
class MissingCredentials(ValueError):
""" Account is missing `addr` and `mail_pw` config values. """
class Account(object):
@@ -26,39 +28,63 @@ class Account(object):
by the underlying deltachat core library. All public Account methods are
meant to be memory-safe and return memory-safe objects.
"""
def __init__(self, db_path, logid=None, eventlogging=True, os_name=None, debug=True):
MissingCredentials = MissingCredentials
def __init__(self, db_path, os_name=None, logging=True):
""" initialize account object.
:param db_path: a path to the account database. The database
will be created if it doesn't exist.
:param logid: an optional logging prefix that should be used with
the default internal logging.
:param eventlogging: if False no eventlogging and no context callback will be configured
:param os_name: this will be put to the X-Mailer header in outgoing messages
:param debug: turn on debug logging for events.
"""
# initialize per-account plugin system
self._pm = hookspec.PerAccount._make_plugin_manager()
self._logging = logging
self.add_account_plugin(self)
self._dc_context = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, as_dc_charpointer(os_name)),
_destroy_dc_context,
)
if eventlogging:
self._evlogger = EventLogger(self._dc_context, logid, debug)
deltachat.set_context_callback(self._dc_context, self._process_event)
self._threads = IOThreads(self._dc_context, self._evlogger._log_event)
else:
self._threads = IOThreads(self._dc_context)
hook = hookspec.Global._get_plugin_manager().hook
self._threads = iothreads.IOThreads(self)
self._hook_event_queue = queue.Queue()
self._in_use_iter_events = False
self._shutdown_event = Event()
# open database
self.db_path = db_path
if hasattr(db_path, "encode"):
db_path = db_path.encode("utf8")
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
raise ValueError("Could not dc_open: {}".format(db_path))
self._configkeys = self.get_config("sys.config_keys").split()
self._imex_events = Queue()
atexit.register(self.shutdown)
hook.dc_account_init(account=self)
def disable_logging(self):
""" disable logging. """
self._logging = False
def enable_logging(self):
""" re-enable logging. """
self._logging = True
@hookspec.account_hookimpl
def ac_process_ffi_event(self, ffi_event):
for name, kwargs in self._map_ffi_event(ffi_event):
ev = HookEvent(self, name=name, kwargs=kwargs)
self._hook_event_queue.put(ev)
# def __del__(self):
# self.shutdown()
def ac_log_line(self, msg):
if self._logging:
self._pm.hook.ac_log_line(message=msg)
def _check_config_key(self, name):
if name not in self._configkeys:
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
@@ -118,16 +144,27 @@ class Account(object):
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
return from_dc_charpointer(res)
def configure(self, **kwargs):
""" set config values and configure this account.
def _preconfigure_keypair(self, addr, public, secret):
"""See dc_preconfigure_keypair() in deltachat.h.
In other words, you don't need this.
"""
res = lib.dc_preconfigure_keypair(self._dc_context,
as_dc_charpointer(addr),
as_dc_charpointer(public),
as_dc_charpointer(secret))
if res == 0:
raise Exception("Failed to set key")
def update_config(self, kwargs):
""" update config values.
:param kwargs: name=value config settings for this account.
values need to be unicode.
:returns: None
"""
for name, value in kwargs.items():
self.set_config(name, value)
lib.dc_configure(self._dc_context)
for key, value in kwargs.items():
self.set_config(key, str(value))
def is_configured(self):
""" determine if the account is configured already; an initial connection
@@ -135,7 +172,7 @@ class Account(object):
:returns: True if account is configured.
"""
return lib.dc_is_configured(self._dc_context)
return bool(lib.dc_is_configured(self._dc_context))
def set_avatar(self, img_path):
"""Set self avatar.
@@ -165,11 +202,6 @@ class Account(object):
raise ValueError("no flags set")
lib.dc_empty_server(self._dc_context, flags)
def get_infostring(self):
""" return info of the configured account. """
self.check_is_configured()
return from_dc_charpointer(lib.dc_get_info(self._dc_context))
def get_latest_backupfile(self, backupdir):
""" return the latest backup file in a given directory.
"""
@@ -191,8 +223,7 @@ class Account(object):
:returns: :class:`deltachat.contact.Contact`
"""
self.check_is_configured()
return Contact(self._dc_context, const.DC_CONTACT_ID_SELF)
return Contact(self, const.DC_CONTACT_ID_SELF)
def create_contact(self, email, name=None):
""" create a (new) Contact. If there already is a Contact
@@ -203,11 +234,14 @@ class Account(object):
:param name: display name for this contact (optional)
:returns: :class:`deltachat.contact.Contact` instance.
"""
name = as_dc_charpointer(name)
email = as_dc_charpointer(email)
contact_id = lib.dc_create_contact(self._dc_context, name, email)
realname, addr = parseaddr(email)
if name:
realname = name
realname = as_dc_charpointer(realname)
addr = as_dc_charpointer(addr)
contact_id = lib.dc_create_contact(self._dc_context, realname, addr)
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
return Contact(self._dc_context, contact_id)
return Contact(self, contact_id)
def delete_contact(self, contact):
""" delete a Contact.
@@ -220,6 +254,14 @@ class Account(object):
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
def get_contact_by_addr(self, email):
""" get a contact for the email address or None if it's blocked or doesn't exist. """
_, addr = parseaddr(email)
addr = as_dc_charpointer(addr)
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
if contact_id:
return self.get_contact_by_id(contact_id)
def get_contacts(self, query=None, with_self=False, only_verified=False):
""" get a (filtered) list of contacts.
@@ -239,7 +281,15 @@ class Account(object):
lib.dc_get_contacts(self._dc_context, flags, query),
lib.dc_array_unref
)
return list(iter_array(dc_array, lambda x: Contact(self._dc_context, x)))
return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_fresh_messages(self):
""" yield all fresh messages from all chats. """
dc_array = ffi.gc(
lib.dc_get_fresh_msgs(self._dc_context),
lib.dc_array_unref
)
yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
def create_chat_by_contact(self, contact):
""" create or get an existing 1:1 chat object for the specified contact or contact id.
@@ -261,6 +311,9 @@ class Account(object):
""" create or get an existing chat object for the
the specified message.
If this message is in the deaddrop chat then
the sender will become an accepted contact.
:param message: messsage id or message instance.
:returns: a :class:`deltachat.chat.Chat` object.
"""
@@ -313,6 +366,13 @@ class Account(object):
"""
return Message.from_db(self, msg_id)
def get_contact_by_id(self, contact_id):
""" return Contact instance or None.
:param contact_id: integer id of this contact.
:returns: None or :class:`deltachat.contact.Contact` instance.
"""
return Contact(self, contact_id)
def get_chat_by_id(self, chat_id):
""" return Chat instance.
:param chat_id: integer id of this chat.
@@ -357,45 +417,37 @@ class Account(object):
lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids))
def export_self_keys(self, path):
""" export public and private keys to the specified directory. """
""" export public and private keys to the specified directory.
Note that the account does not have to be started.
"""
return self._export(path, imex_cmd=1)
def export_all(self, path):
"""return new file containing a backup of all database state
(chats, contacts, keys, media, ...). The file is created in the
the `path` directory.
Note that the account does not have to be started.
"""
export_files = self._export(path, 11)
if len(export_files) != 1:
raise RuntimeError("found more than one new file")
return export_files[0]
def _imex_events_clear(self):
try:
while True:
self._imex_events.get_nowait()
except Empty:
pass
def _export(self, path, imex_cmd):
self._imex_events_clear()
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
if not self._threads.is_started():
lib.dc_perform_imap_jobs(self._dc_context)
files_written = []
while True:
ev = self._imex_events.get()
if isinstance(ev, str):
files_written.append(ev)
elif isinstance(ev, bool):
if not ev:
raise ValueError("export failed, exp-files: {}".format(files_written))
return files_written
with self.temp_plugin(ImexTracker()) as imex_tracker:
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
if not self._threads.is_started():
lib.dc_perform_imap_jobs(self._dc_context)
return imex_tracker.wait_finish()
def import_self_keys(self, path):
""" Import private keys found in the `path` directory.
The last imported key is made the default keys unless its name
contains the string legacy. Public keys are not imported.
Note that the account does not have to be started.
"""
self._import(path, imex_cmd=2)
@@ -408,12 +460,11 @@ class Account(object):
self._import(path, imex_cmd=12)
def _import(self, path, imex_cmd):
self._imex_events_clear()
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
if not self._threads.is_started():
lib.dc_perform_imap_jobs(self._dc_context)
if not self._imex_events.get():
raise ValueError("import from path '{}' failed".format(path))
with self.temp_plugin(ImexTracker()) as imex_tracker:
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
if not self._threads.is_started():
lib.dc_perform_imap_jobs(self._dc_context)
imex_tracker.wait_finish()
def initiate_key_transfer(self):
"""return setup code after a Autocrypt setup message
@@ -478,63 +529,6 @@ class Account(object):
raise ValueError("could not join group")
return Chat(self, chat_id)
def stop_ongoing(self):
lib.dc_stop_ongoing_process(self._dc_context)
#
# meta API for start/stop and event based processing
#
def wait_next_incoming_message(self):
""" wait for and return next incoming message. """
ev = self._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
return self.get_message_by_id(ev[2])
def start_threads(self, mvbox=False, sentbox=False):
""" start IMAP/SMTP threads (and configure account if it hasn't happened).
:raises: ValueError if 'addr' or 'mail_pw' are not configured.
:returns: None
"""
if not self.is_configured():
self.configure()
self._threads.start(mvbox=mvbox, sentbox=sentbox)
def stop_threads(self, wait=True):
""" stop IMAP/SMTP threads. """
if self._threads.is_started():
self.stop_ongoing()
self._threads.stop(wait=wait)
def shutdown(self, wait=True):
""" stop threads and close and remove underlying dc_context and callbacks. """
if hasattr(self, "_dc_context") and hasattr(self, "_threads"):
# print("SHUTDOWN", self)
self.stop_threads(wait=False)
lib.dc_close(self._dc_context)
self.stop_threads(wait=wait) # to wait for threads
deltachat.clear_context_callback(self._dc_context)
del self._dc_context
atexit.unregister(self.shutdown)
def _process_event(self, ctx, evt_name, data1, data2):
assert ctx == self._dc_context
if hasattr(self, "_evlogger"):
self._evlogger(evt_name, data1, data2)
method = getattr(self, "on_" + evt_name.lower(), None)
if method is not None:
method(data1, data2)
return 0
def on_dc_event_imex_progress(self, data1, data2):
if data1 == 1000:
self._imex_events.put(True)
elif data1 == 0:
self._imex_events.put(False)
def on_dc_event_imex_file_written(self, data1, data2):
self._imex_events.put(data1)
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
"""set a new location. It effects all chats where we currently
have enabled location streaming.
@@ -549,149 +543,122 @@ class Account(object):
if dc_res == 0:
raise ValueError("no chat is streaming locations")
#
# meta API for start/stop and event based processing
#
class IOThreads:
def __init__(self, dc_context, log_event=lambda *args: None):
self._dc_context = dc_context
self._thread_quitflag = False
self._name2thread = {}
self._log_event = log_event
def add_account_plugin(self, plugin, name=None):
""" add an account plugin which implements one or more of
the :class:`deltachat.hookspec.PerAccount` hooks.
"""
self._pm.register(plugin, name=name)
self._pm.check_pending()
return plugin
def is_started(self):
return len(self._name2thread) > 0
@contextmanager
def temp_plugin(self, plugin):
""" run a with-block with the given plugin temporarily registered. """
self._pm.register(plugin)
yield plugin
self._pm.unregister(plugin)
def start(self, imap=True, smtp=True, mvbox=False, sentbox=False):
assert not self.is_started()
if imap:
self._start_one_thread("inbox", self.imap_thread_run)
if mvbox:
self._start_one_thread("mvbox", self.mvbox_thread_run)
if sentbox:
self._start_one_thread("sentbox", self.sentbox_thread_run)
if smtp:
self._start_one_thread("smtp", self.smtp_thread_run)
def stop_ongoing(self):
""" Stop ongoing securejoin, configuration or other core jobs. """
lib.dc_stop_ongoing_process(self._dc_context)
def _start_one_thread(self, name, func):
self._name2thread[name] = t = threading.Thread(target=func, name=name)
t.setDaemon(1)
t.start()
def start(self, callback_thread=True):
""" start this account (activate imap/smtp threads etc.)
and return immediately.
def stop(self, wait=False):
self._thread_quitflag = True
lib.dc_interrupt_imap_idle(self._dc_context)
lib.dc_interrupt_smtp_idle(self._dc_context)
lib.dc_interrupt_mvbox_idle(self._dc_context)
lib.dc_interrupt_sentbox_idle(self._dc_context)
if wait:
for name, thread in self._name2thread.items():
thread.join()
If this account is not configured, an internal configuration
job will be scheduled if config values are sufficiently specified.
def imap_thread_run(self):
self._log_event("py-bindings-info", 0, "INBOX THREAD START")
while not self._thread_quitflag:
lib.dc_perform_imap_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_imap_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_imap_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "INBOX THREAD FINISHED")
You may call `wait_shutdown` or `shutdown` after the
account is in started mode.
def mvbox_thread_run(self):
self._log_event("py-bindings-info", 0, "MVBOX THREAD START")
while not self._thread_quitflag:
lib.dc_perform_mvbox_jobs(self._dc_context)
lib.dc_perform_mvbox_fetch(self._dc_context)
lib.dc_perform_mvbox_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "MVBOX THREAD FINISHED")
:raises MissingCredentials: if `addr` and `mail_pw` values are not set.
def sentbox_thread_run(self):
self._log_event("py-bindings-info", 0, "SENTBOX THREAD START")
while not self._thread_quitflag:
lib.dc_perform_sentbox_jobs(self._dc_context)
lib.dc_perform_sentbox_fetch(self._dc_context)
lib.dc_perform_sentbox_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "SENTBOX THREAD FINISHED")
:returns: None
"""
if not self.is_configured():
if not self.get_config("addr") or not self.get_config("mail_pw"):
raise MissingCredentials("addr or mail_pwd not set in config")
lib.dc_configure(self._dc_context)
self._threads.start(callback_thread=callback_thread)
def smtp_thread_run(self):
self._log_event("py-bindings-info", 0, "SMTP THREAD START")
while not self._thread_quitflag:
lib.dc_perform_smtp_jobs(self._dc_context)
lib.dc_perform_smtp_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "SMTP THREAD FINISHED")
def wait_shutdown(self):
""" wait until shutdown of this account has completed. """
self._shutdown_event.wait()
def shutdown(self, wait=True):
""" shutdown account, stop threads and close and remove
underlying dc_context and callbacks. """
dc_context = self._dc_context
if dc_context is None:
return
class EventLogger:
_loglock = threading.RLock()
if self._threads.is_started():
self.stop_ongoing()
self._threads.stop(wait=False)
lib.dc_close(dc_context)
self._hook_event_queue.put(None)
self._threads.stop(wait=wait) # to wait for threads
self._dc_context = None
atexit.unregister(self.shutdown)
self._shutdown_event.set()
hook = hookspec.Global._get_plugin_manager().hook
hook.dc_account_after_shutdown(account=self, dc_context=dc_context)
def __init__(self, dc_context, logid=None, debug=True):
self._dc_context = dc_context
self._event_queue = Queue()
self._debug = debug
if logid is None:
logid = str(self._dc_context).strip(">").split()[-1]
self.logid = logid
self._timeout = None
self.init_time = time.time()
def __call__(self, evt_name, data1, data2):
self._log_event(evt_name, data1, data2)
self._event_queue.put((evt_name, data1, data2))
def set_timeout(self, timeout):
self._timeout = timeout
def consume_events(self, check_error=True):
while not self._event_queue.empty():
self.get()
def get(self, timeout=None, check_error=True):
timeout = timeout or self._timeout
ev = self._event_queue.get(timeout=timeout)
if check_error and ev[0] == "DC_EVENT_ERROR":
raise ValueError("{}({!r},{!r})".format(*ev))
return ev
def ensure_event_not_queued(self, event_name_regex):
__tracebackhide__ = True
rex = re.compile("(?:{}).*".format(event_name_regex))
def _handle_current_events(self):
""" handle all currently queued events and then return. """
while 1:
try:
ev = self._event_queue.get(False)
except Empty:
event = self._hook_event_queue.get(block=False)
except queue.Empty:
break
else:
assert not rex.match(ev[0]), "event found {}".format(ev)
event.call_hook()
def get_matching(self, event_name_regex, check_error=True, timeout=None):
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
rex = re.compile("(?:{}).*".format(event_name_regex))
def iter_events(self, timeout=None):
""" yield hook events until shutdown.
It is not allowed to call iter_events() from multiple threads.
"""
if self._in_use_iter_events:
raise RuntimeError("can only call iter_events() from one thread")
self._in_use_iter_events = True
while 1:
ev = self.get(timeout=timeout, check_error=check_error)
if rex.match(ev[0]):
return ev
event = self._hook_event_queue.get(timeout=timeout)
if event is None:
break
yield event
def get_info_matching(self, regex):
rex = re.compile("(?:{}).*".format(regex))
while 1:
ev = self.get_matching("DC_EVENT_INFO")
if rex.match(ev[2]):
return ev
def _log_event(self, evt_name, data1, data2):
# don't show events that are anyway empty impls now
if evt_name == "DC_EVENT_GET_STRING":
return
if self._debug:
evpart = "{}({!r},{!r})".format(evt_name, data1, data2)
self._log(evpart)
def _log(self, msg):
t = threading.currentThread()
tname = getattr(t, "name", t)
if tname == "MainThread":
tname = "MAIN"
with self._loglock:
print("{:2.2f} [{}-{}] {}".format(time.time() - self.init_time, tname, self.logid, msg))
def _map_ffi_event(self, ffi_event):
name = ffi_event.name
if name == "DC_EVENT_CONFIGURE_PROGRESS":
data1 = ffi_event.data1
if data1 == 0 or data1 == 1000:
success = data1 == 1000
yield "ac_configure_completed", dict(success=success)
elif name == "DC_EVENT_INCOMING_MSG":
msg = self.get_message_by_id(ffi_event.data2)
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
elif name == "DC_EVENT_MSGS_CHANGED":
if ffi_event.data2 != 0:
msg = self.get_message_by_id(ffi_event.data2)
if msg.is_outgoing():
res = map_system_message(msg)
if res and res[0].startswith("ac_member"):
yield res
yield "ac_outgoing_message", dict(message=msg)
elif msg.is_in_fresh():
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
elif name == "DC_EVENT_MSG_DELIVERED":
msg = self.get_message_by_id(ffi_event.data2)
yield "ac_message_delivered", dict(message=msg)
elif name == "DC_EVENT_CHAT_MODIFIED":
chat = self.get_chat_by_id(ffi_event.data1)
yield "ac_chat_modified", dict(chat=chat)
def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):
@@ -718,3 +685,17 @@ class ScannedQRCode:
@property
def contact_id(self):
return self._dc_lot.id()
class HookEvent:
def __init__(self, account, name, kwargs):
assert hasattr(account._pm.hook, name), name
self.account = account
self.name = name
self.kwargs = kwargs
def call_hook(self):
hook = getattr(self.account._pm.hook, self.name, None)
if hook is None:
raise ValueError("event_name {} unknown".format(self.name))
return hook(**self.kwargs)

View File

@@ -30,7 +30,7 @@ class Chat(object):
return not (self == other)
def __repr__(self):
return "<Chat id={} name={} dc_context={}>".format(self.id, self.get_name(), self._dc_context)
return "<Chat id={} name={}>".format(self.id, self.get_name())
@property
def _dc_chat(self):
@@ -51,6 +51,16 @@ class Chat(object):
# ------ chat status/metadata API ------------------------------
def is_group(self):
""" return true if this chat is a group chat.
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat.
"""
return lib.dc_chat_get_type(self._dc_chat) in (
const.DC_CHAT_TYPE_GROUP,
const.DC_CHAT_TYPE_VERIFIED_GROUP
)
def is_deaddrop(self):
""" return true if this chat is a deaddrop chat.
@@ -58,6 +68,13 @@ class Chat(object):
"""
return self.id == const.DC_CHAT_ID_DEADDROP
def is_muted(self):
""" return true if this chat is muted.
:returns: True if chat is muted, False otherwise.
"""
return lib.dc_chat_is_muted(self._dc_chat)
def is_promoted(self):
""" return True if this chat is promoted, i.e.
the member contacts are aware of their membership,
@@ -84,14 +101,45 @@ class Chat(object):
def set_name(self, name):
""" set name of this chat.
:param: name as a unicode string.
:param name: as a unicode string.
:returns: None
"""
name = as_dc_charpointer(name)
return lib.dc_set_chat_name(self._dc_context, self.id, name)
def mute(self, duration=None):
""" mutes the chat
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
:returns: None
"""
if duration is None:
mute_duration = -1
else:
mute_duration = duration
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, mute_duration)
if not bool(ret):
raise ValueError("Call to dc_set_chat_mute_duration failed")
def unmute(self):
""" unmutes the chat
:returns: None
"""
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, 0)
if not bool(ret):
raise ValueError("Failed to unmute chat")
def get_mute_duration(self):
""" Returns the number of seconds until the mute of this chat is lifted.
:param duration:
:returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted)
"""
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
def get_type(self):
""" return type of this chat.
""" (deprecated) return type of this chat.
:returns: one of const.DC_CHAT_TYPE_*
"""
@@ -315,7 +363,7 @@ class Chat(object):
lib.dc_array_unref
)
return list(iter_array(
dc_array, lambda id: Contact(self._dc_context, id))
dc_array, lambda id: Contact(self.account, id))
)
def set_profile_image(self, img_path):
@@ -367,12 +415,6 @@ class Chat(object):
"""
return lib.dc_chat_get_color(self._dc_chat)
def get_subtitle(self):
"""return the subtitle of the chat
:returns: the subtitle
"""
return from_dc_charpointer(lib.dc_chat_get_subtitle(self._dc_chat))
# ------ location streaming API ------------------------------
def is_sending_locations(self):
@@ -385,7 +427,7 @@ class Chat(object):
"""return True if this chat is archived.
:returns: True if archived.
"""
return lib.dc_chat_get_archived(self._dc_chat)
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_ARCHIVED
def enable_sending_locations(self, seconds):
"""enable sending locations for this chat.

View File

@@ -11,6 +11,7 @@ from os.path import join as joinpath
DC_GCL_ARCHIVED_ONLY = 0x01
DC_GCL_NO_SPECIALS = 0x02
DC_GCL_ADD_ALLDONE_HINT = 0x04
DC_GCL_FOR_FORWARDING = 0x08
DC_GCL_VERIFIED_ONLY = 0x01
DC_GCL_ADD_SELF = 0x02
DC_QR_ASK_VERIFYCONTACT = 200
@@ -18,6 +19,7 @@ DC_QR_ASK_VERIFYGROUP = 202
DC_QR_FPR_OK = 210
DC_QR_FPR_MISMATCH = 220
DC_QR_FPR_WITHOUT_ADDR = 230
DC_QR_ACCOUNT = 250
DC_QR_ADDR = 320
DC_QR_TEXT = 330
DC_QR_URL = 332
@@ -97,19 +99,22 @@ DC_EVENT_IMEX_PROGRESS = 2051
DC_EVENT_IMEX_FILE_WRITTEN = 2052
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
DC_EVENT_SECUREJOIN_MEMBER_ADDED = 2062
DC_EVENT_FILE_COPIED = 2055
DC_EVENT_IS_OFFLINE = 2081
DC_EVENT_GET_STRING = 2091
DC_STR_SELFNOTINGRP = 21
DC_KEY_GEN_DEFAULT = 0
DC_KEY_GEN_RSA2048 = 1
DC_KEY_GEN_ED25519 = 2
DC_PROVIDER_STATUS_OK = 1
DC_PROVIDER_STATUS_PREPARATION = 2
DC_PROVIDER_STATUS_BROKEN = 3
DC_CHAT_VISIBILITY_NORMAL = 0
DC_CHAT_VISIBILITY_ARCHIVED = 1
DC_CHAT_VISIBILITY_PINNED = 2
DC_STR_NOMESSAGES = 1
DC_STR_SELF = 2
DC_STR_DRAFT = 3
DC_STR_MEMBER = 4
DC_STR_CONTACT = 6
DC_STR_VOICEMESSAGE = 7
DC_STR_DEADDROP = 8
DC_STR_IMAGE = 9
@@ -141,7 +146,6 @@ DC_STR_ARCHIVEDCHATS = 40
DC_STR_STARREDMSGS = 41
DC_STR_AC_SETUP_MSG_SUBJECT = 42
DC_STR_AC_SETUP_MSG_BODY = 43
DC_STR_SELFTALK_SUBTITLE = 50
DC_STR_CANNOT_LOGIN = 60
DC_STR_SERVER_RESPONSE = 61
DC_STR_MSGACTIONBYUSER = 62
@@ -157,7 +161,7 @@ DC_STR_COUNT = 68
def read_event_defines(f):
rex = re.compile(r'#define\s+((?:DC_EVENT|DC_QR|DC_MSG|DC_LP|DC_EMPTY|DC_CERTCK|DC_STATE|DC_STR|'
r'DC_CONTACT_ID|DC_GCL|DC_CHAT|DC_PROVIDER)_\S+)\s+([x\d]+).*')
r'DC_CONTACT_ID|DC_GCL|DC_CHAT|DC_PROVIDER|DC_KEY_GEN)_\S+)\s+([x\d]+).*')
for line in f:
m = rex.match(line)
if m:

View File

@@ -10,8 +10,9 @@ class Contact(object):
You obtain instances of it through :class:`deltachat.account.Account`.
"""
def __init__(self, dc_context, id):
self._dc_context = dc_context
def __init__(self, account, id):
self.account = account
self._dc_context = account._dc_context
self.id = id
def __eq__(self, other):
@@ -57,3 +58,7 @@ class Contact(object):
if dc_res == ffi.NULL:
return None
return from_dc_charpointer(dc_res)
def get_chat(self):
"""return 1:1 chat for this contact. """
return self.account.create_chat_by_contact(self)

View File

@@ -0,0 +1,137 @@
import deltachat
import threading
import time
import re
from queue import Queue, Empty
from .hookspec import account_hookimpl, global_hookimpl
@global_hookimpl
def dc_account_init(account):
# send all FFI events for this account to a plugin hook
def _ll_event(ctx, evt_name, data1, data2):
assert ctx == account._dc_context
ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2)
account._pm.hook.ac_process_ffi_event(
account=account, ffi_event=ffi_event
)
deltachat.set_context_callback(account._dc_context, _ll_event)
@global_hookimpl
def dc_account_after_shutdown(dc_context):
deltachat.clear_context_callback(dc_context)
class FFIEvent:
def __init__(self, name, data1, data2):
self.name = name
self.data1 = data1
self.data2 = data2
def __str__(self):
return "{name} data1={data1} data2={data2}".format(**self.__dict__)
class FFIEventLogger:
""" If you register an instance of this logger with an Account
you'll get all ffi-events printed.
"""
# to prevent garbled logging
_loglock = threading.RLock()
def __init__(self, account, logid):
"""
:param logid: an optional logging prefix that should be used with
the default internal logging.
"""
self.account = account
self.logid = logid
self.init_time = time.time()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
self._log_event(ffi_event)
def _log_event(self, ffi_event):
# don't show events that are anyway empty impls now
if ffi_event.name == "DC_EVENT_GET_STRING":
return
self.account.ac_log_line(str(ffi_event))
@account_hookimpl
def ac_log_line(self, message):
t = threading.currentThread()
tname = getattr(t, "name", t)
if tname == "MainThread":
tname = "MAIN"
elapsed = time.time() - self.init_time
locname = tname
if self.logid:
locname += "-" + self.logid
s = "{:2.2f} [{}] {}".format(elapsed, locname, message)
with self._loglock:
print(s, flush=True)
class FFIEventTracker:
def __init__(self, account, timeout=None):
self.account = account
self._timeout = timeout
self._event_queue = Queue()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
self._event_queue.put(ffi_event)
def set_timeout(self, timeout):
self._timeout = timeout
def consume_events(self, check_error=True):
while not self._event_queue.empty():
self.get(check_error=check_error)
def get(self, timeout=None, check_error=True):
timeout = timeout if timeout is not None else self._timeout
ev = self._event_queue.get(timeout=timeout)
if check_error and ev.name == "DC_EVENT_ERROR":
raise ValueError(str(ev))
return ev
def ensure_event_not_queued(self, event_name_regex):
__tracebackhide__ = True
rex = re.compile("(?:{}).*".format(event_name_regex))
while 1:
try:
ev = self._event_queue.get(False)
except Empty:
break
else:
assert not rex.match(ev.name), "event found {}".format(ev)
def get_matching(self, event_name_regex, check_error=True, timeout=None):
self.account.ac_log_line("-- waiting for event with regex: {} --".format(event_name_regex))
rex = re.compile("(?:{}).*".format(event_name_regex))
while 1:
ev = self.get(timeout=timeout, check_error=check_error)
if rex.match(ev.name):
return ev
def get_info_matching(self, regex):
rex = re.compile("(?:{}).*".format(regex))
while 1:
ev = self.get_matching("DC_EVENT_INFO")
if rex.match(ev.data2):
return ev
def wait_next_incoming_message(self):
""" wait for and return next incoming message. """
ev = self.get_matching("DC_EVENT_INCOMING_MSG")
return self.account.get_message_by_id(ev.data2)
def wait_next_messages_changed(self):
""" wait for and return next message-changed message or None
if the event contains no msgid"""
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
if ev.data2 > 0:
return self.account.get_message_by_id(ev.data2)

View File

@@ -0,0 +1,92 @@
""" Hooks for Python bindings to Delta Chat Core Rust CFFI"""
import pluggy
account_spec_name = "deltachat-account"
account_hookspec = pluggy.HookspecMarker(account_spec_name)
account_hookimpl = pluggy.HookimplMarker(account_spec_name)
global_spec_name = "deltachat-global"
global_hookspec = pluggy.HookspecMarker(global_spec_name)
global_hookimpl = pluggy.HookimplMarker(global_spec_name)
class PerAccount:
""" per-Account-instance hook specifications.
Except for ac_process_ffi_event all hooks are executed
in the thread which calls Account.wait_shutdown().
"""
@classmethod
def _make_plugin_manager(cls):
pm = pluggy.PluginManager(account_spec_name)
pm.add_hookspecs(cls)
return pm
@account_hookspec
def ac_process_ffi_event(self, ffi_event):
""" process a CFFI low level events for a given account.
ffi_event has "name", "data1", "data2" values as specified
with `DC_EVENT_* <https://c.delta.chat/group__DC__EVENT.html>`_.
DANGER: this hook is executed from the callback invoked by core.
Hook implementations need to be short running and can typically
not call back into core because this would easily cause recursion issues.
"""
@account_hookspec
def ac_log_line(self, message):
""" log a message related to the account. """
@account_hookspec
def ac_configure_completed(self, success):
""" Called when a configure process completed. """
@account_hookspec
def ac_incoming_message(self, message):
""" Called on any incoming message (to deaddrop or chat). """
@account_hookspec
def ac_outgoing_message(self, message):
""" Called on each outgoing message (both system and "normal")."""
@account_hookspec
def ac_message_delivered(self, message):
""" Called when an outgoing message has been delivered to SMTP. """
@account_hookspec
def ac_chat_modified(self, chat):
""" Chat was created or modified regarding membership, avatar, title. """
@account_hookspec
def ac_member_added(self, chat, contact, message):
""" Called for each contact added to an accepted chat. """
@account_hookspec
def ac_member_removed(self, chat, contact, message):
""" Called for each contact removed from a chat. """
class Global:
""" global hook specifications using a per-process singleton
plugin manager instance.
"""
_plugin_manager = None
@classmethod
def _get_plugin_manager(cls):
if cls._plugin_manager is None:
cls._plugin_manager = pm = pluggy.PluginManager(global_spec_name)
pm.add_hookspecs(cls)
return cls._plugin_manager
@global_hookspec
def dc_account_init(self, account):
""" called when `Account::__init__()` function starts executing. """
@global_hookspec
def dc_account_after_shutdown(self, account, dc_context):
""" Called after the account has been shutdown. """

View File

@@ -0,0 +1,106 @@
import threading
import time
from contextlib import contextmanager
from .capi import lib
class IOThreads:
def __init__(self, account):
self.account = account
self._dc_context = account._dc_context
self._thread_quitflag = False
self._name2thread = {}
def is_started(self):
return len(self._name2thread) > 0
def start(self, callback_thread):
assert not self.is_started()
self._start_one_thread("inbox", self.imap_thread_run)
self._start_one_thread("smtp", self.smtp_thread_run)
if callback_thread:
self._start_one_thread("cb", self.cb_thread_run)
if int(self.account.get_config("mvbox_watch")):
self._start_one_thread("mvbox", self.mvbox_thread_run)
if int(self.account.get_config("sentbox_watch")):
self._start_one_thread("sentbox", self.sentbox_thread_run)
def _start_one_thread(self, name, func):
self._name2thread[name] = t = threading.Thread(target=func, name=name)
t.setDaemon(1)
t.start()
@contextmanager
def log_execution(self, message):
self.account.ac_log_line(message + " START")
yield
self.account.ac_log_line(message + " FINISHED")
def stop(self, wait=False):
self._thread_quitflag = True
# Workaround for a race condition. Make sure that thread is
# not in between checking for quitflag and entering idle.
time.sleep(0.5)
lib.dc_interrupt_imap_idle(self._dc_context)
lib.dc_interrupt_smtp_idle(self._dc_context)
if "mvbox" in self._name2thread:
lib.dc_interrupt_mvbox_idle(self._dc_context)
if "sentbox" in self._name2thread:
lib.dc_interrupt_sentbox_idle(self._dc_context)
if wait:
for name, thread in self._name2thread.items():
if thread != threading.currentThread():
thread.join()
def cb_thread_run(self):
with self.log_execution("CALLBACK THREAD START"):
it = self.account.iter_events()
while not self._thread_quitflag:
try:
ev = next(it)
except StopIteration:
break
self.account.ac_log_line("calling hook name={} kwargs={}".format(ev.name, ev.kwargs))
ev.call_hook()
def imap_thread_run(self):
with self.log_execution("INBOX THREAD START"):
while not self._thread_quitflag:
lib.dc_perform_imap_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_imap_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_imap_idle(self._dc_context)
def mvbox_thread_run(self):
with self.log_execution("MVBOX THREAD"):
while not self._thread_quitflag:
lib.dc_perform_mvbox_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_mvbox_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_mvbox_idle(self._dc_context)
def sentbox_thread_run(self):
with self.log_execution("SENTBOX THREAD"):
while not self._thread_quitflag:
lib.dc_perform_sentbox_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_sentbox_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_sentbox_idle(self._dc_context)
def smtp_thread_run(self):
with self.log_execution("SMTP THREAD"):
while not self._thread_quitflag:
lib.dc_perform_smtp_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_smtp_idle(self._dc_context)

View File

@@ -28,7 +28,9 @@ class Message(object):
return self.account == other.account and self.id == other.id
def __repr__(self):
return "<Message id={} dc_context={}>".format(self.id, self._dc_context)
c = self.get_sender_contact()
return "<Message id={} sender={}/{} outgoing={} chat={}/{}>".format(
self.id, c.id, c.addr, self.is_outgoing(), self.chat.id, self.chat.get_name())
@classmethod
def from_db(cls, account, id):
@@ -50,6 +52,16 @@ class Message(object):
lib.dc_msg_unref
))
def accept_sender_contact(self):
""" ensure that the sender is an accepted contact
and that the message has a non-deaddrop chat object.
"""
self.account.create_chat_by_message(self)
self._dc_msg = ffi.gc(
lib.dc_get_msg(self._dc_context, self.id),
lib.dc_msg_unref
)
@props.with_doc
def text(self):
"""unicode text of this messages (might be empty if not a text message). """
@@ -81,6 +93,10 @@ class Message(object):
"""mime type of the file (if it exists)"""
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
def is_system_message(self):
""" return True if this message is a system/info message. """
return lib.dc_msg_is_info(self._dc_msg)
def is_setup_message(self):
""" return True if this message is a setup message. """
return lib.dc_msg_is_setupmessage(self._dc_msg)
@@ -159,6 +175,13 @@ class Message(object):
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
return Chat(self.account, chat_id)
def get_sender_chat(self):
"""return the 1:1 chat with the sender of this message.
:returns: :class:`deltachat.chat.Chat` instance
"""
return self.get_sender_contact().get_chat()
def get_sender_contact(self):
"""return the contact of who wrote the message.
@@ -166,7 +189,7 @@ class Message(object):
"""
from .contact import Contact
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
return Contact(self._dc_context, contact_id)
return Contact(self.account, contact_id)
#
# Message State query methods
@@ -207,6 +230,13 @@ class Message(object):
"""
return self._msgstate == const.DC_STATE_IN_SEEN
def is_outgoing(self):
"""Return True if Message is outgoing. """
return self._msgstate in (
const.DC_STATE_OUT_PREPARING, const.DC_STATE_OUT_PENDING,
const.DC_STATE_OUT_FAILED, const.DC_STATE_OUT_MDN_RCVD,
const.DC_STATE_OUT_DELIVERED)
def is_out_preparing(self):
"""Return True if Message is outgoing, but its file is being prepared.
"""
@@ -269,6 +299,10 @@ class Message(object):
""" return True if it's a file message. """
return self._view_type == const.DC_MSG_FILE
def mark_seen(self):
""" mark this message as seen. """
self.account.mark_seen_messages([self.id])
# some code for handling DC_MSG_* view types
@@ -288,3 +322,29 @@ def get_viewtype_code_from_name(view_type_name):
return code
raise ValueError("message typecode not found for {!r}, "
"available {!r}".format(view_type_name, list(_view_type_mapping.values())))
#
# some helper code for turning system messages into hook events
#
def map_system_message(msg):
if msg.is_system_message():
res = parse_system_add_remove(msg.text)
if res:
contact = msg.account.get_contact_by_addr(res[1])
if contact:
d = dict(chat=msg.chat, contact=contact, message=msg)
return "ac_member_" + res[0], d
def parse_system_add_remove(text):
# Member Me (x@y) removed by a@b.
# Member x@y removed by a@b
text = text.lower()
parts = text.split()
if parts[0] == "member":
if parts[2] in ("removed", "added"):
return parts[2], parts[1]
if parts[3] in ("removed", "added"):
return parts[3], parts[2].strip("()")

View File

@@ -11,27 +11,18 @@ class ProviderNotFoundError(Exception):
class Provider(object):
"""Provider information.
:param domain: The domain to get the provider info for, this is
normally the part following the `@` of the domain.
:param domain: The email to get the provider info for.
"""
def __init__(self, domain):
def __init__(self, account, addr):
provider = ffi.gc(
lib.dc_provider_new_from_domain(as_dc_charpointer(domain)),
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
lib.dc_provider_unref,
)
if provider == ffi.NULL:
raise ProviderNotFoundError("Provider not found")
self._provider = provider
@classmethod
def from_email(cls, email):
"""Create provider info from an email address.
:param email: Email address to get provider info for.
"""
return cls(email.split('@')[-1])
@property
def overview_page(self):
"""URL to the overview page of the provider on providers.delta.chat."""
@@ -39,21 +30,10 @@ class Provider(object):
lib.dc_provider_get_overview_page(self._provider))
@property
def name(self):
"""The name of the provider."""
return from_dc_charpointer(lib.dc_provider_get_name(self._provider))
@property
def markdown(self):
"""Content of the information page, formatted as markdown."""
def get_before_login_hints(self):
"""Should be shown to the user on login."""
return from_dc_charpointer(
lib.dc_provider_get_markdown(self._provider))
@property
def status_date(self):
"""The date the provider info was last updated, as a string."""
return from_dc_charpointer(
lib.dc_provider_get_status_date(self._provider))
lib.dc_provider_get_before_login_hints(self._provider))
@property
def status(self):

View File

@@ -0,0 +1,439 @@
from __future__ import print_function
import os
import sys
import subprocess
import queue
import threading
import fnmatch
import time
import weakref
import tempfile
import pytest
import requests
from . import Account, const
from .tracker import ConfigureTracker
from .capi import lib
from .eventlogger import FFIEventLogger, FFIEventTracker
from _pytest.monkeypatch import MonkeyPatch
from _pytest._code import Source
import deltachat
def pytest_addoption(parser):
parser.addoption(
"--liveconfig", action="store", default=None,
help="a file with >=2 lines where each line "
"contains NAME=VALUE config settings for one account"
)
parser.addoption(
"--ignored", action="store_true",
help="Also run tests marked with the ignored marker",
)
def pytest_configure(config):
config.addinivalue_line(
"markers", "ignored: Mark test as bing slow, skipped unless --ignored is used."
)
cfg = config.getoption('--liveconfig')
if not cfg:
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
if cfg:
config.option.liveconfig = cfg
# Make sure we don't get garbled output because threads keep running
# collect all ever created accounts in a weakref-set (so we don't
# keep objects unneccessarily alive) and enable/disable logging
# for each pytest test phase # (setup/call/teardown).
# Additionally make the acfactory use a logging/no-logging default.
class LoggingAspect:
def __init__(self):
self._accounts = weakref.WeakSet()
@deltachat.global_hookimpl
def dc_account_init(self, account):
self._accounts.add(account)
def disable_logging(self, item):
for acc in self._accounts:
acc.disable_logging()
acfactory = item.funcargs.get("acfactory")
if acfactory:
acfactory.set_logging_default(False)
def enable_logging(self, item):
for acc in self._accounts:
acc.enable_logging()
acfactory = item.funcargs.get("acfactory")
if acfactory:
acfactory.set_logging_default(True)
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_setup(self, item):
self.enable_logging(item)
yield
self.disable_logging(item)
@pytest.hookimpl(hookwrapper=True)
def pytest_pyfunc_call(self, pyfuncitem):
self.enable_logging(pyfuncitem)
yield
self.disable_logging(pyfuncitem)
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_teardown(self, item):
self.enable_logging(item)
yield
self.disable_logging(item)
la = LoggingAspect()
config.pluginmanager.register(la)
deltachat.register_global_plugin(la)
def pytest_report_header(config, startdir):
summary = []
t = tempfile.mktemp()
m = MonkeyPatch()
try:
m.setattr(sys.stdout, "write", lambda x: len(x))
ac = Account(t)
info = ac.get_info()
ac.shutdown()
finally:
m.undo()
os.remove(t)
summary.extend(['Deltachat core={} sqlite={}'.format(
info['deltachat_core_version'],
info['sqlite_version'],
)])
cfg = config.option.liveconfig
if cfg:
if "?" in cfg:
url, token = cfg.split("?", 1)
summary.append('Liveconfig provider: {}?<token ommitted>'.format(url))
else:
summary.append('Liveconfig file: {}'.format(cfg))
return summary
class SessionLiveConfigFromFile:
def __init__(self, fn):
self.fn = fn
self.configlist = []
for line in open(fn):
if line.strip() and not line.strip().startswith('#'):
d = {}
for part in line.split():
name, value = part.split("=")
d[name] = value
self.configlist.append(d)
def get(self, index):
return self.configlist[index]
def exists(self):
return bool(self.configlist)
class SessionLiveConfigFromURL:
def __init__(self, url):
self.configlist = []
self.url = url
def get(self, index):
try:
return self.configlist[index]
except IndexError:
assert index == len(self.configlist), index
res = requests.post(self.url)
if res.status_code != 200:
pytest.skip("creating newtmpuser failed {!r}".format(res))
d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"])
self.configlist.append(config)
return config
def exists(self):
return bool(self.configlist)
@pytest.fixture(scope="session")
def session_liveconfig(request):
liveconfig_opt = request.config.option.liveconfig
if liveconfig_opt:
if liveconfig_opt.startswith("http"):
return SessionLiveConfigFromURL(liveconfig_opt)
else:
return SessionLiveConfigFromFile(liveconfig_opt)
@pytest.fixture
def data(request):
class Data:
def __init__(self):
# trying to find test data heuristically
# because we are run from a dev-setup with pytest direct,
# through tox, and then maybe also from deltachat-binding
# users like "deltabot".
self.paths = [os.path.normpath(x) for x in [
os.path.join(os.path.dirname(request.fspath.strpath), "data"),
os.path.join(os.path.dirname(__file__), "..", "..", "..", "test-data")
]]
def get_path(self, bn):
""" return path of file or None if it doesn't exist. """
for path in self.paths:
fn = os.path.join(path, *bn.split("/"))
if os.path.exists(fn):
return fn
print("WARNING: path does not exist: {!r}".format(fn))
def read_path(self, bn, mode="r"):
fn = self.get_path(bn)
if fn is not None:
with open(fn, mode) as f:
return f.read()
return Data()
@pytest.fixture
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
class AccountMaker:
def __init__(self):
self.live_count = 0
self.offline_count = 0
self._finalizers = []
self._accounts = []
self.init_time = time.time()
self._generated_keys = ["alice", "bob", "charlie",
"dom", "elena", "fiona"]
self.set_logging_default(False)
def finalize(self):
while self._finalizers:
fin = self._finalizers.pop()
fin()
while self._accounts:
acc = self._accounts.pop()
acc.shutdown()
acc.disable_logging()
def make_account(self, path, logid, quiet=False):
ac = Account(path, logging=self._logging)
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
ac._configtracker = ac.add_account_plugin(ConfigureTracker())
if not quiet:
ac.add_account_plugin(FFIEventLogger(ac, logid=logid))
self._accounts.append(ac)
return ac
def set_logging_default(self, logging):
self._logging = bool(logging)
def get_unconfigured_account(self):
self.offline_count += 1
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
ac._evtracker.init_time = self.init_time
ac._evtracker.set_timeout(2)
return ac
def _preconfigure_key(self, account, addr):
# Only set a key if we haven't used it yet for another account.
if self._generated_keys:
keyname = self._generated_keys.pop(0)
fname_pub = data.read_path("key/{name}-public.asc".format(name=keyname))
fname_sec = data.read_path("key/{name}-secret.asc".format(name=keyname))
if fname_pub and fname_sec:
account._preconfigure_keypair(addr, fname_pub, fname_sec)
return True
else:
print("WARN: could not use preconfigured keys for {!r}".format(addr))
def get_configured_offline_account(self):
ac = self.get_unconfigured_account()
# do a pseudo-configured account
addr = "addr{}@offline.org".format(self.offline_count)
ac.set_config("addr", addr)
self._preconfigure_key(ac, addr)
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
ac.set_config("mail_pw", "123")
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
lib.dc_set_config(ac._dc_context, b"configured", b"1")
return ac
def get_online_config(self, pre_generated_key=True, quiet=False):
if not session_liveconfig:
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig")
configdict = session_liveconfig.get(self.live_count)
self.live_count += 1
if "e2ee_enabled" not in configdict:
configdict["e2ee_enabled"] = "1"
# Enable strict certificate checks for online accounts
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count), quiet=quiet)
if pre_generated_key:
self._preconfigure_key(ac, configdict['addr'])
ac._evtracker.init_time = self.init_time
ac._evtracker.set_timeout(30)
return ac, dict(configdict)
def get_online_configuring_account(self, mvbox=False, sentbox=False, move=False,
pre_generated_key=True, quiet=False, config={}):
ac, configdict = self.get_online_config(
pre_generated_key=pre_generated_key, quiet=quiet)
configdict.update(config)
configdict["mvbox_watch"] = str(int(mvbox))
configdict["mvbox_move"] = str(int(move))
configdict["sentbox_watch"] = str(int(sentbox))
ac.update_config(configdict)
ac.start()
return ac
def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False):
ac1 = self.get_online_configuring_account(
pre_generated_key=pre_generated_key, mvbox=mvbox, move=move)
ac1._configtracker.wait_imap_connected()
ac1._configtracker.wait_smtp_connected()
ac1._configtracker.wait_finish()
return ac1
def get_two_online_accounts(self, move=False, quiet=False):
ac1 = self.get_online_configuring_account(move=True, quiet=quiet)
ac2 = self.get_online_configuring_account(quiet=quiet)
ac1._configtracker.wait_finish()
ac2._configtracker.wait_finish()
return ac1, ac2
def clone_online_account(self, account, pre_generated_key=True):
self.live_count += 1
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
if pre_generated_key:
self._preconfigure_key(ac, account.get_config("addr"))
ac._evtracker.init_time = self.init_time
ac._evtracker.set_timeout(30)
ac.update_config(dict(
addr=account.get_config("addr"),
mail_pw=account.get_config("mail_pw"),
mvbox_watch=account.get_config("mvbox_watch"),
mvbox_move=account.get_config("mvbox_move"),
sentbox_watch=account.get_config("sentbox_watch"),
))
ac.start()
return ac
def run_bot_process(self, module, ffi=True):
fn = module.__file__
bot_ac, bot_cfg = self.get_online_config()
args = [
sys.executable,
"-u",
fn,
"--email", bot_cfg["addr"],
"--password", bot_cfg["mail_pw"],
bot_ac.db_path,
]
if ffi:
args.insert(-1, "--show-ffi")
print("$", " ".join(args))
popen = subprocess.Popen(
args=args,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # combine stdout/stderr in one stream
bufsize=0, # line buffering
close_fds=True, # close all FDs other than 0/1/2
universal_newlines=True # give back text
)
bot = BotProcess(popen, bot_cfg)
self._finalizers.append(bot.kill)
return bot
am = AccountMaker()
request.addfinalizer(am.finalize)
return am
class BotProcess:
def __init__(self, popen, bot_cfg):
self.popen = popen
self.addr = bot_cfg["addr"]
# we read stdout as quickly as we can in a thread and make
# the (unicode) lines available for readers through a queue.
self.stdout_queue = queue.Queue()
self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread")
t.setDaemon(1)
t.start()
def _run_stdout_thread(self):
try:
while 1:
line = self.popen.stdout.readline()
if not line:
break
line = line.strip()
self.stdout_queue.put(line)
finally:
self.stdout_queue.put(None)
def kill(self):
self.popen.kill()
def wait(self, timeout=30):
self.popen.wait(timeout=timeout)
def fnmatch_lines(self, pattern_lines):
patterns = [x.strip() for x in Source(pattern_lines.rstrip()).lines if x.strip()]
for next_pattern in patterns:
print("+++FNMATCH:", next_pattern)
ignored = []
while 1:
line = self.stdout_queue.get(timeout=15)
if line is None:
if ignored:
print("BOT stdout terminated after these lines")
for line in ignored:
print(line)
raise IOError("BOT stdout-thread terminated")
if fnmatch.fnmatch(line, next_pattern):
print("+++MATCHED:", line)
break
else:
print("+++IGN:", line)
ignored.append(line)
@pytest.fixture
def tmp_db_path(tmpdir):
return tmpdir.join("test.db").strpath
@pytest.fixture
def lp():
class Printer:
def sec(self, msg):
print()
print("=" * 10, msg, "=" * 10)
def step(self, msg):
print("-" * 5, "step " + msg, "-" * 5)
return Printer()

View File

@@ -0,0 +1,76 @@
from queue import Queue
from threading import Event
from .hookspec import account_hookimpl
class ImexFailed(RuntimeError):
""" Exception for signalling that import/export operations failed."""
class ImexTracker:
def __init__(self):
self._imex_events = Queue()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
if ffi_event.name == "DC_EVENT_IMEX_PROGRESS":
self._imex_events.put(ffi_event.data1)
elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN":
self._imex_events.put(ffi_event.data1)
def wait_finish(self, progress_timeout=60):
""" Return list of written files, raise ValueError if ExportFailed. """
files_written = []
while True:
ev = self._imex_events.get(timeout=progress_timeout)
if isinstance(ev, str):
files_written.append(ev)
elif ev == 0:
raise ImexFailed("export failed, exp-files: {}".format(files_written))
elif ev == 1000:
return files_written
class ConfigureFailed(RuntimeError):
""" Exception for signalling that configuration failed."""
class ConfigureTracker:
ConfigureFailed = ConfigureFailed
def __init__(self):
self._configure_events = Queue()
self._smtp_finished = Event()
self._imap_finished = Event()
self._ffi_events = []
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
self._ffi_events.append(ffi_event)
if ffi_event.name == "DC_EVENT_SMTP_CONNECTED":
self._smtp_finished.set()
elif ffi_event.name == "DC_EVENT_IMAP_CONNECTED":
self._imap_finished.set()
@account_hookimpl
def ac_configure_completed(self, success):
self._configure_events.put(success)
def wait_smtp_connected(self):
""" wait until smtp is configured. """
self._smtp_finished.wait()
def wait_imap_connected(self):
""" wait until smtp is configured. """
self._imap_finished.wait()
def wait_finish(self):
""" wait until configure is completed.
Raise Exception if Configure failed
"""
if not self._configure_events.get():
content = "\n".join(map(str, self._ffi_events))
raise ConfigureFailed(content)

View File

@@ -1,273 +1,18 @@
from __future__ import print_function
import os
import pytest
import requests
import time
from deltachat import Account
from deltachat import const
from deltachat.capi import lib
import tempfile
def pytest_addoption(parser):
parser.addoption(
"--liveconfig", action="store", default=None,
help="a file with >=2 lines where each line "
"contains NAME=VALUE config settings for one account"
)
def pytest_configure(config):
cfg = config.getoption('--liveconfig')
if not cfg:
cfg = os.getenv('DCC_PY_LIVECONFIG')
if cfg:
config.option.liveconfig = cfg
def pytest_report_header(config, startdir):
summary = []
t = tempfile.mktemp()
try:
ac = Account(t, eventlogging=False)
info = ac.get_info()
ac.shutdown()
finally:
os.remove(t)
summary.extend(['Deltachat core={} sqlite={}'.format(
info['deltachat_core_version'],
info['sqlite_version'],
)])
cfg = config.option.liveconfig
if cfg:
if "#" in cfg:
url, token = cfg.split("#", 1)
summary.append('Liveconfig provider: {}#<token ommitted>'.format(url))
else:
summary.append('Liveconfig file: {}'.format(cfg))
return summary
@pytest.fixture(scope="session")
def data():
class Data:
def __init__(self):
self.path = os.path.join(os.path.dirname(__file__), "data")
def get_path(self, bn):
fn = os.path.join(self.path, bn)
assert os.path.exists(fn)
return fn
return Data()
class SessionLiveConfigFromFile:
def __init__(self, fn):
self.fn = fn
self.configlist = []
for line in open(fn):
if line.strip() and not line.strip().startswith('#'):
d = {}
for part in line.split():
name, value = part.split("=")
d[name] = value
self.configlist.append(d)
def get(self, index):
return self.configlist[index]
def exists(self):
return bool(self.configlist)
class SessionLiveConfigFromURL:
def __init__(self, url, create_token):
self.configlist = []
self.url = url
self.create_token = create_token
def get(self, index):
try:
return self.configlist[index]
except IndexError:
assert index == len(self.configlist), index
res = requests.post(self.url, json={"token_create_user": int(self.create_token)})
if res.status_code != 200:
pytest.skip("creating newtmpuser failed {!r}".format(res))
d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"])
self.configlist.append(config)
return config
def exists(self):
return bool(self.configlist)
@pytest.fixture(scope="session")
def session_liveconfig(request):
liveconfig_opt = request.config.option.liveconfig
if liveconfig_opt:
if liveconfig_opt.startswith("http"):
url, create_token = liveconfig_opt.split("#", 1)
return SessionLiveConfigFromURL(url, create_token)
else:
return SessionLiveConfigFromFile(liveconfig_opt)
@pytest.fixture
def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
class AccountMaker:
def __init__(self):
self.live_count = 0
self.offline_count = 0
self._finalizers = []
self.init_time = time.time()
def finalize(self):
while self._finalizers:
fin = self._finalizers.pop()
fin()
def make_account(self, path, logid):
ac = Account(path, logid=logid)
self._finalizers.append(ac.shutdown)
return ac
def get_unconfigured_account(self):
self.offline_count += 1
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(2)
return ac
def get_configured_offline_account(self):
ac = self.get_unconfigured_account()
# do a pseudo-configured account
addr = "addr{}@offline.org".format(self.offline_count)
ac.set_config("addr", addr)
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
ac.set_config("mail_pw", "123")
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
lib.dc_set_config(ac._dc_context, b"configured", b"1")
return ac
def peek_online_config(self):
if not session_liveconfig:
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
return session_liveconfig.get(self.live_count)
def get_online_config(self):
if not session_liveconfig:
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
configdict = session_liveconfig.get(self.live_count)
self.live_count += 1
if "e2ee_enabled" not in configdict:
configdict["e2ee_enabled"] = "1"
# Enable strict certificate checks for online accounts
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(30)
return ac, dict(configdict)
def get_online_configuring_account(self, mvbox=False, sentbox=False):
ac, configdict = self.get_online_config()
ac.configure(**configdict)
ac.start_threads(mvbox=mvbox, sentbox=sentbox)
return ac
def get_one_online_account(self):
ac1 = self.get_online_configuring_account()
wait_successful_IMAP_SMTP_connection(ac1)
wait_configuration_progress(ac1, 1000)
return ac1
def get_two_online_accounts(self):
ac1 = self.get_online_configuring_account()
ac2 = self.get_online_configuring_account()
wait_successful_IMAP_SMTP_connection(ac1)
wait_configuration_progress(ac1, 1000)
wait_successful_IMAP_SMTP_connection(ac2)
wait_configuration_progress(ac2, 1000)
return ac1, ac2
def clone_online_account(self, account):
self.live_count += 1
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(30)
ac.configure(addr=account.get_config("addr"), mail_pw=account.get_config("mail_pw"))
ac.start_threads()
return ac
am = AccountMaker()
request.addfinalizer(am.finalize)
return am
@pytest.fixture
def tmp_db_path(tmpdir):
return tmpdir.join("test.db").strpath
@pytest.fixture
def lp():
class Printer:
def sec(self, msg):
print()
print("=" * 10, msg, "=" * 10)
def step(self, msg):
print("-" * 5, "step " + msg, "-" * 5)
return Printer()
def wait_configuration_progress(account, min_target, max_target=1001):
min_target = min(min_target, max_target)
while 1:
evt_name, data1, data2 = \
account._evlogger.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
if data1 >= min_target and data1 <= max_target:
event = account._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
if event.data1 >= min_target and event.data1 <= max_target:
print("** CONFIG PROGRESS {}".format(min_target), account)
break
def wait_securejoin_inviter_progress(account, target):
while 1:
evt_name, data1, data2 = \
account._evlogger.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
if data2 >= target:
event = account._evtracker.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
if event.data2 >= target:
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), account)
break
def wait_successful_IMAP_SMTP_connection(account):
imap_ok = smtp_ok = False
while not imap_ok or not smtp_ok:
evt_name, data1, data2 = \
account._evlogger.get_matching("DC_EVENT_(IMAP|SMTP)_CONNECTED")
if evt_name == "DC_EVENT_IMAP_CONNECTED":
imap_ok = True
print("** IMAP OK", account)
if evt_name == "DC_EVENT_SMTP_CONNECTED":
smtp_ok = True
print("** SMTP OK", account)
print("** IMAP and SMTP logins successful", account)
def wait_msgs_changed(account, chat_id, msg_id=None):
ev = account._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev[1] == chat_id
if msg_id is not None:
assert ev[2] == msg_id
return ev[2]

1
python/tests/data/key Symbolic link
View File

@@ -0,0 +1 @@
../../../test-data/key

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,18 @@ import shutil
import pytest
from filecmp import cmp
from conftest import wait_configuration_progress, wait_msgs_changed
from conftest import wait_configuration_progress
from deltachat import const
def wait_msgs_changed(account, chat_id, msg_id=None):
ev = account._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev.data1 == chat_id
if msg_id is not None:
assert ev.data2 == msg_id
return ev.data2
class TestOnlineInCreation:
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
@@ -91,22 +99,22 @@ class TestOnlineInCreation:
assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered()
lp.sec("wait for the messages to be delivered to SMTP")
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
assert ev[1] == chat.id
assert ev[2] == prepared_original.id
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
assert ev[1] == chat2.id
assert ev[2] == forwarded_id
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
assert ev.data1 == chat.id
assert ev.data2 == prepared_original.id
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
assert ev.data1 == chat2.id
assert ev.data2 == forwarded_id
lp.sec("wait1 for original or forwarded messages to arrive")
ev1 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev1[1] > const.DC_CHAT_ID_LAST_SPECIAL
received_original = ac2.get_message_by_id(ev1[2])
ev1 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev1.data1 > const.DC_CHAT_ID_LAST_SPECIAL
received_original = ac2.get_message_by_id(ev1.data2)
assert cmp(received_original.filename, orig, shallow=False)
lp.sec("wait2 for original or forwarded messages to arrive")
ev2 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev2[1] > const.DC_CHAT_ID_LAST_SPECIAL
assert ev2[1] != ev1[1]
received_copy = ac2.get_message_by_id(ev2[2])
ev2 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev2.data1 > const.DC_CHAT_ID_LAST_SPECIAL
assert ev2.data1 != ev1.data1
received_copy = ac2.get_message_by_id(ev2.data2)
assert cmp(received_copy.filename, orig, shallow=False)

View File

@@ -1,8 +1,9 @@
from __future__ import print_function
from deltachat import capi, cutil, const, set_context_callback, clear_context_callback
from deltachat import register_global_plugin
from deltachat.hookspec import global_hookimpl
from deltachat.capi import ffi
from deltachat.capi import lib
from deltachat.account import EventLogger
def test_empty_context():
@@ -17,29 +18,31 @@ def test_callback_None2int():
clear_context_callback(ctx)
def test_dc_close_events(tmpdir):
ctx = ffi.gc(
capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
evlog = EventLogger(ctx)
evlog.set_timeout(5)
set_context_callback(
ctx,
lambda ctx, evt_name, data1, data2: evlog(evt_name, data1, data2)
)
p = tmpdir.join("hello.db")
lib.dc_open(ctx, p.strpath.encode("ascii"), ffi.NULL)
capi.lib.dc_close(ctx)
def test_dc_close_events(tmpdir, acfactory):
ac1 = acfactory.get_unconfigured_account()
# register after_shutdown function
shutdowns = []
class ShutdownPlugin:
@global_hookimpl
def dc_account_after_shutdown(self, account):
assert account._dc_context is None
shutdowns.append(account)
register_global_plugin(ShutdownPlugin())
assert hasattr(ac1, "_dc_context")
ac1.shutdown()
assert shutdowns == [ac1]
def find(info_string):
evlog = ac1._evtracker
while 1:
ev = evlog.get_matching("DC_EVENT_INFO", check_error=False)
data2 = ev[2]
data2 = ev.data2
if info_string in data2:
return
else:
print("skipping event", *ev)
print("skipping event", ev)
find("disconnecting inbox-thread")
find("disconnecting sentbox-thread")
@@ -89,10 +92,10 @@ def test_markseen_invalid_message_ids(acfactory):
contact1 = ac1.create_contact(email="some1@example.com", name="some1")
chat = ac1.create_chat_by_contact(contact1)
chat.send_text("one messae")
ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
ac1._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
msg_ids = [9]
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))
ac1._evlogger.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
ac1._evtracker.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
def test_get_special_message_id_returns_empty_message(acfactory):
@@ -102,19 +105,12 @@ def test_get_special_message_id_returns_empty_message(acfactory):
assert msg.id == 0
def test_provider_info():
provider = lib.dc_provider_new_from_email(cutil.as_dc_charpointer("ex@example.com"))
assert cutil.from_dc_charpointer(
lib.dc_provider_get_overview_page(provider)
) == "https://providers.delta.chat/example.com"
assert cutil.from_dc_charpointer(lib.dc_provider_get_name(provider)) == "Example"
assert cutil.from_dc_charpointer(lib.dc_provider_get_markdown(provider)) == "\n..."
assert cutil.from_dc_charpointer(lib.dc_provider_get_status_date(provider)) == "2018-09"
assert lib.dc_provider_get_status(provider) == const.DC_PROVIDER_STATUS_PREPARATION
def test_provider_info_none():
assert lib.dc_provider_new_from_email(cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
def test_get_info_closed():

View File

@@ -1,27 +0,0 @@
import pytest
from deltachat import const
from deltachat import provider
def test_provider_info_from_email():
example = provider.Provider.from_email("email@example.com")
assert example.overview_page == "https://providers.delta.chat/example.com"
assert example.name == "Example"
assert example.markdown == "\n..."
assert example.status_date == "2018-09"
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
def test_provider_info_from_domain():
example = provider.Provider("example.com")
assert example.overview_page == "https://providers.delta.chat/example.com"
assert example.name == "Example"
assert example.markdown == "\n..."
assert example.status_date == "2018-09"
assert example.status == const.DC_PROVIDER_STATUS_PREPARATION
def test_provider_info_none():
with pytest.raises(provider.ProviderNotFoundError):
provider.Provider.from_email("email@unexistent.no")

View File

@@ -7,13 +7,14 @@ envlist =
[testenv]
commands =
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx {posargs:tests}
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs: tests examples}
python tests/package_wheels.py {toxworkdir}/wheelhouse
passenv =
TRAVIS
DCC_RS_DEV
DCC_RS_TARGET
DCC_PY_LIVECONFIG
DCC_NEW_TMP_EMAIL
CARGO_TARGET_DIR
RUSTC_WRAPPER
deps =
@@ -40,13 +41,13 @@ deps =
restructuredtext_lint
commands =
flake8 src/deltachat
flake8 tests/
flake8 tests/ examples/
rst-lint --encoding 'utf-8' README.rst
[testenv:doc]
changedir=doc
deps =
sphinx==2.2.0
sphinx
breathe
commands =
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
@@ -65,11 +66,10 @@ commands =
[pytest]
addopts = -v -ra
python_files = tests/test_*.py
addopts = -v -ra --strict-markers
norecursedirs = .tox
xfail_strict=true
timeout = 60
timeout = 90
timeout_method = thread
[flake8]

View File

@@ -1 +1 @@
nightly-2019-11-06
nightly-2020-03-12

80
scripts/proxy.py Normal file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python3
# Examples:
#
# Original server that doesn't use SSL:
# ./proxy.py 8080 imap.nauta.cu 143
# ./proxy.py 8081 smtp.nauta.cu 25
#
# Original server that uses SSL:
# ./proxy.py 8080 testrun.org 993 --ssl
# ./proxy.py 8081 testrun.org 465 --ssl
from datetime import datetime
import argparse
import selectors
import ssl
import socket
import socketserver
class Proxy(socketserver.ThreadingTCPServer):
allow_reuse_address = True
def __init__(self, proxy_host, proxy_port, real_host, real_port, use_ssl):
self.real_host = real_host
self.real_port = real_port
self.use_ssl = use_ssl
super().__init__((proxy_host, proxy_port), RequestHandler)
class RequestHandler(socketserver.BaseRequestHandler):
def handle(self):
print('{} - {} CONNECTED.'.format(datetime.now(), self.client_address))
total = 0
real_server = (self.server.real_host, self.server.real_port)
with socket.create_connection(real_server) as sock:
if self.server.use_ssl:
context = ssl.create_default_context()
sock = context.wrap_socket(
sock, server_hostname=real_server[0])
forward = {self.request: sock, sock: self.request}
sel = selectors.DefaultSelector()
sel.register(self.request, selectors.EVENT_READ,
self.client_address)
sel.register(sock, selectors.EVENT_READ, real_server)
active = True
while active:
events = sel.select()
for key, mask in events:
print('\n{} - {} wrote:'.format(datetime.now(), key.data))
data = key.fileobj.recv(1024)
received = len(data)
total += received
print(data)
print('{} Bytes\nTotal: {} Bytes'.format(received, total))
if data:
forward[key.fileobj].sendall(data)
else:
print('\nCLOSING CONNECTION.\n\n')
forward[key.fileobj].close()
key.fileobj.close()
active = False
if __name__ == '__main__':
p = argparse.ArgumentParser(description='Simple Python Proxy')
p.add_argument(
"proxy_port", help="the port where the proxy will listen", type=int)
p.add_argument('host', help="the real host")
p.add_argument('port', help="the port of the real host", type=int)
p.add_argument("--ssl", help="use ssl to connect to the real host",
action="store_true")
args = p.parse_args()
with Proxy('', args.proxy_port, args.host, args.port, args.ssl) as proxy:
proxy.serve_forever()

View File

@@ -35,7 +35,7 @@ if __name__ == "__main__":
if len(sys.argv) < 2:
for x in ("Cargo.toml", "deltachat-ffi/Cargo.toml"):
print("{}: {}".format(x, read_toml_version(x)))
raise SystemExit("need argument: new version, example 1.0.0-beta.27")
raise SystemExit("need argument: new version, example: 1.25.0")
newversion = sys.argv[1]
if newversion.count(".") < 2:
raise SystemExit("need at least two dots in version")
@@ -45,7 +45,7 @@ if __name__ == "__main__":
assert core_toml == ffi_toml, (core_toml, ffi_toml)
for line in open("CHANGELOG.md"):
## 1.0.0-beta5
## 1.25.0
if line.startswith("## "):
if line[2:].strip().startswith(newversion):
break

94
spec.md
View File

@@ -1,6 +1,6 @@
# Chat-over-Email specification
Version 0.20.0
Version 0.30.0
This document describes how emails can be used
to implement typical messenger functions
@@ -17,6 +17,9 @@ while staying compatible to existing MUAs.
- [Change group name](#change-group-name)
- [Set group image](#set-group-image)
- [Set profile image](#set-profile-image)
- [Locations](#locations)
- [User locations](#user-locations)
- [Points of interest](#points-of-interest)
- [Miscellaneous](#miscellaneous)
@@ -29,8 +32,7 @@ Messages SHOULD be encrypted by the
Meta data (at least the subject and all chat-headers) SHOULD be encrypted
by the [Memoryhole](https://github.com/autocrypt/memoryhole) standard.
If Memoryhole is not used,
the subject of encrypted messages SHOULD be replaced by the string
`Chat: Encrypted message` where the part after the colon MAY be localized.
the subject of encrypted messages SHOULD be replaced by the string `...`.
# Outgoing messages
@@ -113,6 +115,7 @@ but MUAs typically expose the sender in the UI.
Groups are chats with usually more than one recipient,
each defined by an email-address.
The sender plus the recipients are the group members.
All group members form the member list.
To allow different groups with the same members,
groups are identified by a group-id.
@@ -135,8 +138,7 @@ The group-name MUST be written to `Chat-Group-Name` header
to join a group chat on a second device any time).
The `Subject` header of outgoing group messages
SHOULD start with the characters `Chat:`
followed by the group-name and a colon followed by an excerpt of the message.
SHOULD be set to the group-name.
To identify the group-id on replies from normal MUAs,
the group-id MUST also be added to the message-id of outgoing messages.
@@ -177,12 +179,22 @@ to a normal single-user chat with the email-address given in `From`.
## Add and remove members
Messenger clients MUST construct the member list
from the `From`/`To` headers only on the first group message
or if they see a `Chat-Group-Member-Added`
or `Chat-Group-Member-Removed` action header.
Both headers MUST have the email-address
of the added or removed member as the value.
Messenger clients MUST init the member list
from the `From`/`To` headers on the first group message.
When a member is added later,
a `Chat-Group-Member-Added` action header must be sent
with the value set to the email-address of the added member.
When receiving a `Chat-Group-Member-Added` header, however,
_all missing_ members the `From`/`To` headers has to be added.
This is to mitigate problems when receiving messages
in different orders, esp. on creating new groups.
To remove a member, a `Chat-Group-Member-Removed` header must be sent
with the value set to the email-address of the member to remove.
When receiving a `Chat-Group-Member-Removed` header,
only exaxtly the given member has to be removed from the member list.
Messenger clients MUST NOT construct the member list
on other group messages
(this is to avoid accidentally altered To-lists in normal MUAs;
@@ -332,6 +344,64 @@ To save data, it is RECOMMENDED to add a `Chat-User-Avatar` header
only on image changes.
# Locations
Locations can be attachted to messages using
[standard kml-files](https://www.opengeospatial.org/standards/kml/)
with well-known names.
## User locations
To send the location of the sender,
the app can attach a file with the name `location.kml`.
The file can contain one or more locations.
Apps that support location streaming will typically collect some location events
and send them together in one file.
As each location has an independent timestamp,
the apps can show the location as a track.
Note that the `addr` attribute inside the `location.kml` file
MUST match the users email-address.
Otherwise, the file is discarded silently;
this is to protect against getting wrong locations,
eg. forwarded from a normal MUA.
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document addr="ndh@deltachat.de">
<Placemark>
<Timestamp><when>2020-01-11T20:40:19Z</when></Timestamp>
<Point><coordinates accuracy="1.2">1.234,5.678</coordinates></Point>
</Placemark>
<Placemark>
<Timestamp><when>2020-01-11T20:40:25Z</when></Timestamp>
<Point><coordinates accuracy="5.4">7.654,3.21</coordinates></Point>
</Placemark>
</Document>
</kml>
## Points of interest
To send an "Point of interest", a POI,
use a normal message and attach a file with the name `message.kml`.
In contrast to user locations, this file should contain only one location
and an address-attribute is not needed -
as the location belongs to the message content,
it is fine if the location is detected on forwarding etc.
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Placemark>
<Timestamp><when>2020-01-01T20:40:19Z</when></Timestamp>
<Point><coordinates accuracy="1.2">1.234,5.678</coordinates></Point>
</Placemark>
</Document>
</kml>
# Miscellaneous
Messengers SHOULD use the header `In-Reply-To` as usual.
@@ -368,4 +438,4 @@ as the sending time of the message as indicated by its Date header,
or the time of first receipt if that date is in the future or unavailable.
Copyright © 2017-2019 Delta Chat contributors.
Copyright © 2017-2020 Delta Chat contributors.

View File

@@ -6,10 +6,10 @@ use std::collections::BTreeMap;
use std::str::FromStr;
use std::{fmt, str};
use crate::constants::*;
use crate::contact::*;
use crate::context::Context;
use crate::key::*;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, SignedPublicKey};
/// Possible values for encryption preference
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive)]
@@ -52,13 +52,17 @@ impl str::FromStr for EncryptPreference {
#[derive(Debug)]
pub struct Aheader {
pub addr: String,
pub public_key: Key,
pub public_key: SignedPublicKey,
pub prefer_encrypt: EncryptPreference,
}
impl Aheader {
/// Creates new autocrypt header
pub fn new(addr: String, public_key: Key, prefer_encrypt: EncryptPreference) -> Self {
pub fn new(
addr: String,
public_key: SignedPublicKey,
prefer_encrypt: EncryptPreference,
) -> Self {
Aheader {
addr,
public_key,
@@ -71,9 +75,7 @@ impl Aheader {
wanted_from: &str,
headers: &[mailparse::MailHeader<'_>],
) -> Option<Self> {
use mailparse::MailHeaderMap;
if let Ok(Some(value)) = headers.get_first_value("Autocrypt") {
if let Some(value) = headers.get_header_value(HeaderDef::Autocrypt) {
match Self::from_str(&value) {
Ok(header) => {
if addr_cmp(&header.addr, wanted_from) {
@@ -125,14 +127,10 @@ impl str::FromStr for Aheader {
.split(';')
.filter_map(|a| {
let attribute: Vec<&str> = a.trim().splitn(2, '=').collect();
if attribute.len() < 2 {
return None;
match &attribute[..] {
[key, value] => Some((key.trim().to_string(), value.trim().to_string())),
_ => None,
}
Some((
attribute[0].trim().to_string(),
attribute[1].trim().to_string(),
))
})
.collect();
@@ -142,22 +140,11 @@ impl str::FromStr for Aheader {
return Err(());
}
};
let public_key = match attributes
let public_key: SignedPublicKey = attributes
.remove("keydata")
.and_then(|raw| Key::from_base64(&raw, KeyType::Public))
{
Some(key) => {
if key.verify() {
key
} else {
return Err(());
}
}
None => {
return Err(());
}
};
.ok_or(())
.and_then(|raw| SignedPublicKey::from_base64(&raw).or(Err(())))
.and_then(|key| key.verify().and(Ok(key)).or(Err(())))?;
let prefer_encrypt = attributes
.remove("prefer-encrypt")
@@ -292,7 +279,7 @@ mod tests {
"{}",
Aheader::new(
"test@example.com".to_string(),
Key::from_base64(RAWKEY, KeyType::Public).unwrap(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::Mutual
)
)
@@ -305,7 +292,7 @@ mod tests {
"{}",
Aheader::new(
"test@example.com".to_string(),
Key::from_base64(RAWKEY, KeyType::Public).unwrap(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::NoPreference
)
)

View File

@@ -6,13 +6,13 @@ use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use self::image::GenericImageView;
use image::GenericImageView;
use thiserror::Error;
use crate::constants::AVATAR_SIZE;
use crate::context::Context;
use crate::events::Event;
extern crate image;
/// Represents a file in the blob directory.
///
/// The object has a name, which will always be valid UTF-8. Having a
@@ -56,7 +56,6 @@ impl<'a> BlobObject<'a> {
blobdir: blobdir.to_path_buf(),
blobname: name.clone(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
let blob = BlobObject {
blobdir,
@@ -84,7 +83,6 @@ impl<'a> BlobObject<'a> {
blobdir: dir.to_path_buf(),
blobname: name,
cause: err,
backtrace: failure::Backtrace::new(),
});
} else {
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
@@ -97,7 +95,6 @@ impl<'a> BlobObject<'a> {
blobdir: dir.to_path_buf(),
blobname: name,
cause: std::io::Error::new(std::io::ErrorKind::Other, "supposedly unreachable"),
backtrace: failure::Backtrace::new(),
})
}
@@ -122,7 +119,6 @@ impl<'a> BlobObject<'a> {
blobname: String::from(""),
src: src.as_ref().to_path_buf(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
let (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy());
let (name, mut dst_file) = BlobObject::create_new_file(context.get_blobdir(), &stem, &ext)?;
@@ -138,7 +134,6 @@ impl<'a> BlobObject<'a> {
blobname: name_for_err,
src: src.as_ref().to_path_buf(),
cause: err,
backtrace: failure::Backtrace::new(),
}
})?;
let blob = BlobObject {
@@ -198,17 +193,14 @@ impl<'a> BlobObject<'a> {
.map_err(|_| BlobError::WrongBlobdir {
blobdir: context.get_blobdir().to_path_buf(),
src: path.as_ref().to_path_buf(),
backtrace: failure::Backtrace::new(),
})?;
if !BlobObject::is_acceptible_blob_name(&rel_path) {
return Err(BlobError::WrongName {
blobname: path.as_ref().to_path_buf(),
backtrace: failure::Backtrace::new(),
});
}
let name = rel_path.to_str().ok_or_else(|| BlobError::WrongName {
blobname: path.as_ref().to_path_buf(),
backtrace: failure::Backtrace::new(),
})?;
BlobObject::from_name(context, name.to_string())
}
@@ -236,7 +228,6 @@ impl<'a> BlobObject<'a> {
if !BlobObject::is_acceptible_blob_name(&name) {
return Err(BlobError::WrongName {
blobname: PathBuf::from(name),
backtrace: failure::Backtrace::new(),
});
}
Ok(BlobObject {
@@ -322,13 +313,12 @@ impl<'a> BlobObject<'a> {
let clean = sanitize_filename::sanitize_with_options(name, opts);
let mut iter = clean.splitn(2, '.');
let mut stem = iter.next().unwrap_or_default().to_string();
let mut ext = iter.next().unwrap_or_default().to_string();
stem.truncate(64);
ext.truncate(32);
match ext.len() {
0 => (stem, "".to_string()),
_ => (stem, format!(".{}", ext).to_lowercase()),
let stem: String = iter.next().unwrap_or_default().chars().take(64).collect();
let ext: String = iter.next().unwrap_or_default().chars().take(32).collect();
if ext.is_empty() {
(stem, "".to_string())
} else {
(stem, format!(".{}", ext).to_lowercase())
}
}
@@ -360,7 +350,6 @@ impl<'a> BlobObject<'a> {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
if img.width() <= AVATAR_SIZE && img.height() <= AVATAR_SIZE {
@@ -373,7 +362,6 @@ impl<'a> BlobObject<'a> {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
Ok(())
@@ -387,98 +375,41 @@ impl<'a> fmt::Display for BlobObject<'a> {
}
/// Errors for the [BlobObject].
#[derive(Fail, Debug)]
#[derive(Debug, Error)]
pub enum BlobError {
#[error("Failed to create blob {blobname} in {}", .blobdir.display())]
CreateFailure {
blobdir: PathBuf,
blobname: String,
#[cause]
#[source]
cause: std::io::Error,
backtrace: failure::Backtrace,
},
#[error("Failed to write data to blob {blobname} in {}", .blobdir.display())]
WriteFailure {
blobdir: PathBuf,
blobname: String,
#[cause]
#[source]
cause: std::io::Error,
backtrace: failure::Backtrace,
},
#[error("Failed to copy data from {} to blob {blobname} in {}", .src.display(), .blobdir.display())]
CopyFailure {
blobdir: PathBuf,
blobname: String,
src: PathBuf,
#[cause]
#[source]
cause: std::io::Error,
backtrace: failure::Backtrace,
},
#[error("Failed to recode to blob {blobname} in {}", .blobdir.display())]
RecodeFailure {
blobdir: PathBuf,
blobname: String,
#[cause]
#[source]
cause: image::ImageError,
backtrace: failure::Backtrace,
},
WrongBlobdir {
blobdir: PathBuf,
src: PathBuf,
backtrace: failure::Backtrace,
},
WrongName {
blobname: PathBuf,
backtrace: failure::Backtrace,
},
}
// Implementing Display is done by hand because the failure
// #[fail(display = "...")] syntax does not allow using
// `blobdir.display()`.
impl fmt::Display for BlobError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Match on the data rather than kind, they are equivalent for
// identifying purposes but contain the actual data we need.
match &self {
BlobError::CreateFailure {
blobdir, blobname, ..
} => write!(
f,
"Failed to create blob {} in {}",
blobname,
blobdir.display()
),
BlobError::WriteFailure {
blobdir, blobname, ..
} => write!(
f,
"Failed to write data to blob {} in {}",
blobname,
blobdir.display()
),
BlobError::CopyFailure {
blobdir,
blobname,
src,
..
} => write!(
f,
"Failed to copy data from {} to blob {} in {}",
src.display(),
blobname,
blobdir.display(),
),
BlobError::RecodeFailure {
blobdir, blobname, ..
} => write!(f, "Failed to recode {} in {}", blobname, blobdir.display(),),
BlobError::WrongBlobdir { blobdir, src, .. } => write!(
f,
"File path {} is not in blobdir {}",
src.display(),
blobdir.display(),
),
BlobError::WrongName { blobname, .. } => {
write!(f, "Blob has a bad name: {}", blobname.display(),)
}
}
}
#[error("File path {} is not in {}", .src.display(), .blobdir.display())]
WrongBlobdir { blobdir: PathBuf, src: PathBuf },
#[error("Blob has a badname {}", .blobname.display())]
WrongName { blobname: PathBuf },
}
#[cfg(test)]
@@ -631,4 +562,11 @@ mod tests {
assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
}
#[test]
fn test_sanitise_name() {
let (_, ext) =
BlobObject::sanitise_name("Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt");
assert_eq!(ext, ".txt");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
//! # Chat list module
use crate::chat;
use crate::chat::*;
use crate::constants::*;
use crate::contact::*;
use crate::context::*;
use crate::error::Result;
use crate::error::{bail, ensure, Result};
use crate::lot::Lot;
use crate::message::{Message, MessageState, MsgId};
use crate::stock::StockMessage;
@@ -60,7 +61,7 @@ impl Chatlist {
/// or "Not now".
/// The UI can also offer a "Close" button that calls dc_marknoticed_contact() then.
/// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
/// archived *any* chat using dc_archive_chat(). The UI should show a link as
/// archived *any* chat using dc_set_chat_visibility(). The UI should show a link as
/// "Show archived chats", if the user clicks this item, the UI should show a
/// list of all archived chats that can be created by this function hen using
/// the DC_GCL_ARCHIVED_ONLY flag.
@@ -73,6 +74,9 @@ impl Chatlist {
/// if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
/// chats
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
/// and hides the device-chat,
// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
/// not needed when DC_GCL_ARCHIVED_ONLY is already set)
@@ -88,6 +92,17 @@ impl Chatlist {
query: Option<&str>,
query_contact_id: Option<u32>,
) -> Result<Self> {
let flag_archived_only = 0 != listflags & DC_GCL_ARCHIVED_ONLY;
let flag_for_forwarding = 0 != listflags & DC_GCL_FOR_FORWARDING;
let flag_no_specials = 0 != listflags & DC_GCL_NO_SPECIALS;
let flag_add_alldone_hint = 0 != listflags & DC_GCL_ADD_ALLDONE_HINT;
// Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some
// messages get deleted to avoid reloading the same chatlist.
if let Err(err) = delete_device_expired_messages(context) {
warn!(context, "Failed to hide expired messages: {}", err);
}
let mut add_archived_link_item = false;
let process_row = |row: &rusqlite::Row| {
@@ -101,6 +116,14 @@ impl Chatlist {
.map_err(Into::into)
};
let skip_id = if flag_for_forwarding {
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
.unwrap_or_default()
.0
} else {
ChatId::new(0)
};
// select with left join and minimum:
//
// - the inner select must use `hidden` and _not_ `m.hidden`
@@ -127,18 +150,21 @@ impl Chatlist {
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?))
AND (hidden=0 OR state=?1))
WHERE c.id>9
AND c.blocked=0
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, query_contact_id as i32],
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
process_row,
process_rows,
)?
} else if 0 != listflags & DC_GCL_ARCHIVED_ONLY {
} else if flag_archived_only {
// show archived chats
// (this includes the archived device-chat; we could skip it,
// however, then the number of archived chats do not match, which might be even more irritating.
// and adapting the number requires larger refactorings and seems not to be worth the effort)
context.sql.query_map(
"SELECT c.id, m.id
FROM chats c
@@ -178,18 +204,25 @@ impl Chatlist {
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?))
WHERE c.id>9
AND (hidden=0 OR state=?1))
WHERE c.id>9 AND c.id!=?2
AND c.blocked=0
AND c.name LIKE ?
AND c.name LIKE ?3
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, str_like_cmd],
params![MessageState::OutDraft, skip_id, str_like_cmd],
process_row,
process_rows,
)?
} else {
// show normal chatlist
let sort_id_up = if flag_for_forwarding {
chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
.unwrap_or_default()
.0
} else {
ChatId::new(0)
};
let mut ids = context.sql.query_map(
"SELECT c.id, m.id
FROM chats c
@@ -199,22 +232,24 @@ impl Chatlist {
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?))
WHERE c.id>9
AND (hidden=0 OR state=?1))
WHERE c.id>9 AND c.id!=?2
AND c.blocked=0
AND c.archived=0
AND NOT c.archived=?3
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft],
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
process_row,
process_rows,
)?;
if 0 == listflags & DC_GCL_NO_SPECIALS {
if !flag_no_specials {
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context) {
ids.insert(
0,
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
);
if !flag_for_forwarding {
ids.insert(
0,
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
);
}
}
add_archived_link_item = true;
}
@@ -222,7 +257,7 @@ impl Chatlist {
};
if add_archived_link_item && dc_get_archived_cnt(context) > 0 {
if ids.is_empty() && 0 != listflags & DC_GCL_ADD_ALLDONE_HINT {
if ids.is_empty() && flag_add_alldone_hint {
ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0)));
}
ids.push((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0)));
@@ -245,18 +280,20 @@ impl Chatlist {
///
/// To get the message object from the message ID, use dc_get_chat().
pub fn get_chat_id(&self, index: usize) -> ChatId {
if index >= self.ids.len() {
return ChatId::new(0);
match self.ids.get(index) {
Some((chat_id, _msg_id)) => *chat_id,
None => ChatId::new(0),
}
self.ids[index].0
}
/// Get a single message ID of a chatlist.
///
/// To get the message object from the message ID, use dc_get_msg().
pub fn get_msg_id(&self, index: usize) -> Result<MsgId> {
ensure!(index < self.ids.len(), "Chatlist index out of range");
Ok(self.ids[index].1)
match self.ids.get(index) {
Some((_chat_id, msg_id)) => Ok(*msg_id),
None => bail!("Chatlist index out of range"),
}
}
/// Get a summary for a chatlist index.
@@ -278,27 +315,29 @@ impl Chatlist {
// This is because we may want to display drafts here or stuff as
// "is typing".
// Also, sth. as "No messages" would not work if the summary comes from a message.
let mut ret = Lot::new();
if index >= self.ids.len() {
ret.text2 = Some("ErrBadChatlistIndex".to_string());
return ret;
}
let (chat_id, lastmsg_id) = match self.ids.get(index) {
Some(ids) => ids,
None => {
ret.text2 = Some("ErrBadChatlistIndex".to_string());
return ret;
}
};
let chat_loaded: Chat;
let chat = if let Some(chat) = chat {
chat
} else if let Ok(chat) = Chat::load_from_db(context, self.ids[index].0) {
} else if let Ok(chat) = Chat::load_from_db(context, *chat_id) {
chat_loaded = chat;
&chat_loaded
} else {
return ret;
};
let lastmsg_id = self.ids[index].1;
let mut lastcontact = None;
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id) {
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, *lastmsg_id) {
if lastmsg.from_id != DC_CONTACT_ID_SELF
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
{
@@ -321,6 +360,10 @@ impl Chatlist {
ret
}
pub fn get_index_for_id(&self, id: ChatId) -> Option<usize> {
self.ids.iter().position(|(chat_id, _)| chat_id == &id)
}
}
/// Returns the number of archived chats
@@ -358,7 +401,6 @@ fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
mod tests {
use super::*;
use crate::chat;
use crate::test_utils::*;
#[test]
@@ -378,7 +420,7 @@ mod tests {
// drafts are sorted to the top
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hello".to_string()));
set_draft(&t.ctx, chat_id2, Some(&mut msg));
chat_id2.set_draft(&t.ctx, Some(&mut msg));
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.get_chat_id(0), chat_id2);
@@ -389,11 +431,32 @@ mod tests {
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
assert_eq!(chats.len(), 0);
chat::archive(&t.ctx, chat_id1, true).ok();
chat_id1
.set_visibility(&t.ctx, ChatVisibility::Archived)
.ok();
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
assert_eq!(chats.len(), 1);
}
#[test]
fn test_sort_self_talk_up_on_forward() {
let t = dummy_context();
t.ctx.update_device_chats().unwrap();
create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert!(chats.len() == 3);
assert!(!Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
.unwrap()
.is_self_talk());
let chats = Chatlist::try_load(&t.ctx, DC_GCL_FOR_FORWARDING, None, None).unwrap();
assert!(chats.len() == 2); // device chat cannot be written and is skipped on forwarding
assert!(Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
.unwrap()
.is_self_talk());
}
#[test]
fn test_search_special_chat_names() {
let t = dummy_context();
@@ -416,4 +479,18 @@ mod tests {
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None).unwrap();
assert_eq!(chats.len(), 1);
}
#[test]
fn test_get_summary_unwrap() {
let t = dummy_context();
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap();
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("foo:\nbar \r\n test".to_string()));
chat_id1.set_draft(&t.ctx, Some(&mut msg));
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
let summary = chats.get_summary(&t.ctx, 0, None);
assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary
}
}

View File

@@ -4,10 +4,14 @@ use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
use crate::blob::BlobObject;
use crate::chat::ChatId;
use crate::constants::DC_VERSION_STR;
use crate::context::Context;
use crate::dc_tools::*;
use crate::events::Event;
use crate::job::*;
use crate::message::MsgId;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::stock::StockMessage;
use rusqlite::NO_PARAMS;
@@ -61,6 +65,31 @@ pub enum Config {
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
ShowEmails,
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
MediaQuality,
#[strum(props(default = "0"))]
KeyGenType,
/// Timer in seconds after which the message is deleted from the
/// server.
///
/// Equals to 0 by default, which means the message is never
/// deleted.
///
/// Value 1 is treated as "delete at once": messages are deleted
/// immediately, without moving to DeltaChat folder.
#[strum(props(default = "0"))]
DeleteServerAfter,
/// Timer in seconds after which the message is deleted from the
/// device.
///
/// Equals to 0 by default, which means the message is never
/// deleted.
#[strum(props(default = "0"))]
DeleteDeviceAfter,
SaveMimeHeaders,
ConfiguredAddr,
ConfiguredMailServer,
@@ -98,7 +127,7 @@ impl Context {
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
}
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", 24 * 1024 * 1024 / 4 * 3)),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(self, key),
};
@@ -124,6 +153,29 @@ impl Context {
self.get_config_int(key) != 0
}
/// Gets configured "delete_server_after" value.
///
/// `None` means never delete the message, `Some(0)` means delete
/// at once, `Some(x)` means delete after `x` seconds.
pub fn get_config_delete_server_after(&self) -> Option<i64> {
match self.get_config_int(Config::DeleteServerAfter) {
0 => None,
1 => Some(0),
x => Some(x as i64),
}
}
/// Gets configured "delete_device_after" value.
///
/// `None` means never delete the message, `Some(x)` means delete
/// after `x` seconds.
pub fn get_config_delete_device_after(&self) -> Option<i64> {
match self.get_config_int(Config::DeleteDeviceAfter) {
0 => None,
x => Some(x as i64),
}
}
/// Set the given config key.
/// If `None` is passed as a value the value is cleared and set to the default if there is one.
pub fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
@@ -167,6 +219,15 @@ impl Context {
self.sql.set_raw_config(self, key, val)
}
Config::DeleteDeviceAfter => {
let ret = self.sql.set_raw_config(self, key, value);
// Force chatlist reload to delete old messages immediately.
self.call_cb(Event::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
ret
}
_ => self.sql.set_raw_config(self, key, value),
}
}
@@ -190,9 +251,11 @@ mod tests {
use std::str::FromStr;
use std::string::ToString;
use crate::constants;
use crate::constants::AVATAR_SIZE;
use crate::test_utils::*;
use image::GenericImageView;
use num_traits::FromPrimitive;
use std::fs::File;
use std::io::Write;
@@ -265,4 +328,44 @@ mod tests {
assert_eq!(img.width(), AVATAR_SIZE);
assert_eq!(img.height(), AVATAR_SIZE);
}
#[test]
fn test_selfavatar_copy_without_recode() {
let t = dummy_context();
let avatar_src = t.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
File::create(&avatar_src)
.unwrap()
.write_all(avatar_bytes)
.unwrap();
let avatar_blob = t.ctx.get_blobdir().join("avatar.png");
assert!(!avatar_blob.exists());
t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
.unwrap();
assert!(avatar_blob.exists());
assert_eq!(
std::fs::metadata(&avatar_blob).unwrap().len(),
avatar_bytes.len() as u64
);
let avatar_cfg = t.ctx.get_config(Config::Selfavatar);
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
}
#[test]
fn test_media_quality_config_option() {
let t = dummy_context();
let media_quality = t.ctx.get_config_int(Config::MediaQuality);
assert_eq!(media_quality, 0);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
assert_eq!(media_quality, constants::MediaQuality::Balanced);
t.ctx.set_config(Config::MediaQuality, Some("1")).unwrap();
let media_quality = t.ctx.get_config_int(Config::MediaQuality);
assert_eq!(media_quality, 1);
assert_eq!(constants::MediaQuality::Worse as i32, 1);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
assert_eq!(media_quality, constants::MediaQuality::Worse);
}
}

View File

@@ -1,7 +1,6 @@
//! # Thunderbird's Autoconfiguration implementation
//!
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::constants::*;
@@ -9,33 +8,7 @@ use crate::context::Context;
use crate::login_param::LoginParam;
use super::read_url::read_url;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "Invalid email address: {:?}", _0)]
InvalidEmailAddress(String),
#[fail(display = "XML error at position {}", position)]
InvalidXml {
position: usize,
#[cause]
error: quick_xml::Error,
},
#[fail(display = "Bad or incomplete autoconfig")]
IncompleteAutoconfig(LoginParam),
#[fail(display = "Failed to get URL {}", _0)]
ReadUrlError(#[cause] super::read_url::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
impl From<super::read_url::Error> for Error {
fn from(err: super::read_url::Error) -> Error {
Error::ReadUrlError(err)
}
}
use super::Error;
#[derive(Debug)]
struct MozAutoconfigure<'a> {
@@ -65,16 +38,16 @@ enum MozConfigTag {
Username,
}
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam> {
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam, Error> {
let mut reader = quick_xml::Reader::from_str(xml_raw);
reader.trim_text(true);
// Split address into local part and domain part.
let p = in_emailaddr
.find('@')
.ok_or_else(|| Error::InvalidEmailAddress(in_emailaddr.to_string()))?;
let (in_emaillocalpart, in_emaildomain) = in_emailaddr.split_at(p);
let in_emaildomain = &in_emaildomain[1..];
let parts: Vec<&str> = in_emailaddr.rsplitn(2, '@').collect();
let (in_emaillocalpart, in_emaildomain) = match &parts[..] {
[domain, local] => (local, domain),
_ => return Err(Error::InvalidEmailAddress(in_emailaddr.to_string())),
};
let mut moz_ac = MozAutoconfigure {
in_emailaddr,
@@ -125,7 +98,7 @@ pub fn moz_autoconfigure(
context: &Context,
url: &str,
param_in: &LoginParam,
) -> Result<LoginParam> {
) -> Result<LoginParam, Error> {
let xml_raw = read_url(context, url)?;
let res = parse_xml(&param_in.addr, &xml_raw);

View File

@@ -1,6 +1,5 @@
//! Outlook's Autodiscover
use quick_xml;
use quick_xml::events::BytesEnd;
use crate::constants::*;
@@ -8,33 +7,7 @@ use crate::context::Context;
use crate::login_param::LoginParam;
use super::read_url::read_url;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "XML error at position {}", position)]
InvalidXml {
position: usize,
#[cause]
error: quick_xml::Error,
},
#[fail(display = "Bad or incomplete autoconfig")]
IncompleteAutoconfig(LoginParam),
#[fail(display = "Failed to get URL {}", _0)]
ReadUrlError(#[cause] super::read_url::Error),
#[fail(display = "Number of redirection is exceeded")]
RedirectionError,
}
pub type Result<T> = std::result::Result<T, Error>;
impl From<super::read_url::Error> for Error {
fn from(err: super::read_url::Error) -> Error {
Error::ReadUrlError(err)
}
}
use super::Error;
struct OutlookAutodiscover {
pub out: LoginParam,
@@ -52,7 +25,7 @@ enum ParsingResult {
RedirectUrl(String),
}
fn parse_xml(xml_raw: &str) -> Result<ParsingResult> {
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
let mut outlk_ad = OutlookAutodiscover {
out: LoginParam::new(),
out_imap_set: false,
@@ -143,7 +116,7 @@ pub fn outlk_autodiscover(
context: &Context,
url: &str,
_param_in: &LoginParam,
) -> Result<LoginParam> {
) -> Result<LoginParam, Error> {
let mut url = url.to_string();
/* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */
for _i in 0..10 {

View File

@@ -12,12 +12,14 @@ use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::e2ee;
use crate::error::format_err;
use crate::job::{self, job_add, job_kill_action};
use crate::login_param::{CertificateChecks, LoginParam};
use crate::oauth2::*;
use crate::param::Params;
use crate::{chat, e2ee, provider};
use crate::message::Message;
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
@@ -31,26 +33,28 @@ macro_rules! progress {
};
}
// connect
pub fn configure(context: &Context) {
if context.has_ongoing() {
warn!(context, "There is already another ongoing process running.",);
return;
impl Context {
/// Starts a configuration job.
pub fn configure(&self) {
if self.has_ongoing() {
warn!(self, "There is already another ongoing process running.",);
return;
}
job_kill_action(self, job::Action::ConfigureImap);
job_add(self, job::Action::ConfigureImap, 0, Params::new(), 0);
}
job_kill_action(context, job::Action::ConfigureImap);
job_add(context, job::Action::ConfigureImap, 0, Params::new(), 0);
}
/// Check if the context is already configured.
pub fn dc_is_configured(context: &Context) -> bool {
context.sql.get_raw_config_bool(context, "configured")
/// Checks if the context is already configured.
pub fn is_configured(&self) -> bool {
self.sql.get_raw_config_bool(self, "configured")
}
}
/*******************************************************************************
* Configure JOB
******************************************************************************/
#[allow(non_snake_case, unused_must_use, clippy::cognitive_complexity)]
pub fn JobConfigureImap(context: &Context) -> job::Status {
pub(crate) fn JobConfigureImap(context: &Context) -> job::Status {
if !context.sql.is_open() {
error!(context, "Cannot configure, database not opened.",);
progress!(context, 0);
@@ -65,6 +69,7 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
let mut smtp_connected_here = false;
let mut param_autoconfig: Option<LoginParam> = None;
let was_configured_before = context.is_configured();
context
.inbox_thread
@@ -200,10 +205,7 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
7 => {
progress!(context, 310);
if param_autoconfig.is_none() {
let url = format!(
"https://{}{}/autodiscover/autodiscover.xml",
"", param_domain
);
let url = format!("https://{}/autodiscover/autodiscover.xml", param_domain);
param_autoconfig = outlk_autodiscover(context, &url, &param).ok();
}
true
@@ -371,7 +373,7 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
let create_mvbox = context.get_config_bool(Config::MvboxWatch)
|| context.get_config_bool(Config::MvboxMove);
let imap = &context.inbox_thread.read().unwrap().imap;
if let Err(err) = imap.ensure_configured_folders(context, create_mvbox) {
if let Err(err) = imap.configure_folders(context, create_mvbox) {
warn!(context, "configuring folders failed: {:?}", err);
false
} else {
@@ -439,45 +441,86 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
LoginParam::from_database(context, "configured_raw_").save_to_database(context, "");
}
if let Some(provider) = provider::get_provider_info(&param.addr) {
if !was_configured_before {
if let Some(config_defaults) = &provider.config_defaults {
for def in config_defaults.iter() {
info!(context, "apply config_defaults {}={}", def.key, def.value);
context.set_config(def.key, Some(def.value));
}
}
}
if !provider.after_login_hint.is_empty() {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(provider.after_login_hint.to_string());
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg)).is_err() {
warn!(context, "cannot add after_login_hint as core-provider-info");
}
}
}
context.free_ongoing();
progress!(context, if success { 1000 } else { 0 });
job::Status::Finished(Ok(()))
}
#[allow(clippy::unnecessary_unwrap)]
fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option<LoginParam> {
// XXX we don't have https://github.com/deltachat/provider-db APIs
// integrated yet but we'll already add nauta as a first use case, also
// showing what we need from provider-db in the future.
info!(
context,
"checking internal provider-info for offline autoconfig"
);
if param.addr.ends_with("@nauta.cu") {
let mut p = LoginParam::new();
if let Some(provider) = provider::get_provider_info(&param.addr) {
match provider.status {
provider::Status::OK | provider::Status::PREPARATION => {
let imap = provider.get_imap_server();
let smtp = provider.get_smtp_server();
// clippy complains about these is_some()/unwrap() settings,
// however, rewriting the code to "if let" would make things less obvious,
// esp. if we allow more combinations of servers (pop, jmap).
// therefore, #[allow(clippy::unnecessary_unwrap)] is added above.
if imap.is_some() && smtp.is_some() {
let imap = imap.unwrap();
let smtp = smtp.unwrap();
p.addr = param.addr.clone();
p.mail_server = "imap.nauta.cu".to_string();
p.mail_user = param.addr.clone();
p.mail_pw = param.mail_pw.clone();
p.mail_port = 143;
p.imap_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
let mut p = LoginParam::new();
p.addr = param.addr.clone();
p.send_server = "smtp.nauta.cu".to_string();
p.send_user = param.addr.clone();
p.send_pw = param.mail_pw.clone();
p.send_port = 25;
p.smtp_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags = DC_LP_AUTH_NORMAL as i32
| DC_LP_IMAP_SOCKET_STARTTLS as i32
| DC_LP_SMTP_SOCKET_STARTTLS as i32;
p.mail_server = imap.hostname.to_string();
p.mail_user = imap.apply_username_pattern(param.addr.clone());
p.mail_port = imap.port as i32;
p.imap_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags |= match imap.socket {
provider::Socket::STARTTLS => DC_LP_IMAP_SOCKET_STARTTLS,
provider::Socket::SSL => DC_LP_IMAP_SOCKET_SSL,
};
info!(context, "found offline autoconfig: {}", p);
Some(p)
} else {
info!(context, "no offline autoconfig found");
None
p.send_server = smtp.hostname.to_string();
p.send_user = smtp.apply_username_pattern(param.addr.clone());
p.send_port = smtp.port as i32;
p.smtp_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags |= match smtp.socket {
provider::Socket::STARTTLS => DC_LP_SMTP_SOCKET_STARTTLS as i32,
provider::Socket::SSL => DC_LP_SMTP_SOCKET_SSL as i32,
};
info!(context, "offline autoconfig found: {}", p);
return Some(p);
} else {
info!(context, "offline autoconfig found, but no servers defined");
return None;
}
}
provider::Status::BROKEN => {
info!(context, "offline autoconfig found, provider is broken");
return None;
}
}
}
info!(context, "no offline autoconfig found");
None
}
fn try_imap_connections(
@@ -619,6 +662,28 @@ fn try_smtp_one_param(context: &Context, param: &LoginParam) -> Option<bool> {
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Invalid email address: {0:?}")]
InvalidEmailAddress(String),
#[error("XML error at position {position}")]
InvalidXml {
position: usize,
#[source]
error: quick_xml::Error,
},
#[error("Bad or incomplete autoconfig")]
IncompleteAutoconfig(LoginParam),
#[error("Failed to get URL")]
ReadUrlError(#[from] self::read_url::Error),
#[error("Number of redirection is exceeded")]
RedirectionError,
}
#[cfg(test)]
mod tests {

View File

@@ -1,14 +1,12 @@
use crate::context::Context;
#[derive(Debug, Fail)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[fail(display = "URL request error")]
GetError(#[cause] reqwest::Error),
#[error("URL request error")]
GetError(#[from] reqwest::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
pub fn read_url(context: &Context, url: &str) -> Result<String> {
pub fn read_url(context: &Context, url: &str) -> Result<String, Error> {
info!(context, "Requesting URL {}", url);
match reqwest::blocking::Client::new()

View File

@@ -3,6 +3,7 @@
use deltachat_derive::*;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
lazy_static! {
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
@@ -15,7 +16,20 @@ const DC_SENTBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
)]
#[repr(u8)]
pub enum Blocked {
Not = 0,
@@ -43,7 +57,32 @@ impl Default for ShowEmails {
}
}
pub const DC_IMAP_SEEN: u32 = 0x1;
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(u8)]
pub enum MediaQuality {
Balanced = 0,
Worse = 1,
}
impl Default for MediaQuality {
fn default() -> Self {
MediaQuality::Balanced // also change Config.MediaQuality props(default) on changes
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(u8)]
pub enum KeyGenType {
Default = 0,
Rsa2048 = 1,
Ed25519 = 2,
}
impl Default for KeyGenType {
fn default() -> Self {
KeyGenType::Default
}
}
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
@@ -54,6 +93,7 @@ pub(crate) const DC_FROM_HANDSHAKE: i32 = 0x01;
pub const DC_GCL_ARCHIVED_ONLY: usize = 0x01;
pub const DC_GCL_NO_SPECIALS: usize = 0x02;
pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
pub const DC_GCL_FOR_FORWARDING: usize = 0x08;
pub const DC_GCM_ADDDAYMARKER: u32 = 0x01;
@@ -90,6 +130,8 @@ pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
FromSql,
ToSql,
IntoStaticStr,
Serialize,
Deserialize,
)]
#[repr(u32)]
pub enum Chattype {
@@ -168,13 +210,13 @@ pub const DC_LP_SMTP_SOCKET_SSL: usize = 0x20000;
pub const DC_LP_SMTP_SOCKET_PLAIN: usize = 0x40000;
/// if none of these flags are set, the default is chosen
pub const DC_LP_AUTH_FLAGS: i32 = (DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL);
pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
/// if none of these flags are set, the default is chosen
pub const DC_LP_IMAP_SOCKET_FLAGS: i32 =
(DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_SSL | DC_LP_IMAP_SOCKET_PLAIN);
DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_SSL | DC_LP_IMAP_SOCKET_PLAIN;
/// if none of these flags are set, the default is chosen
pub const DC_LP_SMTP_SOCKET_FLAGS: usize =
(DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN);
DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN;
// QR code scanning (view from Bob, the joiner)
pub const DC_VC_AUTH_REQUIRED: i32 = 2;
@@ -185,7 +227,23 @@ pub const DC_BOB_SUCCESS: i32 = 1;
// max. width/height of an avatar
pub const AVATAR_SIZE: u32 = 192;
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
// this value can be increased if the folder configuration is changed and must be redone on next program start
pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
)]
#[repr(i32)]
pub enum Viewtype {
Unknown = 0,
@@ -269,8 +327,6 @@ const DC_STR_SELFNOTINGRP: usize = 21; // deprecated;
const DC_STR_NOMESSAGES: usize = 1;
const DC_STR_SELF: usize = 2;
const DC_STR_DRAFT: usize = 3;
const DC_STR_MEMBER: usize = 4;
const DC_STR_CONTACT: usize = 6;
const DC_STR_VOICEMESSAGE: usize = 7;
const DC_STR_DEADDROP: usize = 8;
const DC_STR_IMAGE: usize = 9;
@@ -302,7 +358,6 @@ const DC_STR_ARCHIVEDCHATS: usize = 40;
const DC_STR_STARREDMSGS: usize = 41;
const DC_STR_AC_SETUP_MSG_SUBJECT: usize = 42;
const DC_STR_AC_SETUP_MSG_BODY: usize = 43;
const DC_STR_SELFTALK_SUBTITLE: usize = 50;
const DC_STR_CANNOT_LOGIN: usize = 60;
const DC_STR_SERVER_RESPONSE: usize = 61;
const DC_STR_MSGACTIONBYUSER: usize = 62;

View File

@@ -4,7 +4,6 @@ use std::path::PathBuf;
use deltachat_derive::*;
use itertools::Itertools;
use rusqlite;
use crate::aheader::EncryptPreference;
use crate::chat::ChatId;
@@ -12,10 +11,9 @@ use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::e2ee;
use crate::error::{Error, Result};
use crate::error::{bail, ensure, format_err, Result};
use crate::events::Event;
use crate::key::*;
use crate::key::{DcKey, Key, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::message::{MessageState, MsgId};
use crate::mimeparser::AvatarAction;
@@ -24,9 +22,6 @@ use crate::peerstate::*;
use crate::sql;
use crate::stock::StockMessage;
/// Contacts with at least this origin value are shown in the contact list.
const DC_ORIGIN_MIN_CONTACT_LIST: i32 = 0x100;
/// An object representing a single contact in memory.
///
/// The contact object is not updated.
@@ -60,7 +55,7 @@ pub struct Contact {
/// to access this field.
authname: String,
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr`` to access this field.
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr` to access this field.
addr: String,
/// Blocked state. Use dc_contact_is_blocked to access this field.
@@ -94,6 +89,7 @@ pub enum Origin {
UnhandledQrScan = 0x80,
/// Reply-To: of incoming message of known sender
/// Contacts with at least this origin value are shown in the contact list.
IncomingReplyTo = 0x100,
/// Cc: of incoming message of known sender
@@ -118,7 +114,7 @@ pub enum Origin {
Internal = 0x40000,
/// address is in our address book
AdressBook = 0x80000,
AddressBook = 0x80000,
/// set on Alice's side for contacts like Bob that have scanned the QR code offered by her. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling dc_contact_is_verified() !
SecurejoinInvited = 0x0100_0000,
@@ -146,7 +142,7 @@ impl Origin {
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Modifier {
pub(crate) enum Modifier {
None,
Modified,
Created,
@@ -274,7 +270,7 @@ impl Contact {
///
/// To validate an e-mail address independently of the contact database
/// use `dc_may_be_valid_addr()`.
pub fn lookup_id_by_addr(context: &Context, addr: impl AsRef<str>) -> u32 {
pub fn lookup_id_by_addr(context: &Context, addr: impl AsRef<str>, min_origin: Origin) -> u32 {
if addr.as_ref().is_empty() {
return 0;
}
@@ -287,22 +283,43 @@ impl Contact {
if addr_cmp(addr_normalized, addr_self) {
return DC_CONTACT_ID_SELF;
}
context.sql.query_get_value(
context,
"SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE AND id>?2 AND origin>=?3 AND blocked=0;",
params![
addr_normalized,
DC_CONTACT_ID_LAST_SPECIAL as i32,
DC_ORIGIN_MIN_CONTACT_LIST,
min_origin as u32,
],
).unwrap_or_default()
}
/// Lookup a contact and create it if it does not exist yet.
/// The contact is identified by the email-address, a name and an "origin" can be given.
///
/// The "origin" is where the address comes from -
/// from-header, cc-header, addressbook, qr, manual-edit etc.
/// In general, "better" origins overwrite the names of "worse" origins -
/// Eg. if we got a name in cc-header and later in from-header, the name will change -
/// this does not happen the other way round.
///
/// The "best" origin are manually created contacts -
/// names given manually can only be overwritten by further manual edits
/// (until they are set empty again or reset to the name seen in the From-header).
///
/// These manually edited names are _never_ used for sending on the wire -
/// this should avoid sending sth. as "Mama" or "Daddy" to some 3rd party.
/// Instead, for the wire, we use so called "authnames"
/// that can only be set and updated by a From-header.
///
/// The different names used in the function are:
/// - "name": name passed as function argument, belonging to the given origin
/// - "row_name": current name used in the database, typically set to "name"
/// - "row_authname": name as authorized from a contact, set only through a From-header
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
///
/// Returns the contact_id and a `Modifier` value indicating if a modification occured.
pub fn add_or_lookup(
pub(crate) fn add_or_lookup(
context: &Context,
name: impl AsRef<str>,
addr: impl AsRef<str>,
@@ -356,7 +373,9 @@ impl Contact {
if !name.as_ref().is_empty() {
if !row_name.is_empty() {
if origin >= row_origin && name.as_ref() != row_name {
if (origin >= row_origin || row_name == row_authname)
&& name.as_ref() != row_name
{
update_name = true;
}
} else {
@@ -365,6 +384,9 @@ impl Contact {
if origin == Origin::IncomingUnknownFrom && name.as_ref() != row_authname {
update_authname = true;
}
} else if origin == Origin::ManuallyCreated && !row_authname.is_empty() {
// no name given on manual edit, this will update the name to the authname
update_name = true;
}
Ok((row_id, row_name, row_addr, row_origin, row_authname))
@@ -375,16 +397,22 @@ impl Contact {
update_addr = true;
}
if update_name || update_authname || update_addr || origin > row_origin {
let new_name = if update_name {
if !name.as_ref().is_empty() {
name.as_ref()
} else {
&row_authname
}
} else {
&row_name
};
sql::execute(
context,
&context.sql,
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
params![
if update_name {
name.as_ref()
} else {
&row_name
},
new_name,
if update_addr { addr } else { &row_addr },
if origin > row_origin {
origin
@@ -402,11 +430,13 @@ impl Contact {
.ok();
if update_name {
// Update the contact name also if it is used as a group name.
// This is one of the few duplicated data, however, getting the chat list is easier this way.
sql::execute(
context,
&context.sql,
"UPDATE chats SET name=? WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
params![name.as_ref(), Chattype::Single, row_id]
params![new_name, Chattype::Single, row_id]
).ok();
}
sth_modified = Modifier::Modified;
@@ -462,9 +492,18 @@ impl Contact {
for (name, addr) in split_address_book(addr_book.as_ref()).into_iter() {
let name = normalize_name(name);
let (_, modified) = Contact::add_or_lookup(context, name, addr, Origin::AdressBook)?;
if modified != Modifier::None {
modify_cnt += 1
match Contact::add_or_lookup(context, name, addr, Origin::AddressBook) {
Err(err) => {
warn!(
context,
"Failed to add address {} from address book: {}", addr, err
);
}
Ok((_, modified)) => {
if modified != Modifier::None {
modify_cnt += 1
}
}
}
}
if modify_cnt > 0 {
@@ -607,8 +646,6 @@ impl Contact {
let peerstate = Peerstate::from_addr(context, &context.sql, &contact.addr);
let loginparam = LoginParam::from_database(context, "configured_");
let mut self_key = Key::from_self_public(context, &loginparam.addr, &context.sql);
if peerstate.is_some()
&& peerstate
.as_ref()
@@ -623,16 +660,11 @@ impl Contact {
StockMessage::E2eAvailable
});
ret += &p;
if self_key.is_none() {
e2ee::ensure_secret_key_exists(context)?;
self_key = Key::from_self_public(context, &loginparam.addr, &context.sql);
}
let self_key = Key::from(SignedPublicKey::load_self(context)?);
let p = context.stock_str(StockMessage::FingerPrints);
ret += &format!(" {}:", p);
let fingerprint_self = self_key
.map(|k| k.formatted_fingerprint())
.unwrap_or_default();
let fingerprint_self = self_key.formatted_fingerprint();
let fingerprint_other_verified = peerstate
.peek_key(PeerstateVerifiedStatus::BidirectVerified)
.map(|k| k.formatted_fingerprint())
@@ -984,7 +1016,7 @@ fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
}
}
pub fn set_profile_image(
pub(crate) fn set_profile_image(
context: &Context,
contact_id: u32,
profile_image: &AvatarAction,
@@ -1001,7 +1033,6 @@ pub fn set_profile_image(
contact.param.remove(Param::ProfileImage);
true
}
AvatarAction::None => false,
};
if changed {
contact.update_param(context)?;
@@ -1024,7 +1055,7 @@ pub fn normalize_name(full_name: impl AsRef<str>) -> String {
}
let len = full_name.len();
if len > 0 {
if len > 1 {
let firstchar = full_name.as_bytes()[0];
let lastchar = full_name.as_bytes()[len - 1];
if firstchar == b'\'' && lastchar == b'\''
@@ -1077,10 +1108,9 @@ fn cat_fingerprint(
impl Context {
/// determine whether the specified addr maps to the/a self addr
pub fn is_self_addr(&self, addr: &str) -> Result<bool> {
let self_addr = match self.get_config(Config::ConfiguredAddr) {
Some(s) => s,
None => return Err(Error::NotConfigured),
};
let self_addr = self
.get_config(Config::ConfiguredAddr)
.ok_or_else(|| format_err!("Not configured"))?;
Ok(addr_cmp(self_addr, addr))
}
@@ -1133,6 +1163,10 @@ mod tests {
fn test_normalize_name() {
assert_eq!(&normalize_name("Doe, John"), "John Doe");
assert_eq!(&normalize_name(" hello world "), "hello world");
assert_eq!(&normalize_name("<"), "<");
assert_eq!(&normalize_name(">"), ">");
assert_eq!(&normalize_name("'"), "'");
assert_eq!(&normalize_name("\""), "\"");
}
#[test]
@@ -1195,6 +1229,7 @@ mod tests {
let book = concat!(
" Name one \n one@eins.org \n",
"Name two\ntwo@deux.net\n",
"Invalid\n+1234567890\n", // invalid, should be ignored
"\nthree@drei.sam\n",
"Name two\ntwo@deux.net\n" // should not be added again
);
@@ -1281,6 +1316,7 @@ mod tests {
fn test_remote_authnames() {
let t = dummy_context();
// incoming mail `From: bob1 <bob@example.org>` - this should init authname and name
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"bob1",
@@ -1295,6 +1331,7 @@ mod tests {
assert_eq!(contact.get_name(), "bob1");
assert_eq!(contact.get_display_name(), "bob1");
// incoming mail `From: bob2 <bob@example.org>` - this should update authname and name
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"bob2",
@@ -1309,16 +1346,15 @@ mod tests {
assert_eq!(contact.get_name(), "bob2");
assert_eq!(contact.get_display_name(), "bob2");
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t.ctx, "bob3", "bob@example.org", Origin::ManuallyCreated)
.unwrap();
// manually edit name to "bob3" - authname should be still be "bob2" a given in `From:` above
let contact_id = Contact::create(&t.ctx, "bob3", "bob@example.org").unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "bob2");
assert_eq!(contact.get_name(), "bob3");
assert_eq!(contact.get_display_name(), "bob3");
// incoming mail `From: bob4 <bob@example.org>` - this should update authname, manually given name is still "bob3"
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"bob4",
@@ -1334,6 +1370,81 @@ mod tests {
assert_eq!(contact.get_display_name(), "bob3");
}
#[test]
fn test_remote_authnames_create_empty() {
let t = dummy_context();
// manually create "claire@example.org" without a given name
let contact_id = Contact::create(&t.ctx, "", "claire@example.org").unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "claire@example.org");
// incoming mail `From: claire1 <claire@example.org>` - this should update authname and name
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"claire1",
"claire@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "claire1");
assert_eq!(contact.get_name(), "claire1");
assert_eq!(contact.get_display_name(), "claire1");
// incoming mail `From: claire2 <claire@example.org>` - this should update authname and name
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"claire2",
"claire@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "claire2");
assert_eq!(contact.get_name(), "claire2");
assert_eq!(contact.get_display_name(), "claire2");
}
#[test]
fn test_remote_authnames_edit_empty() {
let t = dummy_context();
// manually create "dave@example.org"
let contact_id = Contact::create(&t.ctx, "dave1", "dave@example.org").unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "dave1");
assert_eq!(contact.get_display_name(), "dave1");
// incoming mail `From: dave2 <dave@example.org>` - this should update authname
Contact::add_or_lookup(
&t.ctx,
"dave2",
"dave@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "dave2");
assert_eq!(contact.get_name(), "dave1");
assert_eq!(contact.get_display_name(), "dave1");
// manually clear the name
Contact::create(&t.ctx, "", "dave@example.org").unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "dave2");
assert_eq!(contact.get_name(), "dave2");
assert_eq!(contact.get_display_name(), "dave2");
}
#[test]
fn test_addr_cmp() {
assert!(addr_cmp("AA@AA.ORG", "aa@aa.ORG"));

View File

@@ -1,6 +1,6 @@
//! Context module
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Condvar, Mutex, RwLock};
@@ -9,18 +9,20 @@ use crate::chat::*;
use crate::config::Config;
use crate::constants::*;
use crate::contact::*;
use crate::dc_tools::duration_to_str;
use crate::error::*;
use crate::events::Event;
use crate::imap::*;
use crate::job::*;
use crate::job_thread::JobThread;
use crate::key::*;
use crate::key::{DcKey, Key, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::lot::Lot;
use crate::message::{self, Message, MessengerMessage, MsgId};
use crate::param::Params;
use crate::smtp::Smtp;
use crate::sql::Sql;
use std::time::SystemTime;
/// Callback function type for [Context]
///
@@ -51,12 +53,13 @@ pub struct Context {
cb: Box<ContextCallback>,
pub os_name: Option<String>,
pub cmdline_sel_chat_id: Arc<RwLock<ChatId>>,
pub bob: Arc<RwLock<BobStatus>>,
pub(crate) bob: Arc<RwLock<BobStatus>>,
pub last_smeared_timestamp: RwLock<i64>,
pub running_state: Arc<RwLock<RunningState>>,
/// Mutex to avoid generating the key for the user more than once.
pub generating_key_mutex: Mutex<()>,
pub translated_stockstrings: RwLock<HashMap<usize, String>>,
creation_time: SystemTime,
}
#[derive(Debug, PartialEq, Eq)]
@@ -71,8 +74,8 @@ pub struct RunningState {
/// actual keys and their values which will be present are not
/// guaranteed. Calling [Context::get_info] also includes information
/// about the context on top of the information here.
pub fn get_info() -> HashMap<&'static str, String> {
let mut res = HashMap::new();
pub fn get_info() -> BTreeMap<&'static str, String> {
let mut res = BTreeMap::new();
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("sqlite_version", rusqlite::version().to_string());
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
@@ -83,6 +86,8 @@ pub fn get_info() -> HashMap<&'static str, String> {
impl Context {
/// Creates new context.
pub fn new(cb: Box<ContextCallback>, os_name: String, dbfile: PathBuf) -> Result<Context> {
pretty_env_logger::try_init_timed().ok();
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
@@ -136,6 +141,7 @@ impl Context {
perform_inbox_jobs_needed: Arc::new(RwLock::new(false)),
generating_key_mutex: Mutex::new(()),
translated_stockstrings: RwLock::new(HashMap::new()),
creation_time: std::time::SystemTime::now(),
};
ensure!(
@@ -220,7 +226,7 @@ impl Context {
* UI chat/message related API
******************************************************************************/
pub fn get_info(&self) -> HashMap<&'static str, String> {
pub fn get_info(&self) -> BTreeMap<&'static str, String> {
let unset = "0";
let l = LoginParam::from_database(self, "");
let l2 = LoginParam::from_database(self, "configured_");
@@ -234,7 +240,10 @@ impl Context {
.sql
.get_raw_config_int(self, "dbversion")
.unwrap_or_default();
let journal_mode = self
.sql
.query_get_value(self, "PRAGMA journal_mode;", rusqlite::NO_PARAMS)
.unwrap_or_else(|| "unknown".to_string());
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled);
let mdns_enabled = self.get_config_int(Config::MdnsEnabled);
let bcc_self = self.get_config_int(Config::BccSelf);
@@ -249,10 +258,9 @@ impl Context {
rusqlite::NO_PARAMS,
);
let fingerprint_str = if let Some(key) = Key::from_self_public(self, &l2.addr, &self.sql) {
key.fingerprint()
} else {
"<Not yet calculated>".into()
let fingerprint_str = match SignedPublicKey::load_self(self) {
Ok(key) => Key::from(key).fingerprint(),
Err(err) => format!("<key failure: {}>", err),
};
let inbox_watch = self.get_config_int(Config::InboxWatch);
@@ -280,6 +288,7 @@ impl Context {
res.insert("number_of_contacts", contacts.to_string());
res.insert("database_dir", self.get_dbfile().display().to_string());
res.insert("database_version", dbversion.to_string());
res.insert("journal_mode", journal_mode);
res.insert("blobdir", self.get_blobdir().display().to_string());
res.insert("display_name", displayname.unwrap_or_else(|| unset.into()));
res.insert(
@@ -310,6 +319,9 @@ impl Context {
);
res.insert("fingerprint", fingerprint_str);
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
res
}
@@ -476,14 +488,14 @@ impl Default for RunningState {
}
#[derive(Debug, Default)]
pub struct BobStatus {
pub(crate) struct BobStatus {
pub expects: i32,
pub status: i32,
pub qr_scan: Option<Lot>,
}
#[derive(Debug, PartialEq)]
pub enum PerformJobsNeeded {
pub(crate) enum PerformJobsNeeded {
Not,
AtOnce,
AvoidDos,
@@ -500,7 +512,7 @@ pub struct SmtpState {
pub idle: bool,
pub suspended: bool,
pub doing_jobs: bool,
pub perform_jobs_needed: PerformJobsNeeded,
pub(crate) perform_jobs_needed: PerformJobsNeeded,
pub probe_network: bool,
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,24 +5,24 @@ use core::cmp::{max, min};
use std::borrow::Cow;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::SystemTime;
use std::time::{Duration, SystemTime};
use std::{fmt, fs};
use chrono::{Local, TimeZone};
use rand::{thread_rng, Rng};
use crate::context::Context;
use crate::error::Error;
use crate::error::{bail, Error};
use crate::events::Event;
pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
0 != v && 0 == v & (v - 1)
}
/// Shortens a string to a specified length and adds "..." or "[...]" to the end of
/// the shortened string.
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize, do_unwrap: bool) -> Cow<str> {
let ellipse = if do_unwrap { "..." } else { "[...]" };
/// Shortens a string to a specified length and adds "[...]" to the
/// end of the shortened string.
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
let ellipse = "[...]";
let count = buf.chars().count();
if approx_chars > 0 && count > approx_chars + ellipse.len() {
@@ -75,6 +75,14 @@ pub fn dc_timestamp_to_str(wanted: i64) -> String {
ts.format("%Y.%m.%d %H:%M:%S").to_string()
}
pub fn duration_to_str(duration: Duration) -> String {
let secs = duration.as_secs();
let h = secs / 3600;
let m = (secs % 3600) / 60;
let s = (secs % 3600) % 60;
format!("{}h {}m {}s", h, m, s)
}
pub(crate) fn dc_gm2local_offset() -> i64 {
/* returns the offset that must be _added_ to an UTC/GMT-time to create the localtime.
the function may return negative values. */
@@ -222,43 +230,6 @@ pub(crate) fn dc_extract_grpid_from_rfc724_mid(mid: &str) -> Option<&str> {
None
}
// Function returns a sanitized basename that does not contain
// win/linux path separators and also not any non-ascii chars
fn get_safe_basename(filename: &str) -> String {
// return the (potentially mangled) basename of the input filename
// this might be a path that comes in from another operating system
let mut index: usize = 0;
if let Some(unix_index) = filename.rfind('/') {
index = unix_index + 1;
}
if let Some(win_index) = filename.rfind('\\') {
index = max(index, win_index + 1);
}
if index >= filename.len() {
"nobasename".to_string()
} else {
// we don't allow any non-ascii to be super-safe
filename[index..].replace(|c: char| !c.is_ascii() || c == ':', "-")
}
}
pub fn dc_derive_safe_stem_ext(filename: &str) -> (String, String) {
let basename = get_safe_basename(&filename);
let (mut stem, mut ext) = if let Some(index) = basename.rfind('.') {
(
basename[0..index].to_string(),
basename[index..].to_string(),
)
} else {
(basename, "".to_string())
};
// limit length of stem and ext
stem.truncate(32);
ext.truncate(32);
(stem, ext)
}
// the returned suffix is lower-case
pub fn dc_get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
Path::new(path_filename.as_ref())
@@ -489,6 +460,23 @@ pub(crate) fn time() -> i64 {
.as_secs() as i64
}
/// An invalid email address was encountered
#[derive(Debug, thiserror::Error)]
#[error("Invalid email address: {message} ({addr})")]
pub struct InvalidEmailError {
message: String,
addr: String,
}
impl InvalidEmailError {
fn new(msg: impl Into<String>, addr: impl Into<String>) -> InvalidEmailError {
InvalidEmailError {
message: msg.into(),
addr: addr.into(),
}
}
}
/// Very simple email address wrapper.
///
/// Represents an email address, right now just the `name@domain` portion.
@@ -505,14 +493,14 @@ pub(crate) fn time() -> i64 {
/// assert_eq!(&email.domain, "example.com");
/// assert_eq!(email.to_string(), "someone@example.com");
/// ```
#[derive(Debug, PartialEq, Clone)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct EmailAddress {
pub local: String,
pub domain: String,
}
impl EmailAddress {
pub fn new(input: &str) -> Result<Self, Error> {
pub fn new(input: &str) -> Result<Self, InvalidEmailError> {
input.parse::<EmailAddress>()
}
}
@@ -524,31 +512,55 @@ impl fmt::Display for EmailAddress {
}
impl FromStr for EmailAddress {
type Err = Error;
type Err = InvalidEmailError;
/// Performs a dead-simple parse of an email address.
fn from_str(input: &str) -> Result<EmailAddress, Error> {
ensure!(!input.is_empty(), "empty string is not valid");
fn from_str(input: &str) -> Result<EmailAddress, InvalidEmailError> {
if input.is_empty() {
return Err(InvalidEmailError::new("empty string is not valid", input));
}
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
ensure!(parts.len() > 1, "missing '@' character");
let local = parts[1];
let domain = parts[0];
let err = |msg: &str| {
Err(InvalidEmailError {
message: msg.to_string(),
addr: input.to_string(),
})
};
match &parts[..] {
[domain, local] => {
if local.is_empty() {
return err("empty string is not valid for local part");
}
if domain.len() <= 3 {
return err("domain is too short");
}
let dot = domain.find('.');
match dot {
None => {
return err("invalid domain");
}
Some(dot_idx) => {
if dot_idx >= domain.len() - 2 {
return err("invalid domain");
}
}
}
Ok(EmailAddress {
local: (*local).to_string(),
domain: (*domain).to_string(),
})
}
_ => err("missing '@' character"),
}
}
}
ensure!(
!local.is_empty(),
"empty string is not valid for local part"
);
ensure!(domain.len() > 3, "domain is too short");
let dot = domain.find('.');
ensure!(dot.is_some(), "invalid domain");
ensure!(dot.unwrap() < domain.len() - 2, "invalid domain");
Ok(EmailAddress {
local: local.to_string(),
domain: domain.to_string(),
})
impl rusqlite::types::ToSql for EmailAddress {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Text(self.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
@@ -575,54 +587,42 @@ mod tests {
#[test]
fn test_dc_truncate_1() {
let s = "this is a little test string";
assert_eq!(dc_truncate(s, 16, false), "this is a [...]");
assert_eq!(dc_truncate(s, 16, true), "this is a ...");
assert_eq!(dc_truncate(s, 16), "this is a [...]");
}
#[test]
fn test_dc_truncate_2() {
assert_eq!(dc_truncate("1234", 2, false), "1234");
assert_eq!(dc_truncate("1234", 2, true), "1234");
assert_eq!(dc_truncate("1234", 2), "1234");
}
#[test]
fn test_dc_truncate_3() {
assert_eq!(dc_truncate("1234567", 1, false), "1[...]");
assert_eq!(dc_truncate("1234567", 1, true), "1...");
assert_eq!(dc_truncate("1234567", 1), "1[...]");
}
#[test]
fn test_dc_truncate_4() {
assert_eq!(dc_truncate("123456", 4, false), "123456");
assert_eq!(dc_truncate("123456", 4, true), "123456");
assert_eq!(dc_truncate("123456", 4), "123456");
}
#[test]
fn test_dc_truncate_edge() {
assert_eq!(dc_truncate("", 4, false), "");
assert_eq!(dc_truncate("", 4, true), "");
assert_eq!(dc_truncate("", 4), "");
assert_eq!(dc_truncate("\n hello \n world", 4, false), "\n [...]");
assert_eq!(dc_truncate("\n hello \n world", 4, true), "\n ...");
assert_eq!(dc_truncate("\n hello \n world", 4), "\n [...]");
assert_eq!(dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1), "𐠈[...]");
assert_eq!(
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1, false),
"𐠈[...]"
);
assert_eq!(
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0, false),
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0),
"𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ"
);
// 9 characters, so no truncation
assert_eq!(
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6, false),
"𑒀ὐ¢🜀\u{1e01b}A a🟠",
);
assert_eq!(dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",);
// 12 characters, truncation
assert_eq!(
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6, false),
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6),
"𑒀ὐ¢🜀\u{1e01b}A[...]",
);
}
@@ -707,29 +707,29 @@ mod tests {
#[test]
fn test_emailaddress_parse() {
assert_eq!(EmailAddress::new("").is_ok(), false);
assert_eq!("".parse::<EmailAddress>().is_ok(), false);
assert_eq!(
EmailAddress::new("user@domain.tld").unwrap(),
"user@domain.tld".parse::<EmailAddress>().unwrap(),
EmailAddress {
local: "user".into(),
domain: "domain.tld".into(),
}
);
assert_eq!(EmailAddress::new("uuu").is_ok(), false);
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
assert_eq!(EmailAddress::new("tt.dd@uu").is_ok(), false);
assert_eq!(EmailAddress::new("u@d").is_ok(), false);
assert_eq!(EmailAddress::new("u@d.").is_ok(), false);
assert_eq!(EmailAddress::new("u@d.t").is_ok(), false);
assert_eq!("uuu".parse::<EmailAddress>().is_ok(), false);
assert_eq!("dd.tt".parse::<EmailAddress>().is_ok(), false);
assert_eq!("tt.dd@uu".parse::<EmailAddress>().is_ok(), false);
assert_eq!("u@d".parse::<EmailAddress>().is_ok(), false);
assert_eq!("u@d.".parse::<EmailAddress>().is_ok(), false);
assert_eq!("u@d.t".parse::<EmailAddress>().is_ok(), false);
assert_eq!(
EmailAddress::new("u@d.tt").unwrap(),
"u@d.tt".parse::<EmailAddress>().unwrap(),
EmailAddress {
local: "u".into(),
domain: "d.tt".into(),
}
);
assert_eq!(EmailAddress::new("u@.tt").is_ok(), false);
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
assert_eq!("u@tt".parse::<EmailAddress>().is_ok(), false);
assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
}
use proptest::prelude::*;
@@ -738,11 +738,10 @@ mod tests {
#[test]
fn test_dc_truncate(
buf: String,
approx_chars in 0..10000usize,
do_unwrap: bool,
approx_chars in 0..10000usize
) {
let res = dc_truncate(&buf, approx_chars, do_unwrap);
let el_len = if do_unwrap { 3 } else { 5 };
let res = dc_truncate(&buf, approx_chars);
let el_len = 5;
let l = res.chars().count();
if approx_chars > 0 {
assert!(
@@ -756,28 +755,11 @@ mod tests {
if approx_chars > 0 && buf.chars().count() > approx_chars + el_len {
let l = res.len();
if do_unwrap {
assert_eq!(&res[l-3..l], "...", "missing ellipsis in {}", &res);
} else {
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
}
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
}
}
}
#[test]
fn test_file_get_safe_basename() {
assert_eq!(get_safe_basename("12312/hello"), "hello");
assert_eq!(get_safe_basename("12312\\hello"), "hello");
assert_eq!(get_safe_basename("//12312\\hello"), "hello");
assert_eq!(get_safe_basename("//123:12\\hello"), "hello");
assert_eq!(get_safe_basename("//123:12/\\\\hello"), "hello");
assert_eq!(get_safe_basename("//123:12//hello"), "hello");
assert_eq!(get_safe_basename("//123:12//"), "nobasename");
assert_eq!(get_safe_basename("//123:12/"), "nobasename");
assert!(get_safe_basename("123\x012.hello").ends_with(".hello"));
}
#[test]
fn test_file_handling() {
let t = dummy_context();
@@ -880,4 +862,37 @@ mod tests {
let next = dc_smeared_time(&t.ctx);
assert!((start + count - 1) < next);
}
#[test]
fn test_duration_to_str() {
assert_eq!(duration_to_str(Duration::from_secs(0)), "0h 0m 0s");
assert_eq!(duration_to_str(Duration::from_secs(59)), "0h 0m 59s");
assert_eq!(duration_to_str(Duration::from_secs(60)), "0h 1m 0s");
assert_eq!(duration_to_str(Duration::from_secs(61)), "0h 1m 1s");
assert_eq!(duration_to_str(Duration::from_secs(59 * 60)), "0h 59m 0s");
assert_eq!(
duration_to_str(Duration::from_secs(59 * 60 + 59)),
"0h 59m 59s"
);
assert_eq!(
duration_to_str(Duration::from_secs(59 * 60 + 60)),
"1h 0m 0s"
);
assert_eq!(
duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 59)),
"2h 59m 59s"
);
assert_eq!(
duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 60)),
"3h 0m 0s"
);
assert_eq!(
duration_to_str(Duration::from_secs(3 * 60 * 60 + 59)),
"3h 0m 59s"
);
assert_eq!(
duration_to_str(Duration::from_secs(3 * 60 * 60 + 60)),
"3h 1m 0s"
);
}
}

View File

@@ -3,7 +3,6 @@
//! A module to remove HTML tags from the email text
use lazy_static::lazy_static;
use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
lazy_static! {
@@ -35,6 +34,7 @@ pub fn dehtml(buf: &str) -> String {
};
let mut reader = quick_xml::Reader::from_str(buf);
reader.check_end_names(false);
let mut buf = Vec::new();
@@ -225,4 +225,23 @@ mod tests {
"<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}"
);
}
#[test]
fn test_unclosed_tags() {
let input = r##"
<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'
'http://www.w3.org/TR/html4/loose.dtd'>
<html>
<head>
<title>Hi</title>
<meta http-equiv='Content-Type' content='text/html; charset=iso-8859-1'>
</head>
<body>
lots of text
</body>
</html>
"##;
let txt = dehtml(input);
assert_eq!(txt.trim(), "lots of text");
}
}

View File

@@ -2,14 +2,16 @@
use std::collections::HashSet;
use mailparse::{MailHeaderMap, ParsedMail};
use mailparse::ParsedMail;
use num_traits::FromPrimitive;
use crate::aheader::*;
use crate::config::Config;
use crate::context::Context;
use crate::error::*;
use crate::key::*;
use crate::headerdef::HeaderDef;
use crate::headerdef::HeaderDefMap;
use crate::key::{DcKey, Key, SignedPublicKey, SignedSecretKey};
use crate::keyring::*;
use crate::peerstate::*;
use crate::pgp;
@@ -19,7 +21,7 @@ use crate::securejoin::handle_degrade_event;
pub struct EncryptHelper {
pub prefer_encrypt: EncryptPreference,
pub addr: String,
pub public_key: Key,
pub public_key: SignedPublicKey,
}
impl EncryptHelper {
@@ -34,7 +36,7 @@ impl EncryptHelper {
Some(addr) => addr,
};
let public_key = load_or_generate_self_public_key(context, &addr)?;
let public_key = SignedPublicKey::load_self(context)?;
Ok(EncryptHelper {
prefer_encrypt,
@@ -102,10 +104,9 @@ impl EncryptHelper {
})?;
keyring.add_ref(key);
}
keyring.add_ref(&self.public_key);
let sign_key = Key::from_self_private(context, self.addr.clone(), &context.sql)
.ok_or_else(|| format_err!("missing own private key"))?;
let public_key = Key::from(self.public_key.clone());
keyring.add_ref(&public_key);
let sign_key = Key::from(SignedSecretKey::load_self(context)?);
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
@@ -122,8 +123,8 @@ pub fn try_decrypt(
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
let from = mail
.headers
.get_first_value("From")?
.and_then(|from_addr| mailparse::addrparse(&from_addr).ok())
.get_header(HeaderDef::From_)
.and_then(|from_addr| mailparse::addrparse_header(&from_addr).ok())
.and_then(|from| from.extract_single_info())
.map(|from| from.addr)
.unwrap_or_default();
@@ -185,52 +186,6 @@ pub fn try_decrypt(
Ok((out_mail, signatures))
}
/// Load public key from database or generate a new one.
///
/// This will load a public key from the database, generating and
/// storing a new one when one doesn't exist yet. Care is taken to
/// only generate one key per context even when multiple threads call
/// this function concurrently.
fn load_or_generate_self_public_key(context: &Context, self_addr: impl AsRef<str>) -> Result<Key> {
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
return Ok(key);
}
let _guard = context.generating_key_mutex.lock().unwrap();
// Check again in case the key was generated while we were waiting for the lock.
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
return Ok(key);
}
let start = std::time::Instant::now();
info!(
context,
"Generating keypair with {} bits, e={} ...", 2048, 65537,
);
match pgp::create_keypair(&self_addr) {
Some((public_key, private_key)) => {
if dc_key_save_self_keypair(
context,
&public_key,
&private_key,
&self_addr,
true,
&context.sql,
) {
info!(
context,
"Keypair generated in {:.3}s.",
start.elapsed().as_secs()
);
Ok(public_key)
} else {
Err(format_err!("Failed to save keypair"))
}
}
None => Err(format_err!("Failed to generate keypair")),
}
}
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail<'b>> {
ensure!(
@@ -352,6 +307,7 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool {
///
/// If this succeeds you are also guaranteed that the
/// [Config::ConfiguredAddr] is configured, this address is returned.
// TODO, remove this once deltachat::key::Key no longer exists.
pub fn ensure_secret_key_exists(context: &Context) -> Result<String> {
let self_addr = context.get_config(Config::ConfiguredAddr).ok_or_else(|| {
format_err!(concat!(
@@ -359,7 +315,7 @@ pub fn ensure_secret_key_exists(context: &Context) -> Result<String> {
"cannot ensure secret key if not configured."
))
})?;
load_or_generate_self_public_key(context, &self_addr)?;
SignedPublicKey::load_self(context)?;
Ok(self_addr)
}
@@ -410,49 +366,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
);
}
mod load_or_generate_self_public_key {
use super::*;
#[test]
fn test_existing() {
let t = dummy_context();
let addr = configure_alice_keypair(&t.ctx);
let key = load_or_generate_self_public_key(&t.ctx, addr);
assert!(key.is_ok());
}
#[test]
#[ignore] // generating keys is expensive
fn test_generate() {
let t = dummy_context();
let addr = "alice@example.org";
let key0 = load_or_generate_self_public_key(&t.ctx, addr);
assert!(key0.is_ok());
let key1 = load_or_generate_self_public_key(&t.ctx, addr);
assert!(key1.is_ok());
assert_eq!(key0.unwrap(), key1.unwrap());
}
#[test]
#[ignore]
fn test_generate_concurrent() {
use std::sync::Arc;
use std::thread;
let t = dummy_context();
let ctx = Arc::new(t.ctx);
let ctx0 = Arc::clone(&ctx);
let thr0 =
thread::spawn(move || load_or_generate_self_public_key(&ctx0, "alice@example.org"));
let ctx1 = Arc::clone(&ctx);
let thr1 =
thread::spawn(move || load_or_generate_self_public_key(&ctx1, "alice@example.org"));
let res0 = thr0.join().unwrap();
let res1 = thr1.join().unwrap();
assert_eq!(res0.unwrap(), res1.unwrap());
}
}
#[test]
fn test_has_decrypted_pgp_armor() {
let data = b" -----BEGIN PGP MESSAGE-----";

View File

@@ -1,174 +1,13 @@
//! # Error handling
use lettre_email::mime;
pub use anyhow::{bail, ensure, format_err, Error, Result};
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "{:?}", _0)]
Failure(failure::Error),
#[fail(display = "SQL error: {:?}", _0)]
SqlError(#[cause] crate::sql::Error),
#[fail(display = "{:?}", _0)]
Io(std::io::Error),
#[fail(display = "{:?}", _0)]
Message(String),
#[fail(display = "{:?}", _0)]
Image(image_meta::ImageError),
#[fail(display = "{:?}", _0)]
Utf8(std::str::Utf8Error),
#[fail(display = "PGP: {:?}", _0)]
Pgp(pgp::errors::Error),
#[fail(display = "Base64Decode: {:?}", _0)]
Base64Decode(base64::DecodeError),
#[fail(display = "{:?}", _0)]
FromUtf8(std::string::FromUtf8Error),
#[fail(display = "{}", _0)]
BlobError(#[cause] crate::blob::BlobError),
#[fail(display = "Invalid Message ID.")]
InvalidMsgId,
#[fail(display = "Watch folder not found {:?}", _0)]
WatchFolderNotFound(String),
#[fail(display = "Invalid Email: {:?}", _0)]
MailParseError(#[cause] mailparse::MailParseError),
#[fail(display = "Building invalid Email: {:?}", _0)]
LettreError(#[cause] lettre_email::error::Error),
#[fail(display = "SMTP error: {:?}", _0)]
SmtpError(#[cause] async_smtp::error::Error),
#[fail(display = "FromStr error: {:?}", _0)]
FromStr(#[cause] mime::FromStrError),
#[fail(display = "Not Configured")]
NotConfigured,
}
pub type Result<T> = std::result::Result<T, Error>;
impl From<crate::sql::Error> for Error {
fn from(err: crate::sql::Error) -> Error {
Error::SqlError(err)
}
}
impl From<base64::DecodeError> for Error {
fn from(err: base64::DecodeError) -> Error {
Error::Base64Decode(err)
}
}
impl From<failure::Error> for Error {
fn from(err: failure::Error) -> Error {
Error::Failure(err)
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Error {
Error::Io(err)
}
}
impl From<std::str::Utf8Error> for Error {
fn from(err: std::str::Utf8Error) -> Error {
Error::Utf8(err)
}
}
impl From<image_meta::ImageError> for Error {
fn from(err: image_meta::ImageError) -> Error {
Error::Image(err)
}
}
impl From<pgp::errors::Error> for Error {
fn from(err: pgp::errors::Error) -> Error {
Error::Pgp(err)
}
}
impl From<std::string::FromUtf8Error> for Error {
fn from(err: std::string::FromUtf8Error) -> Error {
Error::FromUtf8(err)
}
}
impl From<crate::blob::BlobError> for Error {
fn from(err: crate::blob::BlobError) -> Error {
Error::BlobError(err)
}
}
impl From<crate::message::InvalidMsgId> for Error {
fn from(_err: crate::message::InvalidMsgId) -> Error {
Error::InvalidMsgId
}
}
impl From<mailparse::MailParseError> for Error {
fn from(err: mailparse::MailParseError) -> Error {
Error::MailParseError(err)
}
}
impl From<lettre_email::error::Error> for Error {
fn from(err: lettre_email::error::Error) -> Error {
Error::LettreError(err)
}
}
impl From<mime::FromStrError> for Error {
fn from(err: mime::FromStrError) -> Error {
Error::FromStr(err)
}
}
#[macro_export]
macro_rules! bail {
($e:expr) => {
return Err($crate::error::Error::Message($e.to_string()));
};
($fmt:expr, $($arg:tt)+) => {
return Err($crate::error::Error::Message(format!($fmt, $($arg)+)));
};
}
#[macro_export]
macro_rules! format_err {
($e:expr) => {
$crate::error::Error::Message($e.to_string());
};
($fmt:expr, $($arg:tt)+) => {
$crate::error::Error::Message(format!($fmt, $($arg)+));
};
}
#[macro_export(local_inner_macros)]
macro_rules! ensure {
($cond:expr, $e:expr) => {
if !($cond) {
bail!($e);
}
};
($cond:expr, $fmt:expr, $($arg:tt)+) => {
if !($cond) {
bail!($fmt, $($arg)+);
}
};
}
// #[fail(display = "Invalid Message ID.")]
// InvalidMsgId,
// #[fail(display = "Watch folder not found {:?}", _0)]
// WatchFolderNotFound(String),
// #[fail(display = "Not Configured")]
// NotConfigured,
#[macro_export]
macro_rules! ensure_eq {

View File

@@ -201,10 +201,4 @@ pub enum Event {
/// (Bob has verified alice and waits until Alice does the same for him)
#[strum(props(id = "2061"))]
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
/// This event is sent out to the inviter when a joiner successfully joined a group.
/// @param data1 (int) chat_id
/// @param data2 (int) contact_id
#[strum(props(id = "2062"))]
SecurejoinMemberAdded { chat_id: ChatId, contact_id: u32 },
}

View File

@@ -1,4 +1,7 @@
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames)]
use crate::strum::AsStaticRef;
use mailparse::{MailHeader, MailHeaderMap};
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
#[strum(serialize_all = "kebab_case")]
#[allow(dead_code)]
pub enum HeaderDef {
@@ -23,7 +26,6 @@ pub enum HeaderDef {
ChatGroupName,
ChatGroupNameChanged,
ChatVerified,
ChatGroupImage, // deprecated
ChatGroupAvatar,
ChatUserAvatar,
ChatVoiceMessage,
@@ -32,6 +34,7 @@ pub enum HeaderDef {
ChatContent,
ChatDuration,
ChatDispositionNotificationTo,
Autocrypt,
AutocryptSetupMessage,
SecureJoin,
SecureJoinGroup,
@@ -43,8 +46,22 @@ pub enum HeaderDef {
impl HeaderDef {
/// Returns the corresponding Event id.
pub fn get_headername(&self) -> String {
self.to_string()
pub fn get_headername(&self) -> &'static str {
self.as_static()
}
}
pub trait HeaderDefMap {
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String>;
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader>;
}
impl HeaderDefMap for [MailHeader<'_>] {
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String> {
self.get_first_value(headerdef.get_headername())
}
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader> {
self.get_first_header(headerdef.get_headername())
}
}
@@ -55,8 +72,24 @@ mod tests {
#[test]
/// Test that kebab_case serialization works as expected
fn kebab_test() {
assert_eq!(HeaderDef::From_.to_string(), "from");
assert_eq!(HeaderDef::From_.get_headername(), "from");
assert_eq!(HeaderDef::_TestHeader.to_string(), "test-header");
assert_eq!(HeaderDef::_TestHeader.get_headername(), "test-header");
}
#[test]
/// Test that headers are parsed case-insensitively
fn test_get_header_value_case() {
let (headers, _) =
mailparse::parse_headers(b"fRoM: Bob\naUtoCryPt-SeTup-MessAge: v99").unwrap();
assert_eq!(
headers.get_header_value(HeaderDef::AutocryptSetupMessage),
Some("v99".to_string())
);
assert_eq!(
headers.get_header_value(HeaderDef::From_),
Some("Bob".to_string())
);
assert_eq!(headers.get_header_value(HeaderDef::Autocrypt), None);
}
}

104
src/imap/client.rs Normal file
View File

@@ -0,0 +1,104 @@
use async_imap::{
error::{Error as ImapError, Result as ImapResult},
Client as ImapClient,
};
use async_native_tls::TlsStream;
use async_std::net::{self, TcpStream};
use super::session::Session;
use crate::login_param::{dc_build_tls, CertificateChecks};
#[derive(Debug)]
pub(crate) enum Client {
Secure(ImapClient<TlsStream<TcpStream>>),
Insecure(ImapClient<TcpStream>),
}
impl Client {
pub async fn connect_secure<A: net::ToSocketAddrs, S: AsRef<str>>(
addr: A,
domain: S,
certificate_checks: CertificateChecks,
) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let tls = dc_build_tls(certificate_checks);
let tls_stream = tls.connect(domain.as_ref(), stream).await?;
let mut client = ImapClient::new(tls_stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
client.debug = true;
}
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client::Secure(client))
}
pub async fn connect_insecure<A: net::ToSocketAddrs>(addr: A) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let mut client = ImapClient::new(stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
client.debug = true;
}
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client::Insecure(client))
}
pub async fn secure<S: AsRef<str>>(
self,
domain: S,
certificate_checks: CertificateChecks,
) -> ImapResult<Client> {
match self {
Client::Insecure(client) => {
let tls = dc_build_tls(certificate_checks);
let client_sec = client.secure(domain, tls).await?;
Ok(Client::Secure(client_sec))
}
// Nothing to do
Client::Secure(_) => Ok(self),
}
}
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
self,
auth_type: S,
authenticator: &A,
) -> Result<Session, (ImapError, Client)> {
match self {
Client::Secure(i) => match i.authenticate(auth_type, authenticator).await {
Ok(session) => Ok(Session::Secure(session)),
Err((err, c)) => Err((err, Client::Secure(c))),
},
Client::Insecure(i) => match i.authenticate(auth_type, authenticator).await {
Ok(session) => Ok(Session::Insecure(session)),
Err((err, c)) => Err((err, Client::Insecure(c))),
},
}
}
pub async fn login<U: AsRef<str>, P: AsRef<str>>(
self,
username: U,
password: P,
) -> Result<Session, (ImapError, Client)> {
match self {
Client::Secure(i) => match i.login(username, password).await {
Ok(session) => Ok(Session::Secure(session)),
Err((err, c)) => Err((err, Client::Secure(c))),
},
Client::Insecure(i) => match i.login(username, password).await {
Ok(session) => Ok(Session::Insecure(session)),
Err((err, c)) => Err((err, Client::Insecure(c))),
},
}
}
}

View File

@@ -1,42 +1,56 @@
use super::Imap;
use async_imap::extensions::idle::IdleResponse;
use async_imap::extensions::idle::{Handle as ImapIdleHandle, IdleResponse};
use async_native_tls::TlsStream;
use async_std::net::TcpStream;
use async_std::prelude::*;
use async_std::task;
use std::sync::atomic::Ordering;
use std::time::{Duration, SystemTime};
use crate::context::Context;
use crate::imap_client::*;
use super::select_folder;
use super::session::Session;
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Fail)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[fail(display = "IMAP IDLE protocol failed to init/complete")]
IdleProtocolFailed(#[cause] async_imap::error::Error),
#[error("IMAP IDLE protocol failed to init/complete")]
IdleProtocolFailed(#[from] async_imap::error::Error),
#[fail(display = "IMAP IDLE protocol timed out")]
IdleTimeout(#[cause] async_std::future::TimeoutError),
#[error("IMAP IDLE protocol timed out")]
IdleTimeout(#[from] async_std::future::TimeoutError),
#[fail(display = "IMAP server does not have IDLE capability")]
#[error("IMAP server does not have IDLE capability")]
IdleAbilityMissing,
#[fail(display = "IMAP select folder error")]
SelectFolderError(#[cause] select_folder::Error),
#[error("IMAP select folder error")]
SelectFolderError(#[from] select_folder::Error),
#[fail(display = "IMAP error")]
ImapError(#[cause] async_imap::error::Error),
#[fail(display = "Setup handle error")]
SetupHandleError(#[cause] super::Error),
#[error("Setup handle error")]
SetupHandleError(#[from] super::Error),
}
impl From<select_folder::Error> for Error {
fn from(err: select_folder::Error) -> Error {
Error::SelectFolderError(err)
#[derive(Debug)]
pub(crate) enum IdleHandle {
Secure(ImapIdleHandle<TlsStream<TcpStream>>),
Insecure(ImapIdleHandle<TcpStream>),
}
impl Session {
pub fn idle(self) -> IdleHandle {
match self {
Session::Secure(i) => {
let h = i.idle();
IdleHandle::Secure(h)
}
Session::Insecure(i) => {
let h = i.idle();
IdleHandle::Insecure(h)
}
}
}
}
@@ -51,9 +65,7 @@ impl Imap {
return Err(Error::IdleAbilityMissing);
}
self.setup_handle_if_needed(context)
.await
.map_err(Error::SetupHandleError)?;
self.setup_handle_if_needed(context).await?;
self.select_folder(context, watch_folder.clone()).await?;
@@ -64,9 +76,7 @@ impl Imap {
// BEWARE: If you change the Secure branch you
// typically also need to change the Insecure branch.
IdleHandle::Secure(mut handle) => {
if let Err(err) = handle.init().await {
return Err(Error::IdleProtocolFailed(err));
}
handle.init().await?;
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
*self.interrupt.lock().await = Some(interrupt);
@@ -80,18 +90,21 @@ impl Imap {
} else {
info!(context, "Idle entering wait-on-remote state");
match idle_wait.await {
IdleResponse::NewData(_) => {
Ok(IdleResponse::NewData(_)) => {
info!(context, "Idle has NewData");
}
// TODO: idle_wait does not distinguish manual interrupts
// from Timeouts if we would know it's a Timeout we could bail
// directly and reconnect .
IdleResponse::Timeout => {
Ok(IdleResponse::Timeout) => {
info!(context, "Idle-wait timeout or interruption");
}
IdleResponse::ManualInterrupt => {
Ok(IdleResponse::ManualInterrupt) => {
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
}
}
}
// if we can't properly terminate the idle
@@ -118,9 +131,7 @@ impl Imap {
}
}
IdleHandle::Insecure(mut handle) => {
if let Err(err) = handle.init().await {
return Err(Error::IdleProtocolFailed(err));
}
handle.init().await?;
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
*self.interrupt.lock().await = Some(interrupt);
@@ -134,18 +145,21 @@ impl Imap {
} else {
info!(context, "Idle entering wait-on-remote state");
match idle_wait.await {
IdleResponse::NewData(_) => {
Ok(IdleResponse::NewData(_)) => {
info!(context, "Idle has NewData");
}
// TODO: idle_wait does not distinguish manual interrupts
// from Timeouts if we would know it's a Timeout we could bail
// directly and reconnect .
IdleResponse::Timeout => {
Ok(IdleResponse::Timeout) => {
info!(context, "Idle-wait timeout or interruption");
}
IdleResponse::ManualInterrupt => {
Ok(IdleResponse::ManualInterrupt) => {
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
}
}
}
// if we can't properly terminate the idle

View File

@@ -5,6 +5,8 @@
use std::sync::atomic::{AtomicBool, Ordering};
use num_traits::FromPrimitive;
use async_imap::{
error::Result as ImapResult,
types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute},
@@ -12,88 +14,74 @@ use async_imap::{
use async_std::sync::{Mutex, RwLock};
use async_std::task;
use crate::config::*;
use crate::constants::*;
use crate::context::Context;
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_receive_imf::{
dc_receive_imf, from_field_to_contact_id, is_msgrmsg_rfc724_mid_in_list,
};
use crate::events::Event;
use crate::imap_client::*;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job::{job_add, Action};
use crate::login_param::{CertificateChecks, LoginParam};
use crate::message::{self, update_server_uid};
use crate::mimeparser;
use crate::oauth2::dc_get_oauth2_access_token;
use crate::param::Params;
use crate::stock::StockMessage;
mod client;
mod idle;
pub mod select_folder;
mod session;
const DC_IMAP_SEEN: usize = 0x0001;
use client::Client;
use session::Session;
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Fail)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[fail(display = "IMAP Connect without configured params")]
#[error("IMAP Connect without configured params")]
ConnectWithoutConfigure,
#[fail(display = "IMAP Connection Failed params: {}", _0)]
#[error("IMAP Connection Failed params: {0}")]
ConnectionFailed(String),
#[fail(display = "IMAP No Connection established")]
#[error("IMAP No Connection established")]
NoConnection,
#[fail(display = "IMAP Could not get OAUTH token")]
#[error("IMAP Could not get OAUTH token")]
OauthError,
#[fail(display = "IMAP Could not login as {}", _0)]
#[error("IMAP Could not login as {0}")]
LoginFailed(String),
#[fail(display = "IMAP Could not fetch")]
FetchFailed(#[cause] async_imap::error::Error),
#[error("IMAP Could not fetch")]
FetchFailed(#[from] async_imap::error::Error),
#[fail(display = "IMAP operation attempted while it is torn down")]
#[error("IMAP operation attempted while it is torn down")]
InTeardown,
#[fail(display = "IMAP operation attempted while it is torn down")]
SqlError(#[cause] crate::sql::Error),
#[error("IMAP operation attempted while it is torn down")]
SqlError(#[from] crate::sql::Error),
#[fail(display = "IMAP got error from elsewhere")]
WrappedError(#[cause] crate::error::Error),
#[error("IMAP got error from elsewhere")]
WrappedError(#[from] crate::error::Error),
#[fail(display = "IMAP select folder error")]
SelectFolderError(#[cause] select_folder::Error),
#[error("IMAP select folder error")]
SelectFolderError(#[from] select_folder::Error),
#[fail(display = "No mailbox selected, folder: {:?}", _0)]
#[error("Mail parse error")]
MailParseError(#[from] mailparse::MailParseError),
#[error("No mailbox selected, folder: {0}")]
NoMailbox(String),
#[fail(display = "IMAP other error: {:?}", _0)]
#[error("IMAP other error: {0}")]
Other(String),
}
impl From<crate::sql::Error> for Error {
fn from(err: crate::sql::Error) -> Error {
Error::SqlError(err)
}
}
impl From<crate::error::Error> for Error {
fn from(err: crate::error::Error) -> Error {
Error::WrappedError(err)
}
}
impl From<Error> for crate::error::Error {
fn from(err: Error) -> crate::error::Error {
crate::error::Error::Message(err.to_string())
}
}
impl From<select_folder::Error> for Error {
fn from(err: select_folder::Error) -> Error {
Error::SelectFolderError(err)
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq)]
pub enum ImapActionResult {
Failed,
@@ -102,7 +90,20 @@ pub enum ImapActionResult {
Success,
}
const PREFETCH_FLAGS: &str = "(UID ENVELOPE)";
/// Prefetch:
/// - Message-ID to check if we already have the message.
/// - In-Reply-To and References to check if message is a reply to chat message.
/// - Chat-Version to check if a message is a chat message
/// - Autocrypt-Setup-Message to check if a message is an autocrypt setup message,
/// not necessarily sent by Delta Chat.
const PREFETCH_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \
FROM \
IN-REPLY-TO REFERENCES \
CHAT-VERSION \
AUTOCRYPT-SETUP-MESSAGE\
)])";
const DELETE_CHECK_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])";
const JUST_UID: &str = "(UID)";
const BODY_FLAGS: &str = "(FLAGS BODY.PEEK[])";
const SELECT_ALL: &str = "1:*";
@@ -154,8 +155,10 @@ struct ImapConfig {
pub selected_mailbox: Option<Mailbox>,
pub selected_folder_needs_expunge: bool,
pub can_idle: bool,
pub has_xlist: bool,
pub imap_delimiter: char,
/// True if the server has MOVE capability as defined in
/// https://tools.ietf.org/html/rfc6851
pub can_move: bool,
}
impl Default for ImapConfig {
@@ -172,8 +175,7 @@ impl Default for ImapConfig {
selected_mailbox: None,
selected_folder_needs_expunge: false,
can_idle: false,
has_xlist: false,
imap_delimiter: '.',
can_move: false,
}
}
}
@@ -267,7 +269,7 @@ impl Imap {
let imap_port = config.imap_port;
context.stock_string_repl_str2(
StockMessage::ServerResponse,
format!("{}:{}", imap_server, imap_port),
format!("IMAP {}:{}", imap_server, imap_port),
err.to_string(),
)
};
@@ -326,7 +328,7 @@ impl Imap {
cfg.imap_port = 0;
cfg.can_idle = false;
cfg.has_xlist = false;
cfg.can_move = false;
}
/// Connects to imap account using already-configured parameters.
@@ -387,7 +389,7 @@ impl Imap {
true
} else {
let can_idle = caps.has_str("IDLE");
let has_xlist = caps.has_str("XLIST");
let can_move = caps.has_str("MOVE");
let caps_list = caps.iter().fold(String::new(), |s, c| {
if let Capability::Atom(x) = c {
s + &format!(" {}", x)
@@ -397,7 +399,7 @@ impl Imap {
});
self.config.write().await.can_idle = can_idle;
self.config.write().await.has_xlist = has_xlist;
self.config.write().await.can_move = can_move;
*self.connected.lock().await = true;
emit_event!(
context,
@@ -555,6 +557,9 @@ impl Imap {
context: &Context,
folder: S,
) -> Result<bool> {
let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails)).unwrap_or_default();
let (uid_validity, last_seen_uid) =
self.select_with_uidvalidity(context, folder.as_ref())?;
@@ -562,7 +567,7 @@ impl Imap {
let mut list = if let Some(ref mut session) = &mut *self.session.lock().await {
// fetch messages with larger UID than the last one seen
// (`UID FETCH lastseenuid+1:*)`, see RFC 4549
// `(UID FETCH lastseenuid+1:*)`, see RFC 4549
let set = format!("{}:*", last_seen_uid + 1);
match session.uid_fetch(set, PREFETCH_FLAGS).await {
Ok(list) => list,
@@ -580,11 +585,16 @@ impl Imap {
list.sort_unstable_by_key(|msg| msg.uid.unwrap_or_default());
for msg in &list {
let cur_uid = msg.uid.unwrap_or_default();
for fetch in &list {
let cur_uid = fetch.uid.unwrap_or_default();
if cur_uid <= last_seen_uid {
// seems that at least dovecot sends the last available UID
// even if we asked for higher UID+N:*
// If the mailbox is not empty, results always include
// at least one UID, even if last_seen_uid+1 is past
// the last UID in the mailbox. It happens because
// uid+1:* is interpreted the same way as *:uid+1.
// See https://tools.ietf.org/html/rfc3501#page-61 for
// standard reference. Therefore, sometimes we receive
// already seen messages and have to filter them out.
info!(
context,
"fetch_new_messages: ignoring uid {}, last seen was {}", cur_uid, last_seen_uid
@@ -593,20 +603,14 @@ impl Imap {
}
read_cnt += 1;
let message_id = prefetch_get_message_id(msg).unwrap_or_default();
if !precheck_imf(context, &message_id, folder.as_ref(), cur_uid) {
// check passed, go fetch the rest
if self.fetch_single_msg(context, &folder, cur_uid).await == 0 {
info!(
context,
"Read error for message {} from \"{}\", trying over later.",
message_id,
folder.as_ref()
);
read_errors += 1;
}
} else {
let headers = get_fetch_headers(fetch)?;
let message_id = prefetch_get_message_id(&headers).unwrap_or_default();
if let Ok(true) =
precheck_imf(context, &message_id, folder.as_ref(), cur_uid).map_err(|err| {
warn!(context, "precheck_imf error: {}", err);
err
})
{
// we know the message-id already or don't want the message otherwise.
info!(
context,
@@ -614,6 +618,37 @@ impl Imap {
message_id,
folder.as_ref(),
);
} else {
// we do not know the message-id
// or the message-id is missing (in this case, we create one in the further process)
// or some other error happened
let show = prefetch_should_download(context, &headers, show_emails)
.map_err(|err| {
warn!(context, "prefetch_should_download error: {}", err);
err
})
.unwrap_or(true);
if !show {
info!(
context,
"Ignoring new message {} from \"{}\".",
message_id,
folder.as_ref(),
);
} else {
// check passed, go fetch the rest
if let Err(err) = self.fetch_single_msg(context, &folder, cur_uid).await {
info!(
context,
"Read error for message {} from \"{}\", trying over later: {}.",
message_id,
folder.as_ref(),
err
);
read_errors += 1;
}
}
}
if read_errors == 0 {
new_last_seen_uid = cur_uid;
@@ -657,17 +692,19 @@ impl Imap {
context.sql.set_raw_config(context, &key, Some(&val)).ok();
}
/// Fetches a single message by server UID.
///
/// If it succeeds, the message should be treated as received even
/// if no database entries are created. If the function returns an
/// error, the caller should try again later.
async fn fetch_single_msg<S: AsRef<str>>(
&self,
context: &Context,
folder: S,
server_uid: u32,
) -> usize {
// the function returns:
// 0 the caller should try over again later
// or 1 if the messages should be treated as received, the caller should not try to read the message again (even if no database entries are returned)
) -> Result<()> {
if !self.is_connected().await {
return 0;
return Err(Error::Other("Not connected".to_string()));
}
let set = format!("{}", server_uid);
@@ -686,54 +723,42 @@ impl Imap {
folder.as_ref(),
err
);
return 0;
return Err(Error::FetchFailed(err));
}
}
} else {
// we could not get a valid imap session, this should be retried
self.trigger_reconnect();
return 0;
return Err(Error::Other("Could not get IMAP session".to_string()));
};
if msgs.is_empty() {
if let Some(msg) = msgs.first() {
// XXX put flags into a set and pass them to dc_receive_imf
let is_deleted = msg.flags().any(|flag| flag == Flag::Deleted);
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
if !is_deleted && msg.body().is_some() {
let body = msg.body().unwrap_or_default();
if let Err(err) =
dc_receive_imf(context, &body, folder.as_ref(), server_uid, is_seen)
{
return Err(Error::Other(format!("dc_receive_imf error: {}", err)));
}
}
} else {
warn!(
context,
"Message #{} does not exist in folder \"{}\".",
server_uid,
folder.as_ref()
);
} else {
let msg = &msgs[0];
// XXX put flags into a set and pass them to dc_receive_imf
let is_deleted = msg.flags().any(|flag| match flag {
Flag::Deleted => true,
_ => false,
});
let is_seen = msg.flags().any(|flag| match flag {
Flag::Seen => true,
_ => false,
});
let flags = if is_seen { DC_IMAP_SEEN } else { 0 };
if !is_deleted && msg.body().is_some() {
let body = msg.body().unwrap_or_default();
if let Err(err) =
dc_receive_imf(context, &body, folder.as_ref(), server_uid, flags as u32)
{
warn!(
context,
"dc_receive_imf failed for imap-message {}/{}: {:?}",
folder.as_ref(),
server_uid,
err
);
}
}
}
1
Ok(())
}
pub fn can_move(&self) -> bool {
task::block_on(async move { self.config.read().await.can_move })
}
pub fn mv(
@@ -742,7 +767,6 @@ impl Imap {
folder: &str,
uid: u32,
dest_folder: &str,
dest_uid: &mut u32,
) -> ImapActionResult {
task::block_on(async move {
if folder == dest_folder {
@@ -759,58 +783,76 @@ impl Imap {
return imapresult;
}
// we are connected, and the folder is selected
// XXX Rust-Imap provides no target uid on mv, so just set it to 0
*dest_uid = 0;
let set = format!("{}", uid);
let display_folder_id = format!("{}/{}", folder, uid);
if let Some(ref mut session) = &mut *self.session.lock().await {
match session.uid_mv(&set, &dest_folder).await {
Ok(_) => {
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} moved to {}",
display_folder_id, dest_folder
))
);
return ImapActionResult::Success;
}
Err(err) => {
warn!(
context,
"Cannot move message, fallback to COPY/DELETE {}/{} to {}: {}",
folder,
uid,
dest_folder,
err
);
}
}
} else {
unreachable!();
};
if let Some(ref mut session) = &mut *self.session.lock().await {
match session.uid_copy(&set, &dest_folder).await {
Ok(_) => {
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid);
ImapActionResult::Failed
} else {
self.config.write().await.selected_folder_needs_expunge = true;
ImapActionResult::Success
if self.can_move() {
if let Some(ref mut session) = &mut *self.session.lock().await {
match session.uid_mv(&set, &dest_folder).await {
Ok(_) => {
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} moved to {}",
display_folder_id, dest_folder
))
);
return ImapActionResult::Success;
}
Err(err) => {
warn!(
context,
"Cannot move message, fallback to COPY/DELETE {}/{} to {}: {}",
folder,
uid,
dest_folder,
err
);
}
}
Err(err) => {
warn!(context, "Could not copy message: {}", err);
ImapActionResult::Failed
}
} else {
unreachable!();
};
} else {
info!(
context,
"Server does not support MOVE, fallback to COPY/DELETE {}/{} to {}",
folder,
uid,
dest_folder
);
}
if let Some(ref mut session) = &mut *self.session.lock().await {
if let Err(err) = session.uid_copy(&set, &dest_folder).await {
warn!(context, "Could not copy message: {}", err);
return ImapActionResult::Failed;
}
} else {
unreachable!();
}
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid);
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} copied to {} (delete FAILED)",
display_folder_id, dest_folder
))
);
ImapActionResult::Failed
} else {
self.config.write().await.selected_folder_needs_expunge = true;
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} copied to {} (delete successfull)",
display_folder_id, dest_folder
))
);
ImapActionResult::Success
}
})
}
@@ -915,16 +957,15 @@ impl Imap {
})
}
// only returns 0 on connection problems; we should try later again in this case *
pub fn delete_msg(
&self,
context: &Context,
message_id: &str,
folder: &str,
uid: &mut u32,
uid: u32,
) -> ImapActionResult {
task::block_on(async move {
if let Some(imapresult) = self.prepare_imap_operation_on_msg(context, folder, *uid) {
if let Some(imapresult) = self.prepare_imap_operation_on_msg(context, folder, uid) {
return imapresult;
}
// we are connected, and the folder is selected
@@ -935,19 +976,23 @@ impl Imap {
// double-check that we are deleting the correct message-id
// this comes at the expense of another imap query
if let Some(ref mut session) = &mut *self.session.lock().await {
match session.uid_fetch(set, PREFETCH_FLAGS).await {
match session.uid_fetch(set, DELETE_CHECK_FLAGS).await {
Ok(msgs) => {
if msgs.is_empty() {
let fetch = if let Some(fetch) = msgs.first() {
fetch
} else {
warn!(
context,
"Cannot delete on IMAP, {}: imap entry gone '{}'",
display_imap_id,
message_id,
);
return ImapActionResult::Failed;
}
let remote_message_id =
prefetch_get_message_id(msgs.first().unwrap()).unwrap_or_default();
return ImapActionResult::AlreadyDone;
};
let remote_message_id = get_fetch_headers(fetch)
.and_then(|headers| prefetch_get_message_id(&headers))
.unwrap_or_default();
if remote_message_id != message_id {
warn!(
@@ -957,7 +1002,7 @@ impl Imap {
remote_message_id,
message_id,
);
*uid = 0;
return ImapActionResult::Failed;
}
}
Err(err) => {
@@ -965,18 +1010,18 @@ impl Imap {
context,
"Cannot delete {} on IMAP: {}", display_imap_id, err
);
*uid = 0;
return ImapActionResult::RetryLater;
}
}
}
// mark the message for deletion
if !self.add_flag_finalized(context, *uid, "\\Deleted").await {
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
warn!(
context,
"Cannot mark message {} as \"Deleted\".", display_imap_id
);
ImapActionResult::Failed
ImapActionResult::RetryLater
} else {
emit_event!(
context,
@@ -995,12 +1040,14 @@ impl Imap {
let folders_configured = context
.sql
.get_raw_config_int(context, "folders_configured");
if folders_configured.unwrap_or_default() >= 3 {
// the "3" here we increase if we have future updates to
// to folder configuration
if folders_configured.unwrap_or_default() >= DC_FOLDERS_CONFIGURED_VERSION {
return Ok(());
}
self.configure_folders(context, create_mvbox)
}
pub fn configure_folders(&self, context: &Context, create_mvbox: bool) -> Result<()> {
task::block_on(async move {
if !self.is_connected().await {
return Err(Error::NoConnection);
@@ -1025,7 +1072,15 @@ impl Imap {
});
info!(context, "sentbox folder is {:?}", sentbox_folder);
let delimiter = self.config.read().await.imap_delimiter;
let mut delimiter = ".";
if let Some(folder) = folders.first() {
if let Some(d) = folder.delimiter() {
if !d.is_empty() {
delimiter = d;
}
}
}
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
let fallback_folder = format!("INBOX{}DeltaChat", delimiter);
let mut mvbox_folder = folders
@@ -1089,9 +1144,11 @@ impl Imap {
Some(sentbox_folder.name()),
)?;
}
context
.sql
.set_raw_config_int(context, "folders_configured", 3)?;
context.sql.set_raw_config_int(
context,
"folders_configured",
DC_FOLDERS_CONFIGURED_VERSION,
)?;
}
info!(context, "FINISHED configuring IMAP-folders.");
Ok(())
@@ -1099,7 +1156,6 @@ impl Imap {
}
async fn list_folders(&self, session: &mut Session, context: &Context) -> Option<Vec<Name>> {
// TODO: use xlist when available
match session.list(Some(""), Some("*")).await {
Ok(list) => {
if list.is_empty() {
@@ -1127,13 +1183,13 @@ impl Imap {
return;
}
if let Err(err) = self.setup_handle_if_needed(context).await {
error!(context, "could not setup imap connection: {:?}", err);
error!(context, "could not setup imap connection: {}", err);
return;
}
if let Err(err) = self.select_folder(context, Some(&folder)).await {
error!(
context,
"Could not select {} for expunging: {:?}", folder, err
"Could not select {} for expunging: {}", folder, err
);
return;
}
@@ -1153,9 +1209,21 @@ impl Imap {
emit_event!(context, Event::ImapFolderEmptied(folder.to_string()));
}
Err(err) => {
error!(context, "expunge failed {}: {:?}", folder, err);
error!(context, "expunge failed {}: {}", folder, err);
}
}
if let Err(err) = crate::sql::execute(
context,
&context.sql,
"UPDATE msgs SET server_folder='',server_uid=0 WHERE server_folder=?",
params![folder],
) {
warn!(
context,
"Failed to reset server_uid and server_folder for deleted messages: {}", err
);
}
});
}
}
@@ -1201,73 +1269,131 @@ fn get_folder_meaning(folder_name: &Name) -> FolderMeaning {
}
}
fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server_uid: u32) -> bool {
if let Ok((old_server_folder, old_server_uid, msg_id)) =
message::rfc724_mid_exists(context, &rfc724_mid)
fn precheck_imf(
context: &Context,
rfc724_mid: &str,
server_folder: &str,
server_uid: u32,
) -> Result<bool> {
if let Some((old_server_folder, old_server_uid, msg_id)) =
message::rfc724_mid_exists(context, &rfc724_mid)?
{
if old_server_folder.is_empty() && old_server_uid == 0 {
info!(context, "[move] detected bbc-self {}", rfc724_mid,);
context.do_heuristics_moves(server_folder.as_ref(), msg_id);
job_add(
info!(
context,
Action::MarkseenMsgOnImap,
msg_id.to_u32() as i32,
Params::new(),
0,
"[move] detected bcc-self {} as {}/{}", rfc724_mid, server_folder, server_uid
);
let delete_server_after = context.get_config_delete_server_after();
if delete_server_after != Some(0) {
context.do_heuristics_moves(server_folder.as_ref(), msg_id);
job_add(
context,
Action::MarkseenMsgOnImap,
msg_id.to_u32() as i32,
Params::new(),
0,
);
}
} else if old_server_folder != server_folder {
info!(context, "[move] detected moved message {}", rfc724_mid,);
info!(
context,
"[move] detected message {} moved by other device from {}/{} to {}/{}",
rfc724_mid,
old_server_folder,
old_server_uid,
server_folder,
server_uid
);
} else if old_server_uid == 0 {
info!(
context,
"[move] detected message {} moved by us from {}/{} to {}/{}",
rfc724_mid,
old_server_folder,
old_server_uid,
server_folder,
server_uid
);
} else if old_server_uid != server_uid {
warn!(
context,
"UID for message {} in folder {} changed from {} to {}",
rfc724_mid,
server_folder,
old_server_uid,
server_uid
);
}
if old_server_folder != server_folder || old_server_uid != server_uid {
update_server_uid(context, &rfc724_mid, server_folder, server_uid);
}
true
Ok(true)
} else {
false
Ok(false)
}
}
fn parse_message_id(message_id: &[u8]) -> crate::error::Result<String> {
let value = std::str::from_utf8(message_id)?;
let addrs = mailparse::addrparse(value)
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
if let Some(info) = addrs.extract_single_info() {
return Ok(info.addr);
}
bail!("could not parse message_id: {}", value);
fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>> {
let header_bytes = match prefetch_msg.header() {
Some(header_bytes) => header_bytes,
None => return Ok(Vec::new()),
};
let (headers, _) = mailparse::parse_headers(header_bytes)?;
Ok(headers)
}
fn prefetch_get_message_id(prefetch_msg: &Fetch) -> Result<String> {
if prefetch_msg.envelope().is_none() {
return Err(Error::Other(
"prefectch: message has no envelope".to_string(),
));
}
let message_id = prefetch_msg.envelope().unwrap().message_id;
if message_id.is_none() {
return Err(Error::Other("prefetch: No message ID found".to_string()));
}
parse_message_id(&message_id.unwrap()).map_err(Into::into)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_message_id() {
assert_eq!(
parse_message_id(b"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
assert_eq!(
parse_message_id(b"<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String> {
if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId) {
Ok(crate::mimeparser::parse_message_id(&message_id)?)
} else {
Err(Error::Other("prefetch: No message ID found".to_string()))
}
}
fn prefetch_is_reply_to_chat_message(context: &Context, headers: &[mailparse::MailHeader]) -> bool {
if let Some(value) = headers.get_header_value(HeaderDef::InReplyTo) {
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
return true;
}
}
if let Some(value) = headers.get_header_value(HeaderDef::References) {
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
return true;
}
}
false
}
fn prefetch_should_download(
context: &Context,
headers: &[mailparse::MailHeader],
show_emails: ShowEmails,
) -> Result<bool> {
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
let is_reply_to_chat_message = prefetch_is_reply_to_chat_message(context, &headers);
// Autocrypt Setup Message should be shown even if it is from non-chat client.
let is_autocrypt_setup_message = headers
.get_header_value(HeaderDef::AutocryptSetupMessage)
.is_some();
let (_contact_id, blocked_contact, origin) =
from_field_to_contact_id(context, &mimeparser::get_from(headers))?;
let accepted_contact = origin.is_known();
let show = is_autocrypt_setup_message
|| match show_emails {
ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
ShowEmails::AcceptedContacts => {
is_chat_message || is_reply_to_chat_message || accepted_contact
}
ShowEmails::All => true,
};
let show = show && !blocked_contact;
Ok(show)
}

View File

@@ -4,25 +4,53 @@ use crate::context::Context;
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Fail)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[fail(display = "IMAP Could not obtain imap-session object.")]
#[error("IMAP Could not obtain imap-session object.")]
NoSession,
#[fail(display = "IMAP Connection Lost or no connection established")]
#[error("IMAP Connection Lost or no connection established")]
ConnectionLost,
#[fail(display = "IMAP Folder name invalid: {:?}", _0)]
#[error("IMAP Folder name invalid: {0}")]
BadFolderName(String),
#[fail(display = "IMAP close/expunge failed: {}", _0)]
CloseExpungeFailed(#[cause] async_imap::error::Error),
#[error("IMAP close/expunge failed")]
CloseExpungeFailed(#[from] async_imap::error::Error),
#[fail(display = "IMAP other error: {:?}", _0)]
#[error("IMAP other error: {0}")]
Other(String),
}
impl Imap {
/// Issues a CLOSE command to expunge selected folder.
///
/// CLOSE is considerably faster than an EXPUNGE, see
/// https://tools.ietf.org/html/rfc3501#section-6.4.2
async fn close_folder(&self, context: &Context) -> Result<()> {
if let Some(ref folder) = self.config.read().await.selected_folder {
info!(context, "Expunge messages in \"{}\".", folder);
if let Some(ref mut session) = &mut *self.session.lock().await {
match session.close().await {
Ok(_) => {
info!(context, "close/expunge succeeded");
}
Err(err) => {
self.trigger_reconnect();
return Err(Error::CloseExpungeFailed(err));
}
}
} else {
return Err(Error::NoSession);
}
}
let mut cfg = self.config.write().await;
cfg.selected_folder = None;
cfg.selected_folder_needs_expunge = false;
Ok(())
}
/// select a folder, possibly update uid_validity and, if needed,
/// expunge the folder to remove delete-marked messages.
pub(super) async fn select_folder<S: AsRef<str>>(
@@ -38,39 +66,14 @@ impl Imap {
return Err(Error::NoSession);
}
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
// if there is _no_ new folder, we continue as we might want to expunge below.
if let Some(ref folder) = folder {
if let Some(ref selected_folder) = self.config.read().await.selected_folder {
if folder.as_ref() == selected_folder {
return Ok(());
}
}
let needs_expunge = self.config.read().await.selected_folder_needs_expunge;
if needs_expunge {
self.close_folder(context).await?;
}
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
let needs_expunge = { self.config.read().await.selected_folder_needs_expunge };
if needs_expunge {
if let Some(ref folder) = self.config.read().await.selected_folder {
info!(context, "Expunge messages in \"{}\".", folder);
// A CLOSE-SELECT is considerably faster than an EXPUNGE-SELECT, see
// https://tools.ietf.org/html/rfc3501#section-6.4.2
if let Some(ref mut session) = &mut *self.session.lock().await {
match session.close().await {
Ok(_) => {
info!(context, "close/expunge succeeded");
}
Err(err) => {
self.trigger_reconnect();
return Err(Error::CloseExpungeFailed(err));
}
}
} else {
return Err(Error::NoSession);
}
}
self.config.write().await.selected_folder_needs_expunge = false;
let folder_str: Option<&str> = folder.as_ref().map(|x| x.as_ref());
if self.config.read().await.selected_folder.as_deref() == folder_str {
return Ok(());
}
// select new folder

View File

@@ -1,125 +1,18 @@
use async_imap::{
error::{Error as ImapError, Result as ImapResult},
extensions::idle::Handle as ImapIdleHandle,
error::Result as ImapResult,
types::{Capabilities, Fetch, Mailbox, Name},
Client as ImapClient, Session as ImapSession,
Session as ImapSession,
};
use async_native_tls::TlsStream;
use async_std::net::{self, TcpStream};
use async_std::net::TcpStream;
use async_std::prelude::*;
use crate::login_param::{dc_build_tls, CertificateChecks};
#[derive(Debug)]
pub(crate) enum Client {
Secure(ImapClient<TlsStream<TcpStream>>),
Insecure(ImapClient<TcpStream>),
}
#[derive(Debug)]
pub(crate) enum Session {
Secure(ImapSession<TlsStream<TcpStream>>),
Insecure(ImapSession<TcpStream>),
}
#[derive(Debug)]
pub(crate) enum IdleHandle {
Secure(ImapIdleHandle<TlsStream<TcpStream>>),
Insecure(ImapIdleHandle<TcpStream>),
}
impl Client {
pub async fn connect_secure<A: net::ToSocketAddrs, S: AsRef<str>>(
addr: A,
domain: S,
certificate_checks: CertificateChecks,
) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let tls = dc_build_tls(certificate_checks)?;
let tls_connector: async_native_tls::TlsConnector = tls.into();
let tls_stream = tls_connector.connect(domain.as_ref(), stream).await?;
let mut client = ImapClient::new(tls_stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
client.debug = true;
}
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client::Secure(client))
}
pub async fn connect_insecure<A: net::ToSocketAddrs>(addr: A) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let mut client = ImapClient::new(stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
client.debug = true;
}
let _greeting = client
.read_response()
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client::Insecure(client))
}
pub async fn secure<S: AsRef<str>>(
self,
domain: S,
certificate_checks: CertificateChecks,
) -> ImapResult<Client> {
match self {
Client::Insecure(client) => {
let tls = dc_build_tls(certificate_checks)?;
let tls_stream = tls.into();
let client_sec = client.secure(domain, &tls_stream).await?;
Ok(Client::Secure(client_sec))
}
// Nothing to do
Client::Secure(_) => Ok(self),
}
}
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
self,
auth_type: S,
authenticator: &A,
) -> Result<Session, (ImapError, Client)> {
match self {
Client::Secure(i) => match i.authenticate(auth_type, authenticator).await {
Ok(session) => Ok(Session::Secure(session)),
Err((err, c)) => Err((err, Client::Secure(c))),
},
Client::Insecure(i) => match i.authenticate(auth_type, authenticator).await {
Ok(session) => Ok(Session::Insecure(session)),
Err((err, c)) => Err((err, Client::Insecure(c))),
},
}
}
pub async fn login<U: AsRef<str>, P: AsRef<str>>(
self,
username: U,
password: P,
) -> Result<Session, (ImapError, Client)> {
match self {
Client::Secure(i) => match i.login(username, password).await {
Ok(session) => Ok(Session::Secure(session)),
Err((err, c)) => Err((err, Client::Secure(c))),
},
Client::Insecure(i) => match i.login(username, password).await {
Ok(session) => Ok(Session::Insecure(session)),
Err((err, c)) => Err((err, Client::Insecure(c))),
},
}
}
}
impl Session {
pub async fn capabilities(&mut self) -> ImapResult<Capabilities> {
let res = match self {
@@ -230,19 +123,6 @@ impl Session {
Ok(res)
}
pub fn idle(self) -> IdleHandle {
match self {
Session::Secure(i) => {
let h = i.idle();
IdleHandle::Secure(h)
}
Session::Insecure(i) => {
let h = i.idle();
IdleHandle::Insecure(h)
}
}
}
pub async fn uid_store<S1, S2>(&mut self, uid_set: S1, query: S2) -> ImapResult<Vec<Fetch>>
where
S1: AsRef<str>,

View File

@@ -10,7 +10,6 @@ use crate::blob::BlobObject;
use crate::chat;
use crate::chat::delete_and_reset_all_device_msgs;
use crate::config::Config;
use crate::configure::*;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
@@ -18,7 +17,7 @@ use crate::e2ee;
use crate::error::*;
use crate::events::Event;
use crate::job::*;
use crate::key::*;
use crate::key::{self, DcKey, Key, SignedSecretKey};
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::*;
@@ -176,9 +175,7 @@ pub fn render_setup_file(context: &Context, passphrase: &str) -> Result<String>
passphrase.len() >= 2,
"Passphrase must be at least 2 chars long."
);
let self_addr = e2ee::ensure_secret_key_exists(context)?;
let private_key = Key::from_self_private(context, self_addr, &context.sql)
.ok_or_else(|| format_err!("Failed to get private key."))?;
let private_key = Key::from(SignedSecretKey::load_self(context)?);
let ac_headers = match context.get_config_bool(Config::E2eeEnabled) {
false => None,
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
@@ -289,7 +286,6 @@ fn set_self_key(
.and_then(|(k, h)| k.split_key().map(|pub_key| (k, pub_key, h)));
ensure!(keys.is_some(), "Not a valid private key");
let (private_key, public_key, header) = keys.unwrap();
let preferencrypt = header.get("Autocrypt-Prefer-Encrypt");
match preferencrypt.map(|s| s.as_str()) {
@@ -314,34 +310,26 @@ fn set_self_key(
let self_addr = context.get_config(Config::ConfiguredAddr);
ensure!(self_addr.is_some(), "Missing self addr");
let addr = EmailAddress::new(&self_addr.unwrap_or_default())?;
// XXX maybe better make dc_key_save_self_keypair delete things
sql::execute(
let (public, secret) = match (public_key, private_key) {
(Key::Public(p), Key::Secret(s)) => (p, s),
_ => bail!("wrong keys unpacked"),
};
let keypair = pgp::KeyPair {
addr,
public,
secret,
};
key::store_self_keypair(
context,
&context.sql,
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
params![public_key.to_bytes(), private_key.to_bytes()],
&keypair,
if set_default {
key::KeyPairUse::Default
} else {
key::KeyPairUse::ReadOnly
},
)?;
if set_default {
sql::execute(
context,
&context.sql,
"UPDATE keypairs SET is_default=0;",
params![],
)?;
}
if !dc_key_save_self_keypair(
context,
&public_key,
&private_key,
self_addr.unwrap_or_default(),
set_default,
&context.sql,
) {
bail!("Cannot save keypair, internal key-state possibly corrupted now!");
}
Ok(())
}
@@ -423,7 +411,7 @@ fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Resul
);
ensure!(
!dc_is_configured(context),
!context.is_configured(),
"Cannot import backups to accounts in use."
);
context.sql.close(&context);
@@ -746,7 +734,6 @@ fn export_key_to_asc_file(
#[cfg(test)]
mod tests {
use super::*;
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
use crate::test_utils::*;
use ::pgp::armor::BlockType;
@@ -767,7 +754,6 @@ mod tests {
assert!(msg.contains("-----BEGIN PGP MESSAGE-----\r\n"));
assert!(msg.contains("Passphrase-Format: numeric9x4\r\n"));
assert!(msg.contains("Passphrase-Begin: he\n"));
assert!(msg.contains("==\n"));
assert!(msg.contains("-----END PGP MESSAGE-----\n"));
}
@@ -801,8 +787,7 @@ mod tests {
#[test]
fn test_export_key_to_asc_file() {
let context = dummy_context();
let base64 = include_str!("../test-data/key/public.asc");
let key = Key::from_base64(base64, KeyType::Public).unwrap();
let key = Key::from(alice_keypair().public);
let blobdir = "$BLOBDIR";
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key).is_ok());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();

View File

@@ -9,6 +9,9 @@ use deltachat_derive::{FromSql, ToSql};
use itertools::Itertools;
use rand::{thread_rng, Rng};
use async_smtp::smtp::response::Category;
use async_smtp::smtp::response::Code;
use async_smtp::smtp::response::Detail;
use async_std::task;
use crate::blob::BlobObject;
@@ -19,7 +22,7 @@ use crate::constants::*;
use crate::contact::Contact;
use crate::context::{Context, PerformJobsNeeded};
use crate::dc_tools::*;
use crate::error::{Error, Result};
use crate::error::{bail, ensure, format_err, Error, Result};
use crate::events::Event;
use crate::imap::*;
use crate::imex::*;
@@ -72,7 +75,19 @@ impl Default for Thread {
}
}
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[derive(
Debug,
Display,
Copy,
Clone,
PartialEq,
Eq,
PartialOrd,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
)]
#[repr(i32)]
pub enum Action {
Unknown = 0,
@@ -80,10 +95,14 @@ pub enum Action {
// Jobs in the INBOX-thread, range from DC_IMAP_THREAD..DC_IMAP_THREAD+999
Housekeeping = 105, // low priority ...
EmptyServer = 107,
DeleteMsgOnImap = 110,
MarkseenMdnOnImap = 120,
OldDeleteMsgOnImap = 110,
MarkseenMsgOnImap = 130,
// Moving message is prioritized lower than deletion so we don't
// bother moving message if it is already scheduled for deletion.
MoveMsg = 200,
DeleteMsgOnImap = 210,
ConfigureImap = 900,
ImexImap = 910, // ... high priority
@@ -108,9 +127,9 @@ impl From<Action> for Thread {
Unknown => Thread::Unknown,
Housekeeping => Thread::Imap,
OldDeleteMsgOnImap => Thread::Imap,
DeleteMsgOnImap => Thread::Imap,
EmptyServer => Thread::Imap,
MarkseenMdnOnImap => Thread::Imap,
MarkseenMsgOnImap => Thread::Imap,
MoveMsg => Thread::Imap,
ConfigureImap => Thread::Imap,
@@ -143,30 +162,68 @@ impl fmt::Display for Job {
}
impl Job {
/// Deletes the job from the database.
fn delete(&self, context: &Context) -> bool {
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", params![self.job_id as i32])
.is_ok()
fn new(action: Action, foreign_id: u32, param: Params, delay_seconds: i64) -> Self {
let timestamp = time();
Self {
job_id: 0,
action,
foreign_id,
desired_timestamp: timestamp + delay_seconds,
added_timestamp: timestamp,
tries: 0,
param,
pending_error: None,
}
}
/// Updates the job already stored in the database.
/// Deletes the job from the database.
fn delete(&self, context: &Context) -> bool {
if self.job_id != 0 {
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", params![self.job_id as i32])
.is_ok()
} else {
// Already deleted.
true
}
}
/// Saves the job to the database, creating a new entry if necessary.
///
/// To add a new job, use [job_add].
fn update(&self, context: &Context) -> bool {
sql::execute(
context,
&context.sql,
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
params![
self.desired_timestamp,
self.tries as i64,
self.param.to_string(),
self.job_id as i32,
],
)
.is_ok()
/// The Job is consumed by this method.
fn save(self, context: &Context) -> bool {
let thread: Thread = self.action.into();
if self.job_id != 0 {
sql::execute(
context,
&context.sql,
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
params![
self.desired_timestamp,
self.tries as i64,
self.param.to_string(),
self.job_id as i32,
],
)
.is_ok()
} else {
sql::execute(
context,
&context.sql,
"INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);",
params![
self.added_timestamp,
thread,
self.action,
self.foreign_id,
self.param.to_string(),
self.desired_timestamp
]
).is_ok()
}
}
fn smtp_send<F>(
@@ -193,15 +250,66 @@ impl Job {
Err(crate::smtp::send::Error::SendError(err)) => {
// Remote error, retry later.
warn!(context, "SMTP failed to send: {}", err);
smtp.disconnect();
self.pending_error = Some(err.to_string());
Status::RetryLater
let res = match err {
async_smtp::smtp::error::Error::Permanent(ref response) => {
match response.code {
// Sometimes servers send a permanent error when actually it is a temporary error
// For documentation see https://tools.ietf.org/html/rfc3463
// Code 5.5.0, see https://support.delta.chat/t/every-other-message-gets-stuck/877/2
Code {
category: Category::MailSystem,
detail: Detail::Zero,
..
} => Status::RetryLater,
_ => {
// If we do not retry, add an info message to the chat
// Error 5.7.1 should definitely go here: Yandex sends 5.7.1 with a link when it thinks that the email is SPAM.
match Message::load_from_db(context, MsgId::new(self.foreign_id)) {
Ok(message) => chat::add_info_msg(
context,
message.chat_id,
err.to_string(),
),
Err(e) => warn!(
context,
"couldn't load chat_id to inform user about SMTP error: {}",
e
),
};
Status::Finished(Err(format_err!("Permanent SMTP error: {}", err)))
}
}
}
async_smtp::smtp::error::Error::Transient(_) => {
// We got a transient 4xx response from SMTP server.
// Give some time until the server-side error maybe goes away.
Status::RetryLater
}
_ => {
if smtp.has_maybe_stale_connection() {
info!(context, "stale connection? immediately reconnecting");
Status::RetryNow
} else {
Status::RetryLater
}
}
};
// this clears last_success info
smtp.disconnect();
res
}
Err(crate::smtp::send::Error::EnvelopeError(err)) => {
// Local error, job is invalid, do not retry.
smtp.disconnect();
warn!(context, "SMTP job is invalid: {}", err);
Status::Finished(Err(Error::SmtpError(err)))
Status::Finished(Err(err.into()))
}
Err(crate::smtp::send::Error::NoTransport) => {
// Should never happen.
@@ -369,7 +477,7 @@ impl Job {
}
self.smtp_send(context, recipients, body, self.job_id, || {
// Remove additional SendMdn jobs we have aggretated into this one.
// Remove additional SendMdn jobs we have aggregated into this one.
job_kill_ids(context, &additional_job_ids)?;
Ok(())
})
@@ -391,18 +499,12 @@ impl Job {
if let Some(dest_folder) = dest_folder {
let server_folder = msg.server_folder.as_ref().unwrap();
let mut dest_uid = 0;
match imap_inbox.mv(
context,
server_folder,
msg.server_uid,
&dest_folder,
&mut dest_uid,
) {
match imap_inbox.mv(context, server_folder, msg.server_uid, &dest_folder) {
ImapActionResult::RetryLater => Status::RetryLater,
ImapActionResult::Success => {
message::update_server_uid(context, &msg.rfc724_mid, &dest_folder, dest_uid);
// XXX Rust-Imap provides no target uid on mv, so just set it to 0
message::update_server_uid(context, &msg.rfc724_mid, &dest_folder, 0);
Status::Finished(Ok(()))
}
ImapActionResult::Failed => {
@@ -415,14 +517,29 @@ impl Job {
}
}
/// Deletes a message on the server.
///
/// foreign_id is a MsgId pointing to a message in the trash chat
/// or a hidden message.
///
/// This job removes the database record. If there are no more
/// records pointing to the same message on the server, the job
/// also removes the message on the server.
#[allow(non_snake_case)]
fn DeleteMsgOnImap(&mut self, context: &Context) -> Status {
let imap_inbox = &context.inbox_thread.read().unwrap().imap;
let mut msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)));
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)));
if !msg.rfc724_mid.is_empty() {
if message::rfc724_mid_cnt(context, &msg.rfc724_mid) > 1 {
let cnt = message::rfc724_mid_cnt(context, &msg.rfc724_mid);
info!(
context,
"Running delete job for message {} which has {} entries in the database",
&msg.rfc724_mid,
cnt
);
if cnt > 1 {
info!(
context,
"The message is deleted from the server when all parts are deleted.",
@@ -432,13 +549,47 @@ impl Job {
we delete the message from the server */
let mid = msg.rfc724_mid;
let server_folder = msg.server_folder.as_ref().unwrap();
let res = imap_inbox.delete_msg(context, &mid, server_folder, &mut msg.server_uid);
if res == ImapActionResult::RetryLater {
// XXX RetryLater is converted to RetryNow here
return Status::RetryNow;
let res = if msg.server_uid == 0 {
// Message is already deleted on IMAP server.
ImapActionResult::AlreadyDone
} else {
imap_inbox.delete_msg(context, &mid, server_folder, msg.server_uid)
};
match res {
ImapActionResult::AlreadyDone | ImapActionResult::Success => {}
ImapActionResult::RetryLater | ImapActionResult::Failed => {
// If job has failed, for example due to some
// IMAP bug, we postpone it instead of failing
// immediately. This will prevent adding it
// immediately again if user has enabled
// automatic message deletion. Without this,
// we might waste a lot of traffic constantly
// retrying message deletion.
return Status::RetryLater;
}
}
}
Message::delete_from_db(context, msg.id);
if msg.chat_id.is_trash() || msg.hidden {
// Messages are stored in trash chat only to keep
// their server UID and Message-ID. Once message is
// deleted from the server, database record can be
// removed as well.
//
// Hidden messages are similar to trashed, but are
// related to some chat. We also delete their
// database records.
job_try!(msg.id.delete_from_db(context))
} else {
// Remove server UID from the database record.
//
// We have either just removed the message from the
// server, in which case UID is not valid anymore, or
// we have more refernces to the same server UID, so
// we remove UID to reduce the number of messages
// pointing to the corresponding UID. Once the counter
// reaches zero, we will remove the message.
job_try!(msg.id.unlink(context));
}
Status::Finished(Ok(()))
} else {
/* eg. device messages have no Message-ID */
@@ -490,43 +641,6 @@ impl Job {
}
}
}
#[allow(non_snake_case)]
fn MarkseenMdnOnImap(&mut self, context: &Context) -> Status {
let folder = self
.param
.get(Param::ServerFolder)
.unwrap_or_default()
.to_string();
let uid = self.param.get_int(Param::ServerUid).unwrap_or_default() as u32;
let imap_inbox = &context.inbox_thread.read().unwrap().imap;
if imap_inbox.set_seen(context, &folder, uid) == ImapActionResult::RetryLater {
return Status::RetryLater;
}
if self.param.get_bool(Param::AlsoMove).unwrap_or_default() {
if let Err(err) = imap_inbox.ensure_configured_folders(context, true) {
warn!(context, "configuring folders failed: {:?}", err);
return Status::RetryLater;
}
let dest_folder = context
.sql
.get_raw_config(context, "configured_mvbox_folder");
if let Some(dest_folder) = dest_folder {
let mut dest_uid = 0;
if ImapActionResult::RetryLater
== imap_inbox.mv(context, &folder, uid, &dest_folder, &mut dest_uid)
{
Status::RetryLater
} else {
Status::Finished(Ok(()))
}
} else {
Status::Finished(Err(format_err!("MVBOX is not configured")))
}
} else {
Status::Finished(Ok(()))
}
}
}
/* delete all pending jobs with the given action */
@@ -792,7 +906,36 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
};
let mimefactory = MimeFactory::from_msg(context, &msg, attach_selfavatar)?;
let mut rendered_msg = mimefactory.render().map_err(|err| {
let mut recipients = mimefactory.recipients();
let from = context
.get_config(Config::ConfiguredAddr)
.unwrap_or_default();
let lowercase_from = from.to_lowercase();
// Send BCC to self if it is enabled and we are not going to
// delete it immediately.
if context.get_config_bool(Config::BccSelf)
&& context.get_config_delete_server_after() != Some(0)
&& !recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
{
recipients.push(from);
}
if recipients.is_empty() {
// may happen eg. for groups with only SELF and bcc_self disabled
info!(
context,
"message {} has no recipient, skipping smtp-send", msg_id
);
set_delivered(context, msg_id);
return Ok(());
}
let rendered_msg = mimefactory.render().map_err(|err| {
message::set_msg_failed(context, msg_id, Some(err.to_string()));
err
})?;
@@ -811,26 +954,6 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
);
}
let lowercase_from = rendered_msg.from.to_lowercase();
if context.get_config_bool(Config::BccSelf)
&& !rendered_msg
.recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
{
rendered_msg.recipients.push(rendered_msg.from.clone());
}
if rendered_msg.recipients.is_empty() {
// may happen eg. for groups with only SELF and bcc_self disabled
info!(
context,
"message {} has no recipient, skipping smtp-send", msg_id
);
set_delivered(context, msg_id);
return Ok(());
}
if rendered_msg.is_gossiped {
chat::set_gossiped_timestamp(context, msg.chat_id, time())?;
}
@@ -849,7 +972,7 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
}
if attach_selfavatar {
if let Err(err) = chat::set_selfavatar_timestamp(context, msg.chat_id, time()) {
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()) {
error!(context, "Failed to set selfavatar timestamp: {:?}", err);
}
}
@@ -859,11 +982,47 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
msg.save_param_to_disk(context);
}
add_smtp_job(context, Action::SendMsgToSmtp, msg.id, &rendered_msg)?;
add_smtp_job(
context,
Action::SendMsgToSmtp,
msg.id,
recipients,
&rendered_msg,
)?;
Ok(())
}
fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
if let Some(delete_server_after) = context.get_config_delete_server_after() {
let threshold_timestamp = time() - delete_server_after;
context.sql.query_row_optional(
"SELECT id FROM msgs \
WHERE timestamp < ? \
AND server_uid != 0",
params![threshold_timestamp],
|row| row.get::<_, MsgId>(0),
)
} else {
Ok(None)
}
}
fn load_imap_deletion_job(context: &Context) -> sql::Result<Option<Job>> {
let res = if let Some(msg_id) = load_imap_deletion_msgid(context)? {
Some(Job::new(
Action::DeleteMsgOnImap,
msg_id.to_u32(),
Params::new(),
0,
))
} else {
None
};
Ok(res)
}
pub fn perform_inbox_jobs(context: &Context) {
info!(context, "dc_perform_inbox_jobs starting.",);
@@ -884,13 +1043,23 @@ pub fn perform_sentbox_jobs(context: &Context) {
}
fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
let mut jobs_loaded = 0;
while let Some(mut job) = load_next_job(context, thread, probe_network) {
jobs_loaded += 1;
if thread == Thread::Imap && jobs_loaded > 20 {
// Let the fetch run, but return back to the job afterwards.
info!(context, "postponing {}-job {} to run fetch...", thread, job);
*context.perform_inbox_jobs_needed.write().unwrap() = true;
break;
}
info!(context, "{}-job {} started...", thread, job);
// some configuration jobs are "exclusive":
// - they are always executed in the imap-thread and the smtp-thread is suspended during execution
// - they may change the database handle change the database handle; we do not keep old pointers therefore
// - they can be re-executed one time AT_ONCE, but they are not save in the database for later execution
// - they may change the database handle; we do not keep old pointers therefore
// - they can be re-executed one time AT_ONCE, but they are not saved in the database for later execution
if Action::ConfigureImap == job.action || Action::ImexImap == job.action {
job_kill_action(context, job.action);
context
@@ -908,52 +1077,10 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
suspend_smtp_thread(context, true);
}
let try_res = (0..2)
.map(|tries| {
info!(
context,
"{} performs immediate try {} of job {}", thread, tries, job
);
let try_res = match job.action {
Action::Unknown => Status::Finished(Err(format_err!("Unknown job id found"))),
Action::SendMsgToSmtp => job.SendMsgToSmtp(context),
Action::EmptyServer => job.EmptyServer(context),
Action::DeleteMsgOnImap => job.DeleteMsgOnImap(context),
Action::MarkseenMsgOnImap => job.MarkseenMsgOnImap(context),
Action::MarkseenMdnOnImap => job.MarkseenMdnOnImap(context),
Action::MoveMsg => job.MoveMsg(context),
Action::SendMdn => job.SendMdn(context),
Action::ConfigureImap => JobConfigureImap(context),
Action::ImexImap => match JobImexImap(context, &job) {
Ok(()) => Status::Finished(Ok(())),
Err(err) => {
error!(context, "{}", err);
Status::Finished(Err(err))
}
},
Action::MaybeSendLocations => location::JobMaybeSendLocations(context, &job),
Action::MaybeSendLocationsEnded => {
location::JobMaybeSendLocationsEnded(context, &mut job)
}
Action::Housekeeping => {
sql::housekeeping(context);
Status::Finished(Ok(()))
}
};
info!(
context,
"{} finished immediate try {} of job {}", thread, tries, job
);
try_res
})
.find(|try_res| match try_res {
Status::RetryNow => false,
_ => true,
})
.unwrap_or(Status::RetryNow);
let try_res = match perform_job_action(context, &mut job, thread, 0) {
Status::RetryNow => perform_job_action(context, &mut job, thread, 1),
x => x,
};
if Action::ConfigureImap == job.action || Action::ImexImap == job.action {
context
@@ -984,7 +1111,6 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
job.tries = tries;
let time_offset = get_backoff_time_offset(tries);
job.desired_timestamp = time() + time_offset;
job.update(context);
info!(
context,
"{}-job #{} not succeeded on try #{}, retry in {} seconds.",
@@ -993,6 +1119,7 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
tries,
time_offset
);
job.save(context);
if thread == Thread::Smtp && tries < JOB_RETRIES - 1 {
context
.smtp_state
@@ -1044,6 +1171,45 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
}
}
fn perform_job_action(context: &Context, mut job: &mut Job, thread: Thread, tries: u32) -> Status {
info!(
context,
"{} begin immediate try {} of job {}", thread, tries, job
);
let try_res = match job.action {
Action::Unknown => Status::Finished(Err(format_err!("Unknown job id found"))),
Action::SendMsgToSmtp => job.SendMsgToSmtp(context),
Action::EmptyServer => job.EmptyServer(context),
Action::OldDeleteMsgOnImap => job.DeleteMsgOnImap(context),
Action::DeleteMsgOnImap => job.DeleteMsgOnImap(context),
Action::MarkseenMsgOnImap => job.MarkseenMsgOnImap(context),
Action::MoveMsg => job.MoveMsg(context),
Action::SendMdn => job.SendMdn(context),
Action::ConfigureImap => JobConfigureImap(context),
Action::ImexImap => match JobImexImap(context, &job) {
Ok(()) => Status::Finished(Ok(())),
Err(err) => {
error!(context, "Import/export failed: {}", err);
Status::Finished(Err(err))
}
},
Action::MaybeSendLocations => location::JobMaybeSendLocations(context, &job),
Action::MaybeSendLocationsEnded => location::JobMaybeSendLocationsEnded(context, &mut job),
Action::Housekeeping => {
sql::housekeeping(context);
Status::Finished(Ok(()))
}
};
info!(
context,
"{} finished immediate try {} of job {}", thread, tries, job
);
try_res
}
fn get_backoff_time_offset(tries: u32) -> i64 {
let n = 2_i32.pow(tries - 1) * 60;
let mut rng = thread_rng();
@@ -1080,17 +1246,15 @@ fn add_smtp_job(
context: &Context,
action: Action,
msg_id: MsgId,
recipients: Vec<String>,
rendered_msg: &RenderedEmail,
) -> Result<()> {
ensure!(
!rendered_msg.recipients.is_empty(),
"no recipients for smtp job set"
);
ensure!(!recipients.is_empty(), "no recipients for smtp job set");
let mut param = Params::new();
let bytes = &rendered_msg.message;
let blob = BlobObject::create(context, &rendered_msg.rfc724_mid, bytes)?;
let recipients = rendered_msg.recipients.join("\x1e");
let recipients = recipients.join("\x1e");
param.set(Param::File, blob.as_name());
param.set(Param::Recipients, &recipients);
@@ -1113,27 +1277,16 @@ pub fn job_add(
return;
}
let timestamp = time();
let thread: Thread = action.into();
let job = Job::new(action, foreign_id as u32, param, delay_seconds);
job.save(context);
sql::execute(
context,
&context.sql,
"INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);",
params![
timestamp,
thread,
action,
foreign_id,
param.to_string(),
(timestamp + delay_seconds as i64)
]
).ok();
match thread {
Thread::Imap => interrupt_inbox_idle(context),
Thread::Smtp => interrupt_smtp_idle(context),
Thread::Unknown => {}
if delay_seconds == 0 {
let thread: Thread = action.into();
match thread {
Thread::Imap => interrupt_inbox_idle(context),
Thread::Smtp => interrupt_smtp_idle(context),
Thread::Unknown => {}
}
}
}
@@ -1153,7 +1306,7 @@ pub fn interrupt_smtp_idle(context: &Context) {
///
/// Load jobs for this "[Thread]", i.e. either load SMTP jobs or load
/// IMAP jobs. The `probe_network` parameter decides how to query
/// jobs, this is tricky and probably wrong currently. Look at the
/// jobs, this is tricky and probably wrong currently. Look at the
/// SQL queries for details.
fn load_next_job(context: &Context, thread: Thread, probe_network: bool) -> Option<Job> {
let query = if !probe_network {
@@ -1177,7 +1330,7 @@ fn load_next_job(context: &Context, thread: Thread, probe_network: bool) -> Opti
params_probe
};
context
let job = context
.sql
.query_map(
query,
@@ -1206,7 +1359,23 @@ fn load_next_job(context: &Context, thread: Thread, probe_network: bool) -> Opti
Ok(None)
},
)
.unwrap_or_default()
.unwrap_or_default();
if thread == Thread::Imap {
if let Some(job) = job {
if job.action < Action::DeleteMsgOnImap {
load_imap_deletion_job(context)
.unwrap_or_default()
.or(Some(job))
} else {
Some(job)
}
} else {
load_imap_deletion_job(context).unwrap_or_default()
}
} else {
job
}
}
#[cfg(test)]

View File

@@ -1,7 +1,7 @@
use std::sync::{Arc, Condvar, Mutex};
use crate::context::Context;
use crate::error::{Error, Result};
use crate::error::{format_err, Result};
use crate::imap::Imap;
#[derive(Debug)]
@@ -99,25 +99,21 @@ impl JobThread {
async fn connect_and_fetch(&mut self, context: &Context) -> Result<()> {
let prefix = format!("{}-fetch", self.name);
match self.imap.connect_configured(context) {
Ok(()) => {
if let Some(watch_folder) = self.get_watch_folder(context) {
let start = std::time::Instant::now();
info!(context, "{} started...", prefix);
let res = self
.imap
.fetch(context, &watch_folder)
.await
.map_err(Into::into);
let elapsed = start.elapsed().as_millis();
info!(context, "{} done in {:.3} ms.", prefix, elapsed);
self.imap.connect_configured(context)?;
if let Some(watch_folder) = self.get_watch_folder(context) {
let start = std::time::Instant::now();
info!(context, "{} started...", prefix);
let res = self
.imap
.fetch(context, &watch_folder)
.await
.map_err(Into::into);
let elapsed = start.elapsed().as_millis();
info!(context, "{} done in {:.3} ms.", prefix, elapsed);
res
} else {
Err(Error::WatchFolderNotFound("not-set".to_string()))
}
}
Err(err) => Err(crate::error::Error::Message(err.to_string())),
res
} else {
Err(format_err!("WatchFolder not found: not-set"))
}
}
@@ -143,7 +139,7 @@ impl JobThread {
if state.jobs_needed {
info!(
context,
"{}-IDLE will not be started as it was interrupted while not ideling.",
"{}-IDLE will not be started as it was interrupted while not idling.",
self.name,
);
state.jobs_needed = false;

View File

@@ -4,14 +4,169 @@ use std::collections::BTreeMap;
use std::io::Cursor;
use std::path::Path;
use pgp::composed::{Deserializable, SignedPublicKey, SignedSecretKey};
use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
use pgp::ser::Serialize;
use pgp::types::{KeyTrait, SecretKeyTrait};
use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::sql::{self, Sql};
use crate::dc_tools::{dc_write_file, time, EmailAddress, InvalidEmailError};
use crate::sql;
// Re-export key types
pub use crate::pgp::KeyPair;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
/// Error type for deltachat key handling.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("Could not decode base64")]
Base64Decode(#[from] base64::DecodeError),
#[error("rPGP error: {}", _0)]
Pgp(#[from] pgp::errors::Error),
#[error("Failed to generate PGP key: {}", _0)]
Keygen(#[from] crate::pgp::PgpKeygenError),
#[error("Failed to load key: {}", _0)]
LoadKey(#[from] sql::Error),
#[error("Failed to save generated key: {}", _0)]
StoreKey(#[from] SaveKeyError),
#[error("No address configured")]
NoConfiguredAddr,
#[error("Configured address is invalid: {}", _0)]
InvalidConfiguredAddr(#[from] InvalidEmailError),
}
pub type Result<T> = std::result::Result<T, Error>;
/// Convenience trait for working with keys.
///
/// This trait is implemented for rPGP's [SignedPublicKey] and
/// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world.
pub trait DcKey: Serialize + Deserializable {
type KeyType: Serialize + Deserializable;
/// Create a key from some bytes.
fn from_slice(bytes: &[u8]) -> Result<Self::KeyType> {
Ok(<Self::KeyType as Deserializable>::from_bytes(Cursor::new(
bytes,
))?)
}
/// Create a key from a base64 string.
fn from_base64(data: &str) -> Result<Self::KeyType> {
// strip newlines and other whitespace
let cleaned: String = data.trim().split_whitespace().collect();
let bytes = base64::decode(cleaned.as_bytes())?;
Self::from_slice(&bytes)
}
/// Load the users' default key from the database.
fn load_self(context: &Context) -> Result<Self::KeyType>;
/// Serialise the key to a base64 string.
fn to_base64(&self) -> String {
// Not using Serialize::to_bytes() to make clear *why* it is
// safe to ignore this error.
// Because we write to a Vec<u8> the io::Write impls never
// fail and we can hide this error.
let mut buf = Vec::new();
self.to_writer(&mut buf).unwrap();
base64::encode(&buf)
}
}
impl DcKey for SignedPublicKey {
type KeyType = SignedPublicKey;
fn load_self(context: &Context) -> Result<Self::KeyType> {
match context.sql.query_row(
r#"
SELECT public_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1;
"#,
params![],
|row| row.get::<_, Vec<u8>>(0),
) {
Ok(bytes) => Self::from_slice(&bytes),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
let keypair = generate_keypair(context)?;
Ok(keypair.public)
}
Err(err) => Err(err.into()),
}
}
}
impl DcKey for SignedSecretKey {
type KeyType = SignedSecretKey;
fn load_self(context: &Context) -> Result<Self::KeyType> {
match context.sql.query_row(
r#"
SELECT private_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1;
"#,
params![],
|row| row.get::<_, Vec<u8>>(0),
) {
Ok(bytes) => Self::from_slice(&bytes),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
let keypair = generate_keypair(context)?;
Ok(keypair.secret)
}
Err(err) => Err(err.into()),
}
}
}
fn generate_keypair(context: &Context) -> Result<KeyPair> {
let addr = context
.get_config(Config::ConfiguredAddr)
.ok_or_else(|| Error::NoConfiguredAddr)?;
let addr = EmailAddress::new(&addr)?;
let _guard = context.generating_key_mutex.lock().unwrap();
// Check if the key appeared while we were waiting on the lock.
match context.sql.query_row(
r#"
SELECT public_key, private_key
FROM keypairs
WHERE addr=?1
AND is_default=1;
"#,
params![addr],
|row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, Vec<u8>>(1)?)),
) {
Ok((pub_bytes, sec_bytes)) => Ok(KeyPair {
addr,
public: SignedPublicKey::from_slice(&pub_bytes)?,
secret: SignedSecretKey::from_slice(&sec_bytes)?,
}),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
let start = std::time::Instant::now();
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType))
.unwrap_or_default();
info!(context, "Generating keypair with type {}", keytype);
let keypair = crate::pgp::create_keypair(addr, keytype)?;
store_self_keypair(context, &keypair, KeyPairUse::Default)?;
info!(
context,
"Keypair generated in {:.3}s.",
start.elapsed().as_secs()
);
Ok(keypair)
}
Err(err) => Err(err.into()),
}
}
/// Cryptographic key
#[derive(Debug, PartialEq, Eq, Clone)]
@@ -35,7 +190,7 @@ impl From<SignedSecretKey> for Key {
impl std::convert::TryFrom<Key> for SignedSecretKey {
type Error = ();
fn try_from(value: Key) -> Result<Self, Self::Error> {
fn try_from(value: Key) -> std::result::Result<Self, Self::Error> {
match value {
Key::Public(_) => Err(()),
Key::Secret(key) => Ok(key),
@@ -46,7 +201,7 @@ impl std::convert::TryFrom<Key> for SignedSecretKey {
impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedSecretKey {
type Error = ();
fn try_from(value: &'a Key) -> Result<Self, Self::Error> {
fn try_from(value: &'a Key) -> std::result::Result<Self, Self::Error> {
match value {
Key::Public(_) => Err(()),
Key::Secret(key) => Ok(key),
@@ -57,7 +212,7 @@ impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedSecretKey {
impl std::convert::TryFrom<Key> for SignedPublicKey {
type Error = ();
fn try_from(value: Key) -> Result<Self, Self::Error> {
fn try_from(value: Key) -> std::result::Result<Self, Self::Error> {
match value {
Key::Public(key) => Ok(key),
Key::Secret(_) => Err(()),
@@ -68,7 +223,7 @@ impl std::convert::TryFrom<Key> for SignedPublicKey {
impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedPublicKey {
type Error = ();
fn try_from(value: &'a Key) -> Result<Self, Self::Error> {
fn try_from(value: &'a Key) -> std::result::Result<Self, Self::Error> {
match value {
Key::Public(key) => Ok(key),
Key::Secret(_) => Err(()),
@@ -92,7 +247,7 @@ impl Key {
if bytes.is_empty() {
return None;
}
let res: Result<Key, _> = match key_type {
let res: std::result::Result<Key, _> = match key_type {
KeyType::Public => SignedPublicKey::from_bytes(Cursor::new(bytes)).map(Into::into),
KeyType::Private => SignedSecretKey::from_bytes(Cursor::new(bytes)).map(Into::into),
};
@@ -111,7 +266,7 @@ impl Key {
key_type: KeyType,
) -> Option<(Self, BTreeMap<String, String>)> {
let bytes = data.as_bytes();
let res: Result<(Key, _), _> = match key_type {
let res: std::result::Result<(Key, _), _> = match key_type {
KeyType::Public => SignedPublicKey::from_armor_single(Cursor::new(bytes))
.map(|(k, h)| (Into::into(k), h)),
KeyType::Private => SignedSecretKey::from_armor_single(Cursor::new(bytes))
@@ -127,43 +282,6 @@ impl Key {
}
}
pub fn from_base64(encoded_data: &str, key_type: KeyType) -> Option<Self> {
// strip newlines and other whitespace
let cleaned: String = encoded_data.trim().split_whitespace().collect();
let bytes = cleaned.as_bytes();
base64::decode(bytes)
.ok()
.and_then(|decoded| Self::from_slice(&decoded, key_type))
}
pub fn from_self_public(
context: &Context,
self_addr: impl AsRef<str>,
sql: &Sql,
) -> Option<Self> {
let addr = self_addr.as_ref();
sql.query_get_value(
context,
"SELECT public_key FROM keypairs WHERE addr=? AND is_default=1;",
&[addr],
)
.and_then(|blob: Vec<u8>| Self::from_slice(&blob, KeyType::Public))
}
pub fn from_self_private(
context: &Context,
self_addr: impl AsRef<str>,
sql: &Sql,
) -> Option<Self> {
sql.query_get_value(
context,
"SELECT private_key FROM keypairs WHERE addr=? AND is_default=1;",
&[self_addr.as_ref()],
)
.and_then(|blob: Vec<u8>| Self::from_slice(&blob, KeyType::Private))
}
pub fn to_bytes(&self) -> Vec<u8> {
match self {
Key::Public(k) => k.to_bytes().unwrap_or_default(),
@@ -242,20 +360,95 @@ impl Key {
}
}
pub fn dc_key_save_self_keypair(
/// Use of a [KeyPair] for encryption or decryption.
///
/// This is used by [store_self_keypair] to know what kind of key is
/// being saved.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum KeyPairUse {
/// The default key used to encrypt new messages.
Default,
/// Only used to decrypt existing message.
ReadOnly,
}
/// Error saving a keypair to the database.
#[derive(Debug, thiserror::Error)]
#[error("SaveKeyError: {message}")]
pub struct SaveKeyError {
message: String,
#[source]
cause: anyhow::Error,
}
impl SaveKeyError {
fn new(message: impl Into<String>, cause: impl Into<anyhow::Error>) -> Self {
Self {
message: message.into(),
cause: cause.into(),
}
}
}
/// Store the keypair as an owned keypair for addr in the database.
///
/// This will save the keypair as keys for the given address. The
/// "self" here refers to the fact that this DC instance owns the
/// keypair. Usually `addr` will be [Config::ConfiguredAddr].
///
/// If either the public or private keys are already present in the
/// database, this entry will be removed first regardless of the
/// address associated with it. Practically this means saving the
/// same key again overwrites it.
///
/// [Config::ConfiguredAddr]: crate::config::Config::ConfiguredAddr
pub fn store_self_keypair(
context: &Context,
public_key: &Key,
private_key: &Key,
addr: impl AsRef<str>,
is_default: bool,
sql: &Sql,
) -> bool {
sql::execute(
context,
sql,
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created) VALUES (?,?,?,?,?);",
params![addr.as_ref(), is_default as i32, public_key.to_bytes(), private_key.to_bytes(), time()],
).is_ok()
keypair: &KeyPair,
default: KeyPairUse,
) -> std::result::Result<(), SaveKeyError> {
// Everything should really be one transaction, more refactoring
// is needed for that.
let public_key = keypair
.public
.to_bytes()
.map_err(|err| SaveKeyError::new("failed to serialise public key", err))?;
let secret_key = keypair
.secret
.to_bytes()
.map_err(|err| SaveKeyError::new("failed to serialise secret key", err))?;
context
.sql
.execute(
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
params![public_key, secret_key],
)
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
if default == KeyPairUse::Default {
context
.sql
.execute("UPDATE keypairs SET is_default=0;", params![])
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
}
let is_default = match default {
KeyPairUse::Default => true,
KeyPairUse::ReadOnly => false,
};
context
.sql
.execute(
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
VALUES (?,?,?,?,?);",
params![
keypair.addr.to_string(),
is_default as i32,
public_key,
secret_key,
time()
],
)
.map(|_| ())
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))
}
/// Make a fingerprint human-readable, in hex format.
@@ -287,6 +480,14 @@ pub fn dc_normalize_fingerprint(fp: &str) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use std::convert::TryFrom;
use lazy_static::lazy_static;
lazy_static! {
static ref KEYPAIR: KeyPair = alice_keypair();
}
#[test]
fn test_normalize_fingerprint() {
@@ -373,9 +574,9 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
}
#[test]
#[ignore] // is too expensive
fn test_from_slice_roundtrip() {
let (public_key, private_key) = crate::pgp::create_keypair("hello").unwrap();
let public_key = Key::from(KEYPAIR.public.clone());
let private_key = Key::from(KEYPAIR.secret.clone());
let binary = public_key.to_bytes();
let public_key2 = Key::from_slice(&binary, KeyType::Public).expect("invalid public key");
@@ -408,9 +609,62 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
}
#[test]
#[ignore] // is too expensive
fn test_load_self_existing() {
let alice = alice_keypair();
let t = dummy_context();
configure_alice_keypair(&t.ctx);
let pubkey = SignedPublicKey::load_self(&t.ctx).unwrap();
assert_eq!(alice.public, pubkey);
let seckey = SignedSecretKey::load_self(&t.ctx).unwrap();
assert_eq!(alice.secret, seckey);
}
#[test]
#[ignore] // generating keys is expensive
fn test_load_self_generate_public() {
let t = dummy_context();
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.unwrap();
let key = SignedPublicKey::load_self(&t.ctx);
assert!(key.is_ok());
}
#[test]
#[ignore] // generating keys is expensive
fn test_load_self_generate_secret() {
let t = dummy_context();
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.unwrap();
let key = SignedSecretKey::load_self(&t.ctx);
assert!(key.is_ok());
}
#[test]
#[ignore] // generating keys is expensive
fn test_load_self_generate_concurrent() {
use std::sync::Arc;
use std::thread;
let t = dummy_context();
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.unwrap();
let ctx = Arc::new(t.ctx);
let ctx0 = Arc::clone(&ctx);
let thr0 = thread::spawn(move || SignedPublicKey::load_self(&ctx0));
let ctx1 = Arc::clone(&ctx);
let thr1 = thread::spawn(move || SignedPublicKey::load_self(&ctx1));
let res0 = thr0.join().unwrap();
let res1 = thr1.join().unwrap();
assert_eq!(res0.unwrap(), res1.unwrap());
}
#[test]
fn test_ascii_roundtrip() {
let (public_key, private_key) = crate::pgp::create_keypair("hello").unwrap();
let public_key = Key::from(KEYPAIR.public.clone());
let private_key = Key::from(KEYPAIR.secret.clone());
let s = public_key.to_armored_string(None).unwrap();
let (public_key2, _) =
@@ -423,4 +677,51 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
Key::from_armored_string(&s, KeyType::Private).expect("invalid private key");
assert_eq!(private_key, private_key2);
}
#[test]
fn test_split_key() {
let private_key = Key::from(KEYPAIR.secret.clone());
let public_wrapped = private_key.split_key().unwrap();
let public = SignedPublicKey::try_from(public_wrapped).unwrap();
assert_eq!(public.primary_key, KEYPAIR.public.primary_key);
}
#[test]
fn test_save_self_key_twice() {
// Saving the same key twice should result in only one row in
// the keypairs table.
let t = dummy_context();
let nrows = || {
t.ctx
.sql
.query_get_value::<_, u32>(&t.ctx, "SELECT COUNT(*) FROM keypairs;", params![])
.unwrap()
};
assert_eq!(nrows(), 0);
store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap();
assert_eq!(nrows(), 1);
store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap();
assert_eq!(nrows(), 1);
}
// Convenient way to create a new key if you need one, run with
// `cargo test key::tests::gen_key`.
// #[test]
// fn gen_key() {
// let name = "fiona";
// let keypair = crate::pgp::create_keypair(
// EmailAddress::new(&format!("{}@example.net", name)).unwrap(),
// )
// .unwrap();
// std::fs::write(
// format!("test-data/key/{}-public.asc", name),
// keypair.public.to_base64(),
// )
// .unwrap();
// std::fs::write(
// format!("test-data/key/{}-secret.asc", name),
// keypair.secret.to_base64(),
// )
// .unwrap();
// }
}

View File

@@ -1,8 +1,8 @@
use std::borrow::Cow;
use crate::constants::*;
use crate::constants::KeyType;
use crate::context::Context;
use crate::key::*;
use crate::key::Key;
use crate::sql::Sql;
#[derive(Default, Clone, Debug)]

View File

@@ -2,8 +2,6 @@
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
#![allow(clippy::match_bool)]
#[macro_use]
extern crate failure_derive;
#[macro_use]
extern crate num_derive;
#[macro_use]
@@ -27,23 +25,22 @@ pub(crate) mod events;
pub use events::*;
mod aheader;
pub mod blob;
mod blob;
pub mod chat;
pub mod chatlist;
pub mod config;
pub mod configure;
mod configure;
pub mod constants;
pub mod contact;
pub mod context;
mod e2ee;
mod imap;
mod imap_client;
pub mod imex;
#[macro_use]
pub mod job;
mod job_thread;
pub mod key;
pub mod keyring;
mod keyring;
pub mod location;
mod login_param;
pub mod lot;
@@ -54,6 +51,7 @@ pub mod oauth2;
mod param;
pub mod peerstate;
pub mod pgp;
pub mod provider;
pub mod qr;
pub mod securejoin;
mod simplify;

View File

@@ -1,7 +1,6 @@
//! Location handling
use bitflags::bitflags;
use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::chat::{self, ChatId};
@@ -9,7 +8,7 @@ use crate::config::Config;
use crate::constants::*;
use crate::context::*;
use crate::dc_tools::*;
use crate::error::Error;
use crate::error::{ensure, Error};
use crate::events::Event;
use crate::job::{self, job_action_exists, job_add, Job};
use crate::message::{Message, MsgId};
@@ -64,11 +63,10 @@ impl Kml {
Default::default()
}
pub fn parse(context: &Context, content: &[u8]) -> Result<Self, Error> {
ensure!(content.len() <= 1024 * 1024, "kml-file is too large");
pub fn parse(context: &Context, to_parse: &[u8]) -> Result<Self, Error> {
ensure!(to_parse.len() <= 1024 * 1024, "kml-file is too large");
let to_parse = String::from_utf8_lossy(content);
let mut reader = quick_xml::Reader::from_str(&to_parse);
let mut reader = quick_xml::Reader::from_reader(to_parse);
reader.trim_text(true);
let mut kml = Kml::new();
@@ -124,9 +122,9 @@ impl Kml {
}
} else if self.tag.contains(KmlTag::COORDINATES) {
let parts = val.splitn(2, ',').collect::<Vec<_>>();
if parts.len() == 2 {
self.curr.longitude = parts[0].parse().unwrap_or_default();
self.curr.latitude = parts[1].parse().unwrap_or_default();
if let [longitude, latitude] = &parts[..] {
self.curr.longitude = longitude.parse().unwrap_or_default();
self.curr.latitude = latitude.parse().unwrap_or_default();
}
}
}
@@ -365,6 +363,7 @@ fn is_marker(txt: &str) -> bool {
txt.len() == 1 && !txt.starts_with(' ')
}
/// Deletes all locations from the database.
pub fn delete_all(context: &Context) -> Result<(), Error> {
sql::execute(context, &context.sql, "DELETE FROM locations;", params![])?;
context.call_cb(Event::LocationChanged(None));
@@ -548,7 +547,7 @@ pub fn save(
}
#[allow(non_snake_case)]
pub fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Status {
pub(crate) fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Status {
let now = time();
let mut continue_streaming = false;
info!(
@@ -639,7 +638,7 @@ pub fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Status {
}
#[allow(non_snake_case)]
pub fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) -> job::Status {
pub(crate) fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) -> job::Status {
// this function is called when location-streaming _might_ have ended for a chat.
// the function checks, if location-streaming is really ended;
// if so, a device-message is added if not yet done.

View File

@@ -258,10 +258,8 @@ fn get_readable_flags(flags: i32) -> String {
res
}
pub fn dc_build_tls(
certificate_checks: CertificateChecks,
) -> Result<native_tls::TlsConnector, native_tls::Error> {
let mut tls_builder = native_tls::TlsConnector::builder();
pub fn dc_build_tls(certificate_checks: CertificateChecks) -> async_native_tls::TlsConnector {
let tls_builder = async_native_tls::TlsConnector::new();
match certificate_checks {
CertificateChecks::Automatic => {
// Same as AcceptInvalidCertificates for now.
@@ -270,13 +268,12 @@ pub fn dc_build_tls(
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true)
}
CertificateChecks::Strict => &mut tls_builder,
CertificateChecks::Strict => tls_builder,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => tls_builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true),
}
.build()
}
#[cfg(test)]

View File

@@ -40,11 +40,11 @@ impl Lot {
}
pub fn get_text1(&self) -> Option<&str> {
self.text1.as_ref().map(|s| s.as_str())
self.text1.as_deref()
}
pub fn get_text2(&self) -> Option<&str> {
self.text2.as_ref().map(|s| s.as_str())
self.text2.as_deref()
}
pub fn get_text1_meaning(&self) -> Meaning {
@@ -86,6 +86,9 @@ pub enum LotState {
/// test1=formatted fingerprint
QrFprWithoutAddr = 230,
/// text1=domain
QrAccount = 250,
/// id=contact
QrAddr = 320,

View File

@@ -3,14 +3,15 @@
use std::path::{Path, PathBuf};
use deltachat_derive::{FromSql, ToSql};
use failure::Fail;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use crate::chat::{self, Chat, ChatId};
use crate::constants::*;
use crate::contact::*;
use crate::context::*;
use crate::dc_tools::*;
use crate::error::Error;
use crate::error::{ensure, Error};
use crate::events::Event;
use crate::job::*;
use crate::lot::{Lot, LotState, Meaning};
@@ -20,6 +21,10 @@ use crate::pgp::*;
use crate::sql;
use crate::stock::StockMessage;
lazy_static! {
static ref UNWRAP_RE: regex::Regex = regex::Regex::new(r"\s+").unwrap();
}
// In practice, the user additionally cuts the string themselves
// pixel-accurate.
const SUMMARY_CHARACTERS: usize = 160;
@@ -29,7 +34,9 @@ const SUMMARY_CHARACTERS: usize = 160;
/// Some message IDs are reserved to identify special message types.
/// This type can represent both the special as well as normal
/// messages.
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
#[derive(
Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
)]
pub struct MsgId(u32);
impl MsgId {
@@ -47,10 +54,7 @@ impl MsgId {
///
/// This kind of message ID can not be used for real messages.
pub fn is_special(self) -> bool {
match self.0 {
0..=DC_MSG_ID_LAST_SPECIAL => true,
_ => false,
}
self.0 <= DC_MSG_ID_LAST_SPECIAL
}
/// Whether the message ID is unset.
@@ -80,6 +84,55 @@ impl MsgId {
self.0 == DC_MSG_ID_DAYMARKER
}
/// Put message into trash chat and delete message text.
///
/// It means the message is deleted locally, but not on the server
/// yet.
pub fn trash(self, context: &Context) -> crate::sql::Result<()> {
let chat_id = ChatId::new(DC_CHAT_ID_TRASH);
sql::execute(
context,
&context.sql,
"UPDATE msgs SET chat_id=?, txt='', txt_raw='' WHERE id=?",
params![chat_id, self],
)
}
/// Deletes a message and corresponding MDNs from the database.
pub fn delete_from_db(self, context: &Context) -> crate::sql::Result<()> {
// We don't use transactions yet, so remove MDNs first to make
// sure they are not left while the message is deleted.
sql::execute(
context,
&context.sql,
"DELETE FROM msgs_mdns WHERE msg_id=?;",
params![self],
)?;
sql::execute(
context,
&context.sql,
"DELETE FROM msgs WHERE id=?;",
params![self],
)?;
Ok(())
}
/// Removes IMAP server UID and folder from the database record.
///
/// It is used to avoid trying to remove the message from the
/// server multiple times when there are multiple message records
/// pointing to the same server UID.
pub(crate) fn unlink(self, context: &Context) -> sql::Result<()> {
sql::execute(
context,
&context.sql,
"UPDATE msgs \
SET server_folder='', server_uid=0 \
WHERE id=?",
params![self],
)
}
/// Bad evil escape hatch.
///
/// Avoid using this, eventually types should be cleaned up enough
@@ -116,7 +169,7 @@ impl rusqlite::types::ToSql for MsgId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
if self.0 <= DC_MSG_ID_LAST_SPECIAL {
return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
InvalidMsgId.compat(),
InvalidMsgId,
)));
}
let val = rusqlite::types::Value::Integer(self.0 as i64);
@@ -144,13 +197,24 @@ impl rusqlite::types::FromSql for MsgId {
/// This usually occurs when trying to use a message ID of
/// [DC_MSG_ID_LAST_SPECIAL] or below in a situation where this is not
/// possible.
#[derive(Debug, Fail)]
#[fail(display = "Invalid Message ID.")]
#[derive(Debug, thiserror::Error)]
#[error("Invalid Message ID.")]
pub struct InvalidMsgId;
#[derive(Debug, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[derive(
Debug,
Copy,
Clone,
PartialEq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
)]
#[repr(u8)]
pub enum MessengerMessage {
pub(crate) enum MessengerMessage {
No = 0,
Yes = 1,
@@ -171,7 +235,7 @@ impl Default for MessengerMessage {
/// to check if a mail was sent, use dc_msg_is_sent()
/// approx. max. length returned by dc_msg_get_text()
/// approx. max. length returned by dc_get_msg_info()
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Message {
pub(crate) id: MsgId,
pub(crate) from_id: u32,
@@ -289,25 +353,6 @@ impl Message {
.map_err(Into::into)
}
pub fn delete_from_db(context: &Context, msg_id: MsgId) {
if let Ok(msg) = Message::load_from_db(context, msg_id) {
sql::execute(
context,
&context.sql,
"DELETE FROM msgs WHERE id=?;",
params![msg.id],
)
.ok();
sql::execute(
context,
&context.sql,
"DELETE FROM msgs_mdns WHERE msg_id=?;",
params![msg.id],
)
.ok();
}
}
pub fn get_filemime(&self) -> Option<String> {
if let Some(m) = self.param.get(Param::MimeType) {
return Some(m.to_string());
@@ -427,7 +472,7 @@ impl Message {
pub fn get_text(&self) -> Option<String> {
self.text
.as_ref()
.map(|text| dc_truncate(text, 30000, false).to_string())
.map(|text| dc_truncate(text, 30000).to_string())
}
pub fn get_filename(&self) -> Option<String> {
@@ -610,7 +655,19 @@ impl Message {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
ToSql,
FromSql,
Serialize,
Deserialize,
)]
#[repr(i32)]
pub enum MessageState {
Undefined = 0,
@@ -786,7 +843,7 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
return ret;
}
let rawtxt = rawtxt.unwrap_or_default();
let rawtxt = dc_truncate(rawtxt.trim(), 100_000, false);
let rawtxt = dc_truncate(rawtxt.trim(), 100_000);
let fts = dc_timestamp_to_str(msg.get_timestamp());
ret += &format!("Sent: {}", fts);
@@ -923,13 +980,15 @@ pub fn get_mime_headers(context: &Context, msg_id: MsgId) -> Option<String> {
}
pub fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
for msg_id in msg_ids.iter() {
for msg_id in msg_ids {
if let Ok(msg) = Message::load_from_db(context, *msg_id) {
if msg.location_id > 0 {
delete_poi_location(context, msg.location_id);
}
}
update_msg_chat_id(context, *msg_id, ChatId::new(DC_CHAT_ID_TRASH));
if let Err(err) = msg_id.trash(context) {
error!(context, "Unable to trash message {}: {}", msg_id, err);
}
job_add(
context,
Action::DeleteMsgOnImap,
@@ -949,16 +1008,6 @@ pub fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
};
}
fn update_msg_chat_id(context: &Context, msg_id: MsgId, chat_id: ChatId) -> bool {
sql::execute(
context,
&context.sql,
"UPDATE msgs SET chat_id=? WHERE id=?;",
params![chat_id, msg_id],
)
.is_ok()
}
fn delete_poi_location(context: &Context, location_id: u32) -> bool {
sql::execute(
context,
@@ -1118,18 +1167,20 @@ pub fn get_summarytext_by_raw(
return prefix;
}
if let Some(text) = text {
let summary = if let Some(text) = text {
if text.as_ref().is_empty() {
prefix
} else if prefix.is_empty() {
dc_truncate(text.as_ref(), approx_characters, true).to_string()
dc_truncate(text.as_ref(), approx_characters).to_string()
} else {
let tmp = format!("{} {}", prefix, text.as_ref());
dc_truncate(&tmp, approx_characters, true).to_string()
dc_truncate(&tmp, approx_characters).to_string()
}
} else {
prefix
}
};
UNWRAP_RE.replace_all(&summary, " ").to_string()
}
// as we do not cut inside words, this results in about 32-42 characters.
@@ -1164,7 +1215,7 @@ pub fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option<impl AsRef
}
if let Some(error) = error {
msg.param.set(Param::Error, error.as_ref());
error!(context, "{}", error.as_ref());
warn!(context, "Message failed: {}", error.as_ref());
}
if sql::execute(
@@ -1317,10 +1368,55 @@ pub fn get_deaddrop_msg_cnt(context: &Context) -> usize {
}
}
pub fn estimate_deletion_cnt(
context: &Context,
from_server: bool,
seconds: i64,
) -> Result<usize, Error> {
let self_chat_id = chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
.unwrap_or_default()
.0;
let threshold_timestamp = time() - seconds;
let cnt: isize = if from_server {
context.sql.query_row(
"SELECT COUNT(*)
FROM msgs m
WHERE m.id > ?
AND timestamp < ?
AND chat_id != ?
AND server_uid != 0;",
params![DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id],
|row| row.get(0),
)?
} else {
context.sql.query_row(
"SELECT COUNT(*)
FROM msgs m
WHERE m.id > ?
AND timestamp < ?
AND chat_id != ?
AND chat_id != ? AND hidden = 0;",
params![
DC_MSG_ID_LAST_SPECIAL,
threshold_timestamp,
self_chat_id,
ChatId::new(DC_CHAT_ID_TRASH)
],
|row| row.get(0),
)?
};
Ok(cnt as usize)
}
/// Counts number of database records pointing to specified
/// Message-ID.
///
/// Unlinked messages are excluded.
pub fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> i32 {
// check the number of messages with the same rfc724_mid
match context.sql.query_row(
"SELECT COUNT(*) FROM msgs WHERE rfc724_mid=?;",
"SELECT COUNT(*) FROM msgs WHERE rfc724_mid=? AND NOT server_uid = 0",
&[rfc724_mid],
|row| row.get(0),
) {
@@ -1335,12 +1431,15 @@ pub fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> i32 {
pub(crate) fn rfc724_mid_exists(
context: &Context,
rfc724_mid: &str,
) -> Result<(String, u32, MsgId), Error> {
ensure!(!rfc724_mid.is_empty(), "empty rfc724_mid");
) -> Result<Option<(String, u32, MsgId)>, Error> {
if rfc724_mid.is_empty() {
warn!(context, "Empty rfc724_mid passed to rfc724_mid_exists");
return Ok(None);
}
context
.sql
.query_row(
.query_row_optional(
"SELECT server_folder, server_uid, id FROM msgs WHERE rfc724_mid=?",
&[rfc724_mid],
|row| {
@@ -1361,7 +1460,8 @@ pub fn update_server_uid(
server_uid: u32,
) {
match context.sql.execute(
"UPDATE msgs SET server_folder=?, server_uid=? WHERE rfc724_mid=?;",
"UPDATE msgs SET server_folder=?, server_uid=? \
WHERE rfc724_mid=?",
params![server_folder.as_ref(), server_uid, rfc724_mid],
) {
Ok(_) => {}

View File

@@ -9,14 +9,22 @@ use crate::contact::*;
use crate::context::{get_version_str, Context};
use crate::dc_tools::*;
use crate::e2ee::*;
use crate::error::Error;
use crate::error::{bail, ensure, format_err, Error};
use crate::location;
use crate::message::{self, Message};
use crate::mimeparser::SystemMessage;
use crate::param::*;
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::simplify::escape_message_footer_marks;
use crate::stock::StockMessage;
// attachments of 25 mb brutto should work on the majority of providers
// (brutto examples: web.de=50, 1&1=40, t-online.de=32, gmail=25, posteo=50, yahoo=25, all-inkl=100).
// as an upper limit, we double the size; the core won't send messages larger than this
// to get the netto sizes, we subtract 1 mb header-overhead and the base64-overhead.
pub const RECOMMENDED_FILE_SIZE: u64 = 24 * 1024 * 1024 / 4 * 3;
const UPPER_LIMIT_FILE_SIZE: u64 = 49 * 1024 * 1024 / 4 * 3;
#[derive(Debug, Clone)]
pub enum Loaded {
Message { chat: Chat },
@@ -53,9 +61,6 @@ pub struct RenderedEmail {
pub is_gossiped: bool,
pub last_added_location_id: u32,
pub from: String,
pub recipients: Vec<String>,
/// Message ID (Message in the sense of Email)
pub rfc724_mid: String,
}
@@ -64,7 +69,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
pub fn from_msg(
context: &'a Context,
msg: &'b Message,
add_selfavatar: bool,
attach_selfavatar: bool,
) -> Result<MimeFactory<'a, 'b>, Error> {
let chat = Chat::load_from_db(context, msg.chat_id)?;
@@ -102,21 +107,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let command = msg.param.get_cmd();
/* for added members, the list is just fine */
if command == SystemMessage::MemberRemovedFromGroup {
let email_to_remove = msg.param.get(Param::Arg).unwrap_or_default();
let self_addr = context
.get_config(Config::ConfiguredAddr)
.unwrap_or_default();
if !email_to_remove.is_empty()
&& !addr_cmp(email_to_remove, self_addr)
&& !recipients_contain_addr(&recipients, &email_to_remove)
{
recipients.push(("".to_string(), email_to_remove.to_string()));
}
}
if command != SystemMessage::AutocryptSetupMessage
&& command != SystemMessage::SecurejoinMessage
&& context.get_config_bool(Config::MdnsEnabled)
@@ -152,7 +142,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
references,
req_mdn,
last_added_location_id: 0,
attach_selfavatar: add_selfavatar,
attach_selfavatar,
context,
};
Ok(factory)
@@ -342,6 +332,13 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
}
pub fn recipients(&self) -> Vec<String> {
self.recipients
.iter()
.map(|(_, addr)| addr.clone())
.collect()
}
pub fn render(mut self) -> Result<RenderedEmail, Error> {
// Headers that are encrypted
// - Chat-*, except Chat-Version
@@ -357,7 +354,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
self.from_addr.clone(),
);
let mut to = Vec::with_capacity(self.recipients.len());
let mut to = Vec::new();
for (name, addr) in self.recipients.iter() {
if name.is_empty() {
to.push(Address::new_mailbox(addr.clone()));
@@ -369,6 +366,10 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
}
if to.is_empty() {
to.push(from.clone());
}
if !self.references.is_empty() {
unprotected_headers.push(Header::new("References".into(), self.references.clone()));
}
@@ -558,8 +559,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
};
let MimeFactory {
recipients,
from_addr,
last_added_location_id,
..
} = self;
@@ -570,12 +569,50 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
is_encrypted,
is_gossiped,
last_added_location_id,
recipients: recipients.into_iter().map(|(_, addr)| addr).collect(),
from: from_addr,
rfc724_mid,
})
}
fn get_message_kml_part(&self) -> Option<PartBuilder> {
let latitude = self.msg.param.get_float(Param::SetLatitude)?;
let longitude = self.msg.param.get_float(Param::SetLongitude)?;
let kml_file = location::get_message_kml(self.msg.timestamp_sort, latitude, longitude);
let part = PartBuilder::new()
.content_type(
&"application/vnd.google-earth.kml+xml"
.parse::<mime::Mime>()
.unwrap(),
)
.header((
"Content-Disposition",
"attachment; filename=\"message.kml\"",
))
.body(kml_file);
Some(part)
}
fn get_location_kml_part(&mut self) -> Result<PartBuilder, Error> {
let (kml_content, last_added_location_id) =
location::get_kml(self.context, self.msg.chat_id)?;
let part = PartBuilder::new()
.content_type(
&"application/vnd.google-earth.kml+xml"
.parse::<mime::Mime>()
.unwrap(),
)
.header((
"Content-Disposition",
"attachment; filename=\"location.kml\"",
))
.body(kml_content);
if !self.msg.param.exists(Param::SetLatitude) {
// otherwise, the independent location is already filed
self.last_added_location_id = last_added_location_id;
}
Ok(part)
}
#[allow(clippy::cognitive_complexity)]
fn render_message(
&mut self,
@@ -591,7 +628,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let command = self.msg.param.get_cmd();
let mut placeholdertext = None;
let mut meta_part = None;
let mut add_compatibility_header = false;
if chat.typ == Chattype::VerifiedGroup {
protected_headers.push(Header::new("Chat-Verified".to_string(), "1".to_string()));
@@ -634,7 +670,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
"vg-member-added".to_string(),
));
}
add_compatibility_header = true;
}
SystemMessage::GroupNameChanged => {
let value_to_add = self.msg.param.get(Param::Arg).unwrap_or_default();
@@ -655,7 +690,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
"0".to_string(),
));
}
add_compatibility_header = true;
}
_ => {}
}
@@ -723,18 +757,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let (mail, filename_as_sent) = build_body_file(context, &meta, "group-image")?;
meta_part = Some(mail);
protected_headers.push(Header::new(
"Chat-Group-Avatar".into(),
filename_as_sent.clone(),
));
// add the old group-image headers for versions <=0.973 resp. <=beta.15 (december 2019)
// image deletion is not supported in the compatibility layer.
// this can be removed some time after releasing 1.0,
// grep for #DeprecatedAvatar to get the place where compatibility parsing takes place.
if add_compatibility_header {
protected_headers.push(Header::new("Chat-Group-Image".into(), filename_as_sent));
}
protected_headers.push(Header::new("Chat-Group-Avatar".into(), filename_as_sent));
}
if self.msg.viewtype == Viewtype::Sticker {
@@ -785,7 +808,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let message_text = format!(
"{}{}{}{}{}",
fwdhint.unwrap_or_default(),
&final_text,
escape_message_footer_marks(final_text),
if !final_text.is_empty() && !footer.is_empty() {
"\r\n\r\n"
} else {
@@ -796,16 +819,17 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
);
// Message is sent as text/plain, with charset = utf-8
let mut parts = vec![PartBuilder::new()
let main_part = PartBuilder::new()
.content_type(&mime::TEXT_PLAIN_UTF_8)
.body(message_text)];
.body(message_text);
let mut parts = Vec::new();
// add attachment part
if chat::msgtype_has_file(self.msg.viewtype) {
if !is_file_size_okay(context, &self.msg) {
bail!(
"Message exceeds the recommended {} MB.",
24 * 1024 * 1024 / 4 * 3 / 1000 / 1000,
RECOMMENDED_FILE_SIZE / 1_000_000,
);
} else {
let (file_part, _) = build_body_file(context, &self.msg, "")?;
@@ -817,58 +841,22 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
parts.push(meta_part);
}
if self.msg.param.exists(Param::SetLatitude) {
let param = &self.msg.param;
let kml_file = location::get_message_kml(
self.msg.timestamp_sort,
param.get_float(Param::SetLatitude).unwrap_or_default(),
param.get_float(Param::SetLongitude).unwrap_or_default(),
);
parts.push(
PartBuilder::new()
.content_type(
&"application/vnd.google-earth.kml+xml"
.parse::<mime::Mime>()
.unwrap(),
)
.header((
"Content-Disposition",
"attachment; filename=\"message.kml\"",
))
.body(kml_file),
);
if let Some(msg_kml_part) = self.get_message_kml_part() {
parts.push(msg_kml_part);
}
if location::is_sending_locations_to_chat(context, self.msg.chat_id) {
match location::get_kml(context, self.msg.chat_id) {
Ok((kml_content, last_added_location_id)) => {
parts.push(
PartBuilder::new()
.content_type(
&"application/vnd.google-earth.kml+xml"
.parse::<mime::Mime>()
.unwrap(),
)
.header((
"Content-Disposition",
"attachment; filename=\"location.kml\"",
))
.body(kml_content),
);
if !self.msg.param.exists(Param::SetLatitude) {
// otherwise, the independent location is already filed
self.last_added_location_id = last_added_location_id;
}
}
match self.get_location_kml_part() {
Ok(part) => parts.push(part),
Err(err) => {
warn!(context, "mimefactory: could not get location: {}", err);
warn!(context, "mimefactory: could not send location: {}", err);
}
}
}
if self.attach_selfavatar {
match context.get_config(Config::Selfavatar) {
Some(path) => match build_selfavatar_file(context, path) {
Some(path) => match build_selfavatar_file(context, &path) {
Ok((part, filename)) => {
parts.push(part);
protected_headers.push(Header::new("Chat-User-Avatar".into(), filename))
@@ -879,18 +867,18 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
}
// Single part, render as regular message.
if parts.len() == 1 {
return Ok(parts.pop().unwrap());
if parts.is_empty() {
// Single part, render as regular message.
Ok(main_part)
} else {
// Multiple parts, render as multipart.
let mut message = PartBuilder::new().message_type(MimeMultipartType::Mixed);
message = message.child(main_part.build());
for part in parts.into_iter() {
message = message.child(part.build());
}
Ok(message)
}
// Multiple parts, render as multipart.
let mut message = PartBuilder::new().message_type(MimeMultipartType::Mixed);
for part in parts.into_iter() {
message = message.child(part.build());
}
Ok(message)
}
/// Render an MDN
@@ -1063,7 +1051,7 @@ fn build_body_file(
Ok((mail, filename_to_send))
}
fn build_selfavatar_file(context: &Context, path: String) -> Result<(PartBuilder, String), Error> {
fn build_selfavatar_file(context: &Context, path: &str) -> Result<(PartBuilder, String), Error> {
let blob = BlobObject::from_path(context, path)?;
let filename_to_send = match blob.suffix() {
Some(suffix) => format!("avatar.{}", suffix),
@@ -1099,7 +1087,7 @@ fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
match msg.param.get_path(Param::File, context).unwrap_or(None) {
Some(path) => {
let bytes = dc_get_filebytes(context, &path);
bytes <= (49 * 1024 * 1024 / 4 * 3)
bytes <= UPPER_LIMIT_FILE_SIZE
}
None => false,
}
@@ -1108,7 +1096,7 @@ fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
fn render_rfc724_mid(rfc724_mid: &str) -> String {
let rfc724_mid = rfc724_mid.trim().to_string();
if rfc724_mid.chars().nth(0).unwrap_or_default() == '<' {
if rfc724_mid.chars().next().unwrap_or_default() == '<' {
rfc724_mid
} else {
format!("<{}>", rfc724_mid)

File diff suppressed because it is too large Load Diff

View File

@@ -311,9 +311,15 @@ impl Oauth2 {
return None;
}
if let Ok(response) = parsed {
// serde_json::Value.as_str() removes the quotes of json-strings
// CAVE: serde_json::Value.as_str() removes the quotes of json-strings
// but serde_json::Value.to_string() does not!
if let Some(addr) = response.get("email") {
Some(addr.to_string())
if let Some(s) = addr.as_str() {
Some(s.to_string())
} else {
warn!(context, "E-mail in userinfo is not a string: {}", addr);
None
}
} else {
warn!(context, "E-mail missing in userinfo.");
None

Some files were not shown because too many files have changed in this diff Show More