Compare commits

...

236 Commits

Author SHA1 Message Date
holger krekel
ff34ed6054 try add a little testing of imex events 2020-10-19 18:48:55 +02:00
Hocuri
8aaea09011 Add progress for backup import/export 2020-10-19 15:38:25 +02:00
Hocuri
c005f756d6 Do not allow to save drafts in non-writeable chats, fix #1986 (#1997) 2020-10-19 13:15:48 +02:00
Hocuri
3c6d52842e Doc comments are show in HTML documentation.
This is not a proper documentation, just a note on implementation.
2020-10-19 13:07:55 +02:00
Hocuri
4d2542cee5 Don't show HTML if there is no content and there is a file attached
Fix https://github.com/deltachat/deltachat-core-rust/issues/1982
2020-10-19 13:07:55 +02:00
B. Petersen
9bf8799484 fix processing of protection-changed messages
by #2001, processing of protection-enabled and protection-disabled
messages is only done if verification-checks pass.

unfortunately, these verification checks
were only applied on already protected chats,
so that enabling via a protection-enabled message was not possible.

this is fixed by this pr.
moreover, when inner_set_protection() fails,
the error is shown in the chat.
2020-10-19 13:03:56 +02:00
B. Petersen
e15372531e skip protection enabled/disabled messages where they did not appear in the past with verified-groups
this avoids confusion - as long the current core
does not communicate with a UI that enables/disables protection,
everything looks just the same as in the past :)
2020-10-19 13:02:24 +02:00
link2xt
0ae9443e22 Update sanitize-filename (#2013) 2020-10-19 12:14:05 +02:00
Alexander Krotov
67cddedf7e Switch from lazy_static to once_cell 2020-10-18 15:47:21 +03:00
bjoern
bf72ae4ccc Merge pull request #2007 from deltachat/tweak-protection-msg
do not say, a concrete user has enabled protection if we do not really know
2020-10-17 22:20:36 +02:00
B. Petersen
c7863c67bf refine test for add_info_msg() and add_info_msg_with_cmd() 2020-10-17 22:02:55 +02:00
B. Petersen
3d108fedc4 set message-info-type also for unpromoted protect/unprotect messages so that UI can show a shield-icon or sth. line that 2020-10-17 21:31:09 +02:00
B. Petersen
546e8dedce allow adding info-message with defined commands, add tests for that and for set_protection() 2020-10-17 21:27:41 +02:00
Alexander Krotov
64cd48a4e1 Fix nightly clippy warnings 2020-10-17 15:24:09 +03:00
bjoern
2e118b773e Merge pull request #2009 from deltachat/fix-link
fix link to Protected Headers draft
2020-10-17 13:34:23 +02:00
B. Petersen
6206c82ee5 fix link to Protected Headers draft 2020-10-17 13:27:16 +02:00
B. Petersen
e7736138a8 clarify 'promote' parameter in add_protection_msg() 2020-10-17 11:52:56 +02:00
B. Petersen
78b44cb4d0 do not say a concrete user has enabled protection if we do not really know 2020-10-17 11:46:46 +02:00
bjoern
0a9a2394d8 Merge pull request #2001 from deltachat/protect-one-to-one
check protection properties for all chats, allow missing Chat-Verified header
2020-10-16 23:02:41 +02:00
bjoern
6e37c1442e Merge pull request #2006 from deltachat/update-provider-db-2020-10-16-ii
update provider-db, second time today
2020-10-16 22:58:28 +02:00
B. Petersen
c323798386 update provider-db, second time today 2020-10-16 21:43:18 +02:00
Hocuri
b8e98c0b81 Also add missing suffix (#1973) 2020-10-16 18:42:21 +02:00
B. Petersen
7a82fd4bbd update provider-db
ran `./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs`
to pull in fetch_existing setting for nauta.
2020-10-16 16:19:07 +02:00
B. Petersen
0a300da347 rename option 'Prefetch' to 'FetchExisting' and limit it to that 2020-10-16 15:28:41 +02:00
B. Petersen
aa26c52813 add prefetch config value 2020-10-16 15:28:41 +02:00
Alexander Krotov
19697e255e Parse normal MUA messages consisting of only a quote 2020-10-16 14:57:02 +02:00
Hocuri
07e4762f71 Call update_device_chats automatically during configure (#1957)
update_device_chats() takes about 2 seconds on a modern device (Android) because the
welcome image file has to be written to the disk as a blob. The problem
was that this was done after the progress bar had vanished and before
anything else happened so that I thought that something had gone wrong
multiple times.

The UIs have to remove update_device_chats(), too..
2020-10-15 17:43:22 +02:00
B. Petersen
6ec743f8b1 make clippy happy 2020-10-15 16:29:21 +02:00
B. Petersen
010be693e1 check protected properties for all chats
before, checks were done for groups only.
moreover, apply protection-changes only when the check passes.
2020-10-15 16:04:43 +02:00
B. Petersen
d8a7a178c2 skip check for Chat-Verified on normal messages
this would exclude non-deltas from protected-chats
(where they could participate in verified-groups in the past)
and makes migration a bit harder when using a fuzzy multi-device-setup.

however, the test still takes place
when a group is created or protection is enabled/disabled.
2020-10-15 15:19:25 +02:00
B. Petersen
18e9073bfe add unit test for should_encrypt() 2020-10-15 06:45:57 +02:00
B. Petersen
0032468a87 remove now inaccurate comment 2020-10-15 06:45:57 +02:00
B. Petersen
7e793a518c priorize e2ee_guaranteed over Reset
should_encrypt() shall return Ok(false)
on peerstate=EncryptPreference::Reset
only for opportunistic groups.

for verified/protected groups (e2ee_guaranteed set),
Ok(true) or Error() should be returned.

this bug was introduced by #1946 (Require quorum to enable encryption).
2020-10-15 06:45:57 +02:00
bjoern
e5b0194e8c Merge pull request #1987 from deltachat/name-quote-only-drafts
name quote-only drafts as such in the summary
2020-10-14 23:23:03 +02:00
holger krekel
13055b9c87 use new imap-proto/async-imap 0.4.1 to fix #1834 2020-10-14 15:41:23 +03:00
jikstra
5661e0b8f1 Add test to verify exporting and importing the secret key works 2020-10-14 06:46:25 +02:00
Alexander Krotov
1672905c71 Remove outdated tests/stress.rs
All useful code has already been moved to relevant modules.
2020-10-14 05:58:47 +02:00
B. Petersen
d13d62105a name quote-only drafts as such in the summary
a draft may contain only a quote,
without any text set yet.

these drafts cannot be sent, however, appear in the summary -
currently with the summary-text "", which results to sth.
as "Draft: " - which looks like an error or at least a bit weird.

this pr sets the summary text to "Reply" - similar to "Image", "Video" etc. -
the UI just expects some text here, not an empty string.

the result are summaries as "Draft: Reply" on all UIs -
which, btw. is also roughly the same what Signal does in this case.
2020-10-13 21:35:29 +02:00
bjoern
0b80b81129 Merge pull request #1991 from deltachat/remove-DC_STR_COUNT
remove DC_STR_COUNT
2020-10-13 21:32:48 +02:00
B. Petersen
9b72aba8e3 remove DC_STR_COUNT
the constant comes from c-core
and was used to define the size of the string-array that time.

this is no longer needed in core
and also the UIs seems not to use it
(they will treat DC_STR* like an id,
there should be no need to know the max. DC_STR* value)
2020-10-13 18:18:34 +02:00
Alexander Krotov
1f24c5f8a4 Merge "protected groups" branch into master 2020-10-13 18:44:31 +03:00
B. Petersen
7f882a6406 less duplicate code on calling inner_set_protection() 2020-10-13 14:59:29 +02:00
B. Petersen
50f3af58f8 remove dc_chat_is_verified() 2020-10-13 14:59:28 +02:00
B. Petersen
8425e23d82 move get_protection_msg() to stock.rs as stock_protection_msg() 2020-10-13 14:59:28 +02:00
B. Petersen
9fc6bbf41f tweak error texts 2020-10-13 14:59:28 +02:00
B. Petersen
1e2e042244 clarify that signature of add_protection_msg() takes chat-promoted parameter 2020-10-13 14:59:28 +02:00
B. Petersen
03d86360d6 details docstring, thanks @flub 2020-10-13 14:59:28 +02:00
B. Petersen
4eb8d3fef6 tweak documentation towards https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text - thanks @flub 2020-10-13 14:59:28 +02:00
B. Petersen
da727740ab use warn! for ffi-usage-errors 2020-10-13 14:59:28 +02:00
bjoern
3a993a4b77 Update src/chat.rs
Co-authored-by: Floris Bruynooghe <flub@devork.be>
2020-10-13 14:59:27 +02:00
B. Petersen
45dae1ff0c protect against attackers dropping the protect-this-chat message by not showing unprotected messages directly; this is done by checking the Chat-Verified flag on each incoming message. moreover, make sure, the flag is signed+encrypted (it must be read from the protected headers). 2020-10-13 14:59:27 +02:00
B. Petersen
f144426bf5 use 'unreachable' instead of 'panic' 2020-10-13 14:59:27 +02:00
B. Petersen
e447bdc0c3 simplify code-path 2020-10-13 14:59:27 +02:00
B. Petersen
c1768bb311 add dc_msg_get_info_type() to allow drawing a shield in the uis 2020-10-13 14:59:27 +02:00
B. Petersen
66cb3d4358 fix ffi and bindings, error is already logged in core 2020-10-13 14:59:27 +02:00
B. Petersen
47f4f2bd08 use language of receiver for protection-messages, show correct sender 2020-10-13 14:59:26 +02:00
B. Petersen
12cf89735c handling incoming protection-changes messages, always add info-msg 2020-10-13 14:59:26 +02:00
B. Petersen
d240bbcd07 if a message is replaced by an error, this also removes special is_system_message states 2020-10-13 14:59:26 +02:00
B. Petersen
5e07a36cd2 add/send info-message on protection changes 2020-10-13 14:59:26 +02:00
B. Petersen
49b5962568 add set_chat_protection() api 2020-10-13 14:59:26 +02:00
B. Petersen
a7998c190c remove DC_CHAT_TYPE_VERIFIED_GROUP resp. Chattype::VerifiedGroup 2020-10-13 14:59:26 +02:00
B. Petersen
b8a55f3aa4 replace chat.is_verified() by chat.is_protected() 2020-10-13 14:59:25 +02:00
B. Petersen
ab8bf3c2f3 use ProtectionStatus to create chats 2020-10-13 14:59:25 +02:00
B. Petersen
d05dd977d9 migrate database
add 'protected' row in chats table,
convert old verified-groups to 'protected'
2020-10-13 14:59:25 +02:00
Alexander Krotov
8b3494b5c1 Do not display [...] after non-chat quotes
Top quotes are displayed as quotes for non-chat mails, so [...] used to
indicate there was a quote is not needed.
2020-10-12 21:55:22 +03:00
bjoern
d9a45eb931 Merge pull request #1981 from deltachat/notice-on-answer
basic DC_EVENT_MSGS_NOTICED multi-device-support
2020-10-12 17:58:32 +02:00
Alexander Krotov
cb5bcebf75 Separate quote from reply with an empty line
This makes quotes easier to read in previous DC versions and plaintext
MUAs.
2020-10-11 02:28:41 +03:00
Alexander Krotov
69f159792e Only use summary for quote if text is empty
For text messages, summary joins all lines into one, so multi-line quotes
look bad in Thunderbird.
2020-10-11 01:37:24 +03:00
Hocuri
bb50b9abe4 Show more errors (#1967) 2020-10-10 18:19:31 +02:00
holger krekel
48e1f53826 fix recovering offline/lost connection situations 2020-10-10 17:44:12 +03:00
Hocuri
be88b946b6 Peek reipients, fetch existing messages
Read all of an e-mail accounts messages and extract all To/CC addresses
if the From was from our own account.
Then, fetch existing messages from the server and show them.

Also, I fixed two other things:
- just by chance my test failed because of an completely unrelated bug.
The bug: bcc_self messages were not marked as read if mvbox_move was set
to true.
- add some color to the test output (minor change)
2020-10-10 15:56:30 +03:00
B. Petersen
c2b222e6a5 emit DC_EVENT_MSGS_NOTICED when a chat was answered on another device 2020-10-10 12:50:23 +02:00
Hocuri
cf5342c367 Allow drafts without text if there is a quote 2020-10-10 12:41:43 +03:00
Hocuri
990ab739cc Save quote as draft 2020-10-10 12:41:43 +03:00
Alexander Krotov
eaec03142b Use get_summarytext() for quotes
Co-Authored-By: Hocuri <hocuri@gmx.de>
2020-10-10 12:41:43 +03:00
Hocuri
ea731a3619 Code style 2020-10-10 12:41:43 +03:00
Hocuri
719cba68b3 Don't flood the log
with `src/message.rs:1794: Empty rfc724_mid passed to rfc724_mid_exists`
messages
2020-10-10 12:41:43 +03:00
Alexander Krotov
20182b027e Add quote API
Sticky encryption rule, requiring that all replies to encrypted messages
are encrypted, applies only to messages with a quote now.

Co-Authored-By: B. Petersen <r10s@b44t.com>
2020-10-10 12:41:43 +03:00
Alexander Krotov
8c82a5cbfa prepare_msg_raw: do not set GuaranteeE2ee
This code is inconsistent with EncryptHelper::should_encrypt, which uses
quorum rule.

UI does not display or use encryption state in-between preparing and
sending message anyway.

In addition, "sticky encryption" rule, which required all replies to
encrypted messages to be encrypted, is dropped. It is going to be
restored with the introduction of quoting.
2020-10-10 12:41:43 +03:00
Alexander Krotov
25274f13c3 cargo fmt
Also formatted SQL query, because rustfmt can't format statements with
too long strings.
2020-10-10 12:41:43 +03:00
Alexander Krotov
093839c2b0 prepare_msg_raw: replace large if with early exit 2020-10-10 12:41:43 +03:00
B. Petersen
4c8e6ef495 use combined index (state, hidden, chat_id) to speed up marknoticed_chat() 2020-10-10 00:32:45 +02:00
René Rössler
2fe600f885 Merge pull request #1971 from deltachat/accessible-msg-error
Accessible msg error and type changes
2020-10-08 12:54:20 +02:00
René Rössler
9739c0305b Accessible msg error and type changes 2020-10-08 11:51:04 +02:00
bjoern
893e4b91ba Merge pull request #1970 from deltachat/push_unconditional
if we merge to master, we always upload -- before if a flaky functional test failed it would prevent uploading of docs and wheels.
2020-10-07 15:03:02 +02:00
holger krekel
5cb1d10401 if we merge to master, we always upload -- before if a flaky functional test failed it would prevent uploading of docs and wheels. 2020-10-07 14:45:23 +02:00
B. Petersen
11107d5484 add comment about unused 'starred' column 2020-10-06 09:32:50 +02:00
B. Petersen
5405bfbc8d remove unused types "starred" and "in-creation"
both are not used in productive:
- chat-id "in-creation" was dropped completely already in core-c
  as it does not allow assigning messages to chats in time.
- message flag "starred" was added at some point and never used.
  ux-wise, "disappearing messages" promises to clean up a chat -
  this does not fit well with "starring" messages.
  "saved messages" chat seems to be superior in that regard.
  but anyway, it is not used but comes at maintainance costs,
  so it is better to delete its functionality for now.
  (the db entries, however, are left for now,
  that might be more tricky to remove)
2020-10-06 09:32:50 +02:00
Alexander Krotov
a0c92753a9 prepare_msg: return Err if no address is configured
Address is used to generate Message-Id.

Previously error toast was shown, but MsgId was returned from
chat::prepare_msg anyway. Now chat::prepare_msg returns an error.

error!() is removed, because FFI library shows errors via
.unwrap_or_log_default().
2020-10-05 10:37:21 +03:00
Alexander Krotov
de97e0263f ffi: add missing "ignoring careless call" warnings 2020-10-05 08:57:52 +02:00
Hocuri
44558d0ce8 Revert "Export to the new backup format (#1952)" (#1958)
This reverts commit 1fdb697c09.

because Desktop never had a release with tar-import
2020-10-02 15:34:22 +02:00
Alexander Krotov
be40417a7f sql: do not set dbversion after each migration
This variable is not used afterwards, and it is already not set in
migrations added after version 54.
2020-10-02 16:20:39 +03:00
Alexander Krotov
8301e27f86 dc_get_info: accept immutable context 2020-10-02 16:20:32 +03:00
Alexander Krotov
21b18836ca test_configure_error_msgs: do not check for "password" repetition
When run against host that is not in the provider database or has multiple
IMAP servers listed, this test fails because one error is returned for
each server.
2020-10-02 14:28:10 +03:00
Hocuri
2e3352afca fix #1953 by cargo update (esp. update async-io to 1.1.6) 2020-10-02 12:39:27 +02:00
Alexander Krotov
9667859410 deltachat.h: fix a typo 2020-10-02 03:33:04 +03:00
Alexander Krotov
b437ab86d1 Fix a typo: "probram" 2020-10-02 03:24:24 +03:00
Hocuri
1fdb697c09 Export to the new backup format (#1952) 2020-10-01 19:25:02 +02:00
Jikstra
7200e62375 Mention in dc_event_get_data2_str method that it can also return 0 (#1955)
* Mention in dc_event_get_data2_str method that it can also return 0

* Update deltachat-ffi/deltachat.h

Co-authored-by: bjoern <r10s@b44t.com>

Co-authored-by: bjoern <r10s@b44t.com>
2020-10-01 13:37:04 +02:00
Friedel Ziegelmayer
7ddf3ba754 Merge pull request #1950 from deltachat/update-async-std
update async-std to 1.6.5
2020-09-29 14:00:33 +02:00
Friedel Ziegelmayer
7786a4ced4 fix: avoid manual poll impl for accounts events 2020-09-29 14:00:10 +02:00
dignifiedquire
c649db15b6 update async-std to 1.6.5
making sure latest bug fixes are in
2020-09-28 21:40:49 +02:00
Alexander Krotov
60a8b47ad0 e2ee: require quorum to enable encryption
Previously, standard Autocrypt rule was used to determine whether
encryption is enabled: if at least one recipient does not prefer
encryption, encryption is disabled.

This rule has been problematic in large groups, because the larger
is the group, the higher is the chance that one of the users does not
prefer encryption.

New rule requires a majority of users to prefer encryption. Note that it
does not affect 1:1 chats, because it is required that *strictly* more
than a half users in a chat prefer encryption.
2020-09-27 23:38:06 +03:00
Alexander Krotov
0344bc387c Add encryption preference test 2020-09-27 23:38:06 +03:00
bjoern
0f1798ae50 Merge pull request #1948 from deltachat/clarify-str-unref
clarify dc_str_unref()
2020-09-26 23:12:24 +02:00
Hocuri
02baf4b1f0 Show some inner errors (do not hide them with .context) (#1916) 2020-09-26 22:48:47 +02:00
B. Petersen
9fb2c59b6e clarify dc_str_unref() 2020-09-26 18:01:41 +02:00
Alexander Krotov
9121e30600 param: escape newlines in values
Newlines are escaped by repeating them.

This encoding is compatible to existing values, because they do not
contain newlines.
2020-09-26 07:01:39 +03:00
bjoern
39d8cffe18 Merge pull request #1943 from deltachat/fix-doxygen-warnings
fix doxygen warnings
2020-09-26 01:36:07 +02:00
B. Petersen
9486c67904 remove obsolete TCL_SUBST from Doxyfile
fix warnings as
Tag 'TCL_SUBST' at line N of file 'Doxyfile' has become obsolete.
2020-09-25 23:27:20 +02:00
B. Petersen
29c4bbab2b do not use '@return' on void functions
partly, we wrote '@return None.' for void functions,
however, some linter complain about that (recently swift)
and they're right, it does not make much sense.
2020-09-25 23:27:20 +02:00
bjoern
9a80385278 Merge pull request #1942 from deltachat/add-msgs-seen-event
split DC_EVENT_MSGS_NOTICED off DC_EVENT_MSGS_CHANGED, remove dc_marknoticed_all_chats()
2020-09-25 21:41:10 +02:00
B. Petersen
f0fb1bfdcb make clippy happy 2020-09-24 14:36:04 +02:00
B. Petersen
ab90b6b390 emit multiple events if messages given to dc_markseen_msgs() belong to different chats 2020-09-24 14:05:34 +02:00
B. Petersen
e9733e7525 always set chat_id on DC_EVENT_MSGS_NOTICED 2020-09-24 12:21:18 +02:00
B. Petersen
f3c7d2f9c6 remove unused function dc_marknoticed_all_chats() 2020-09-24 11:36:40 +02:00
B. Petersen
b5e1b1a2d2 test for DC_EVENT_MSGS_NOTICED 2020-09-24 11:36:40 +02:00
B. Petersen
5c1b69c3c5 if possible, set chat_id in DC_EVENT_MSGS_NOTICED even emitted by dc_markseen_msgs() 2020-09-24 11:36:40 +02:00
B. Petersen
12bc364e42 split DC_EVENT_MSGS_NOTICED off DC_EVENT_MSGS_CHANGED
the new event can be used for updating the badge counter.
to get the old behaviour, implementations can just do the same on both events.
2020-09-24 11:36:39 +02:00
Hocuri
879bd7e35e Improve sentbox name guessing 2020-09-24 10:25:32 +02:00
bjoern
81b0b24114 Merge pull request #1940 from deltachat/update-provider-db-2020-09-22
update provider-database
2020-09-23 14:11:49 +02:00
B. Petersen
2095962466 update provider-database
executed `python3 src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs`
2020-09-22 10:35:04 +02:00
Friedel Ziegelmayer
0c03024b97 feat: update dependencies
* feat: update dependencies

updates

- pgp
- async-std
- surf
- mailparse

* simplify dev deps

* more deps

* fixup

* fixup
2020-09-21 23:53:53 +02:00
bjoern
cd990039bd Merge pull request #1911 from deltachat/fix_error_asm
Tune down error event on failed dc_continue_key_transfer to a warn
2020-09-21 18:27:40 +02:00
bjoern
184f303b54 Merge pull request #1938 from deltachat/summary-regexp
get_summarytext_by_raw: drop leading and trailing whitespace
2020-09-21 18:26:45 +02:00
Hocuri
637d2661e8 Show better errors when configuring (#1917)
* Show all errors when configuring

* Shorten some overly long msgs
2020-09-21 15:06:33 +02:00
B. Petersen
987eaae0c1 tweak ephemeral ffi documentation
add some crosslinks, clarify when the timer is started,
and avoid mixing "ephemeral" with "delete old messages".
2020-09-21 00:40:21 +03:00
Alexander Krotov
fc0e88539a get_summarytext_by_raw: drop leading and trailing whitespace
Also remove unnecessary use of regexp.
2020-09-20 23:51:52 +03:00
Alexander Krotov
c124eadf9d Emit chat modification event on contact rename 2020-09-20 00:45:36 +03:00
B. Petersen
423c0dc808 fix doc for DC_EVENT_CONTACTS_CHANGED 2020-09-19 23:00:57 +03:00
Alexander Krotov
97b1a1c392 Set contact ID in event related to contact blocking 2020-09-19 22:01:45 +03:00
Alexander Krotov
fe1c99c5e8 Set contact ID in ContactsChanged on modification 2020-09-19 22:01:45 +03:00
Alexander Krotov
332a387c98 Fix nightly clippy warnings 2020-09-19 17:49:32 +03:00
Alexander Krotov
92b304dee4 Fix nightly warnings about unused attributes 2020-09-19 16:08:45 +03:00
bjoern
92abae0b5b Merge pull request #1901 from deltachat/validate-system-date
add device-message on bad system clock or on outdated app
2020-09-19 13:41:17 +02:00
bjoern
81db6e3ee2 Merge pull request #1927 from deltachat/newacc-transaction
sql: create new accounts in one transaction
2020-09-19 13:22:28 +02:00
B. Petersen
af67e798fb warn about outdated app 2020-09-19 12:40:03 +02:00
B. Petersen
4090120041 make sure, new added device messages are always at the end of the chat 2020-09-19 12:40:02 +02:00
B. Petersen
49f07421ec add a test that checks maybe_warn_on_bad_time() adds a new message only every day 2020-09-19 12:40:02 +02:00
B. Petersen
7b38d6693d add a device-message if the system clock seems to be inaccurate 2020-09-19 12:40:02 +02:00
B. Petersen
277bbfaead add a function to get the timestamp of the last provider-db update 2020-09-19 12:40:02 +02:00
bjoern
f8d7242079 Merge pull request #1933 from deltachat/macos
ci: disable macOS
2020-09-19 11:51:38 +02:00
Alexander Krotov
498880d874 ci: disable macOS 2020-09-19 03:19:48 +03:00
Alexander Krotov
4573e6d18b ci: set huponexit for inner bash processes
Followup for aae8163696
2020-09-18 23:42:32 +02:00
holger krekel
a26c43e9fd use per build-job CARGO_TARGET_DIR 2020-09-19 00:39:30 +03:00
Alexander Krotov
238c4bb792 sql: set PRAGMA temp_store=memory
On Android, there is to /tmp directory, so SQLite may throw
SQLITE_IOERR_GETTEMPPATH when trying to find a place for temporary
files. Storing temporary files in memory is a workaround that always
works (until out of memory).

See https://github.com/mozilla/mentat/issues/505 for discussion of a
similar issue.
2020-09-19 00:10:12 +03:00
Alexander Krotov
efcdb45301 ci: use different target dirs for Python and Rust tests
This prevents CI jobs from locking each other.
2020-09-18 20:42:27 +03:00
Alexander Krotov
0485c55718 sql: create new accounts in one transaction
This prevents SQLite from synchronizing to disk after each statement,
saving time on high latency HDDs.
2020-09-18 19:41:41 +03:00
bjoern
5742360e3e Merge pull request #1915 from deltachat/resultify-sqy-open
Resultify sql.open()
2020-09-17 23:05:58 +02:00
Hocuri
99a36e8629 rustfmt 2020-09-17 17:29:29 +02:00
Hocuri
6253a2cef7 . 2020-09-17 12:27:57 +02:00
Hocuri
aee6eb2261 {:#} once more 2020-09-17 09:39:14 +02:00
Hocuri
6d6ac66f4d Show dbfile when opening fails 2020-09-16 17:24:04 +02:00
Hocuri
4ed2638594 Also show the inner error 2020-09-16 16:02:25 +02:00
Hocuri
b892dafa49 Clippy 2020-09-16 15:17:56 +02:00
Hocuri
e870b33e03 Revert "Just for testing, let importing the db always fail - .context() just overwrites the underlying error!!!!!"
This reverts commit 27e53ddbff.
2020-09-16 15:05:05 +02:00
Hocuri
27e53ddbff Just for testing, let importing the db always fail - .context() just overwrites the underlying error!!!!! 2020-09-16 15:00:36 +02:00
Hocuri
396ccebb5c Log more 2020-09-16 15:00:22 +02:00
Hocuri
f9cc3cbef0 Resultify sql.open() 2020-09-16 13:45:32 +02:00
Alexander Krotov
0a5d1e5551 sql: create new accounts without migration
This speeds up account creation by ~50% on HDD.
2020-09-15 02:03:58 +03:00
Alexander Krotov
49c8964aec Add benchmark for account creation 2020-09-15 02:03:58 +03:00
jikstra
ec5ca4464b Tune down error event on failed dc_continue_key_transfer to a warn 2020-09-14 15:54:51 +02:00
bjoern
c3f9f473ac Merge pull request #1907 from deltachat/prep-1.46
prepare 1.46
2020-09-13 15:26:32 +02:00
B. Petersen
b0d68ce09e bump version to 1.46 2020-09-13 14:35:17 +02:00
B. Petersen
191de6c445 update changelog for 1.46 2020-09-13 14:33:21 +02:00
bjoern
6ebdbe7dd6 Merge pull request #1908 from deltachat/ehlo-127
Update to non-release version of async-smtp
2020-09-13 14:26:50 +02:00
Alexander Krotov
b42b1ad99b Make dc_accounts_get_all return accounts sorted
HashMap may rearrange all accounts after each insertion and deletion,
making it unpredictable where the new account appears.
2020-09-13 14:36:59 +03:00
Alexander Krotov
b28f5c8716 Update to non-release version of async-smtp
It is one commit ahead 0.3.4, replacing EHLO localhost with EHLO [127.0.0.1].
2020-09-13 13:04:23 +03:00
bjoern
3ce244b048 Merge pull request #1906 from deltachat/imap-progress
configure: add progress! calls during IMAP configuration
2020-09-12 21:37:24 +02:00
Alexander Krotov
53c47bd862 configure: add progress! calls during IMAP configuration 2020-09-12 20:14:58 +03:00
Alexander Krotov
d6a0763b1d Teach Python bindings to process (char *)0 2020-09-12 19:42:41 +03:00
Alexander Krotov
ecbc83390e Add "Configuration failed" stock string 2020-09-12 19:42:41 +03:00
Alexander Krotov
f5b16cf086 Set data2 in ConfigureProgress event
For now it is only set on error, but could contain user-readable log
messages in the future.
2020-09-12 19:42:41 +03:00
Alexander Krotov
cdba74a027 configure: add expand_param_vector function 2020-09-12 15:19:45 +03:00
Alexander Krotov
a065f654e8 Fix a typo in deltachat.h 2020-09-12 14:14:57 +03:00
Floris Bruynooghe
2a254c51fa Remove the Bob::status field and BobStatus enum
This field is entirely unused.
2020-09-11 18:40:36 +02:00
Floris Bruynooghe
428dbfb537 Resultify join_securejoin
This gets rid of ChatId::new(0) usage and is generally a nice first
refactoing step.  The complexity of cleanup() unravels nicely.
2020-09-11 18:38:51 +02:00
Alexander Krotov
b0bb0214c0 Transpose if branches
This removes three ifs and adds two ifs, making it more clear that
nothing is done if there is no Autocrypt header.
2020-09-09 21:46:45 +03:00
Alexander Krotov
f7897d5f1a e2ee: add test for encrypted message without Autocrypt header 2020-09-09 21:46:45 +03:00
Alexander Krotov
42c5bbcda3 Do not reset peerstate on encrypted messages
If message does not contain Autocrypt header, but is encrypted, do not
change the peerstate.
2020-09-09 21:46:45 +03:00
Alexander Krotov
f657b2950c Split ForcePlaintext param into two booleans
This allows to send encrypted messages without Autocrypt header.
2020-09-09 21:46:45 +03:00
Alexander Krotov
6fcc589655 Use ForcePlaintext as enum, not i32 2020-09-09 21:46:45 +03:00
Floris Bruynooghe
a7178f4f25 Hack to fix group chat creation race condition
In the current design the dc_receive_imf() pipeline calls
handle_securejoin_handshake() before it creates the group.  However
handle_securejoin_handshake() already signals to securejoin() that the
chat exists, which is not true.

The proper fix would be to re-desing how group-creation works,
potentially allowinng handle_securejoin_handshake() to already create
the group and no longer require any further processing by
dc_receive_imf().
2020-09-07 18:53:05 +02:00
Floris Bruynooghe
ce01e1652f Add happy path test for secure-join 2020-09-07 18:53:05 +02:00
B. Petersen
c6339c4ae4 remove python bindings and tests for unused dc_empty_server() 2020-09-07 06:38:48 +03:00
B. Petersen
65f2a3b1c6 remove unused dc_empty_server() and related code
'Delete emails from server' was an experimental ad-hoc function
that was added before 'Automatically delete old messages' was introduced
to allow at least some way to cleanup.

'Delete emails from server'-UI was already removed from android/ios in june
(see https://github.com/deltachat/deltachat-android/pull/1406)
and the function never exists on desktop at all.
2020-09-07 06:38:48 +03:00
B. Petersen
87c6f7d42b remove unused defines from ffi, these parts do not have an implementation since some time 2020-09-07 06:38:48 +03:00
Floris Bruynooghe
cd925624a7 Add some initial tests for securejoin
This currently tests the setup-contact full flow and the shortcut
flow.  More securejoin tests are needed but this puts some
infrastructure in place to start writing these.
2020-09-05 22:59:35 +02:00
Alexander Krotov
cdd1ccb458 Ignore reordered autocrypt headers
This commit fixes condition which ignores reordered autocrypt messages.
If a plaintext message resetting peerstate has been received, autocrypt
header should only be applied if it has higher timestamp.

Previously, timestamp of the last received autocrypt header was used,
which may be lower than last_seen timestamp.

# Conflicts:
#	src/peerstate.rs
2020-09-05 21:29:54 +03:00
Alexander Krotov
e388e4cc1e Do not emit network errors during configuration
Previously DC_EVENT_ERROR_NETWORK events were emitted for each failed
attempt during autoconfiguration, even if eventually configuration
succeeded. Such error reports are not useful and often confusing,
especially if they report failures to connect to domains that don't
exist, such as imap.example.org when mail.example.org should be used.

Now DC_EVENT_ERROR_NETWORK is only emitted when attempting to connect
with existing IMAP and SMTP configuration already stored in the
database.

Configuration failure is still indicated by DC_EVENT_CONFIGURE_PROGRESS
with data1 set to 0. Python tests from TestOnlineConfigurationFails
group are changed to only wait for this event.
2020-09-05 21:17:26 +03:00
Alexander Krotov
9b741825ef Attempt IMAP and SMTP configuration in parallel
SMTP configurations are tested in a separate async task.
2020-09-05 21:09:47 +03:00
Alexander Krotov
f4e0c6b5f1 Remove Peerstate::new()
Create and return immutable Peerstate instead.
2020-09-05 21:07:58 +03:00
Alexander Krotov
a68528479f Remove dead code markers 2020-09-05 21:07:28 +03:00
Alexander Krotov
383c5ba7fd Smtp.send: join recipients list without .collect 2020-09-05 21:06:32 +03:00
Floris Bruynooghe
ee27c7d9d4 Run clippy on tests and examples 2020-09-05 18:13:16 +02:00
bjoern
11b9a933b0 Merge pull request #1881 from deltachat/fix-bottleneck
add an index to significantly speed up get_fresh_msg_cnt()
2020-09-02 23:10:23 +02:00
bjoern
8d9fa233c5 Update src/chat.rs
Co-authored-by: Asiel Díaz Benítez <asieldbenitez@gmail.com>
2020-09-02 21:36:12 +02:00
Floris Bruynooghe
edcad6f5d5 Use PartialEq on SecureJoinStep 2020-09-02 21:23:16 +02:00
Floris Bruynooghe
23eb3c40ca Turn Bob::expects into an enum and add docs
This turns the Bob::expects field into an enum, removing the old
constants.  It also makes the field private, nothi0ng outside the
securejoin module uses it.

Finally it documents stuff, including a seemingly-unrelated Param.
But that param is used by the securejoin module... It's nice to have
doc tooltips be helpful.
2020-09-02 21:23:16 +02:00
B. Petersen
8727e0acf8 add an index to significantly speed up get_fresh_msg_cnt() 2020-09-02 17:06:46 +02:00
Alexander Krotov
dd682e87db imap: resultify Imap.connect 2020-08-30 15:04:07 +03:00
Alexander Krotov
0b743c6bc3 imap: use anyhow for error handling
There is already free-form Error::Other error type, and SqlError had
incorrect error description (a copy from InTeardown).
2020-08-30 15:04:07 +03:00
Alexander Krotov
8f290530fd Use enum type for Bob status 2020-08-30 15:03:43 +03:00
Alexander Krotov
0f164861c7 Fix clippy::iter_next_slice errors 2020-08-28 02:13:23 +03:00
Alexander Krotov
4481ab18f5 configure: try multiple servers for each protocol
LoginParamNew structure, which contained possible IMAP and SMTP
configurations to try is replaced with uniform vectors of ServerParams
structures. These vectors are initialized from provider database, online
Mozilla or Outlook XML configuration or user entered parameters.

During configuration, vectors of ServerParams are expanded to replace
unknown values with all possible variants, which are tried one by one
until configuration succeeds or all variants for a particular protocol
(IMAP or SMTP) are exhausted.

ServerParams structure is moved into configure submodule, and all
dependencies on it outside of this submodule are removed.
2020-08-27 23:11:25 +03:00
Hocuri
927c7eb59d Fix cancelling imex (#1855)
Fix deltachat/deltachat-android#1579

Also: Make sure that if an error happens, the UI can show the error to the user
2020-08-27 15:02:00 +02:00
Alexander Krotov
6eef4066db Do not restrict message.kml coordinates precision
Using only two digits results in visible difference between original
location and location received by other users.
2020-08-27 11:19:08 +03:00
holger krekel
df8e5f6088 fix python packaging tty allocation 2020-08-23 23:24:25 +02:00
Alexander Krotov
1cb4e41883 auto_mozilla: use match to parse socket types 2020-08-23 22:15:06 +03:00
Alexander Krotov
cbfada3e4a Implement Default and FromStr for MozConfigTag 2020-08-23 22:15:06 +03:00
Alexander Krotov
c19e35b68d Parse multiple servers in Mozilla autoconfig
Co-Authored-By: Simon Laux <mobile.info@simonlaux.de>
2020-08-23 22:15:06 +03:00
B. Petersen
94e52b5598 use Viewtype::File for types that may be unsupported on some systems.
in general, Viewtypes other than File should be used only
when the added file type is tested on all platforms -
including Android4 currently.

as this is easily overseen,
i've added a comment.

this partly reverts Viewtype changes done by
https://github.com/deltachat/deltachat-core-rust/pull/1818
2020-08-23 19:53:58 +03:00
Alexander Krotov
f6854fd22f Add python test for contact renaming 2020-08-23 16:44:13 +03:00
bjoern
3fc03fb5ca Merge pull request #1859 from deltachat/cancel_build
try harder to let all build processes die when ssh dies
2020-08-23 15:37:07 +02:00
Alexander Krotov
42bd9f71f0 imap: store ServerLoginParam instead of its fields
This prevents errors when copying it field-by-field.
2020-08-23 16:31:26 +03:00
Alexander Krotov
064d62e758 Imap.connect: copy security setting 2020-08-23 16:31:26 +03:00
bjoern
5a2f0d07a0 Merge pull request #1857 from deltachat/mime-pdf
Guess MIME type for .pdf and many other extensions
2020-08-23 11:12:32 +02:00
holger krekel
fd54b6b5b1 another try 2020-08-23 08:37:38 +02:00
holger krekel
aae8163696 try harder to let all build processes die when ssh dies 2020-08-23 08:08:28 +02:00
Alexander Krotov
a01755b65b Guess MIME type for .pdf and many other extensions 2020-08-23 01:20:32 +03:00
Alexander Krotov
6763dd653e Do not override mime type set by the user 2020-08-23 01:20:32 +03:00
Hocuri
0fc57bdb35 Separate IMAP and SMTP configuration
Co-Authored-By: link2xt <ilabdsf@gmail.com>
Co-Authored-By: bjoern <r10s@b44t.com>
2020-08-22 21:29:39 +03:00
Alexander Krotov
4bd2a9084c Fix a typo 2020-08-22 17:29:38 +03:00
Alexander Krotov
0816e6d0f6 Warn if IMAP deletion is scheduled for message without UID 2020-08-22 14:46:06 +03:00
Alexander Krotov
c7d72d64cc Schedule resync on UID validity change 2020-08-22 14:46:06 +03:00
Alexander Krotov
e33f6c1c85 Schedule resync job when DeleteServerAfter option is set 2020-08-22 14:46:06 +03:00
Alexander Krotov
b4c85c534d Add a job to resync folder UIDs 2020-08-22 14:46:06 +03:00
Alexander Krotov
763334d0aa Sort message replies after parent message 2020-08-22 13:57:29 +03:00
Alexander Krotov
be922eef0f Format plain text as Format=Flowed DelSp=No
This avoids triggering spam filters which require that lines are wrapped
to 78 characters.
2020-08-22 13:57:10 +03:00
Hocuri
1325b2f7c6 Fix #1791 Receive group system messages from blocked users (#1823)
Fix #1791 and show all group messages if the user already is in the group, even if the sender is blocked
Also fix a comment
Co-authored-by: link2xt <ilabdsf@gmail.com>
2020-08-21 11:57:37 +02:00
Hocuri
b9ca7b8ace Remove newlines from group names, chat names and the displayname (#1845) 2020-08-20 09:05:08 +02:00
Hocuri
3faf968b7c Fix tests 2020-08-19 20:03:08 +02:00
Hocuri
1a736ca6c3 Fix #1804: remove <!doctype html> and accept invalid HTML
This fixes #1804 in two ways: First, it removes a <!doctype html> from
the start of the mail, if there is any.

Then, it parses the html itself it quick-xml fails, just stripping
everything between < and >.

Both of these would have fixed this specific issue.

Also, add tests for both fixes.
2020-08-19 20:03:08 +02:00
holger krekel
f1ec1a0765 try use SCCACHE 2020-08-19 16:08:00 +02:00
holger krekel
fc2367894b try to reinstate remote_tests_rust 2020-08-19 16:08:00 +02:00
bjoern
a0293de397 Merge pull request #1848 from deltachat/prep-1.45
prepare 1.45
2020-08-18 22:02:20 +02:00
81 changed files with 7923 additions and 4147 deletions

View File

@@ -138,6 +138,12 @@ jobs:
- py-docs
- wheelhouse
remote_tests_rust:
machine: true
steps:
- checkout
- run: ci_scripts/remote_tests_rust.sh
remote_tests_python:
machine: true
steps:
@@ -172,14 +178,17 @@ workflows:
jobs:
# - cargo_fetch
- remote_tests_rust:
filters:
tags:
only: /.*/
- remote_tests_python:
filters:
tags:
only: /.*/
- remote_python_packaging:
requires:
- remote_tests_python
filters:
branches:
only: master

View File

@@ -38,7 +38,7 @@ jobs:
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all
args: --workspace --tests --examples
build_and_test:
@@ -47,7 +47,9 @@ jobs:
continue-on-error: ${{ matrix.experimental }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
# macOS disabled due to random failures related to caching
#os: [ubuntu-latest, windows-latest, macOS-latest]
os: [ubuntu-latest, windows-latest]
rust: [1.45.0]
experimental: [false]
# include:

View File

@@ -1,5 +1,52 @@
# Changelog
## Unreleased
- breaking change: `dc_update_device_chats()` was removed. This is now done automatically during configure.
- Added a `bot` config. Currently, it only prevents filling the device chats automatically.
## 1.46.0
- breaking change: `dc_configure()` report errors in
`DC_EVENT_CONFIGURE_PROGRESS`: capturing error events is no longer working
#1886 #1905
- breaking change: removed `DC_LP_{IMAP|SMTP}_SOCKET*` from `server_flags`;
added `mail_security` and `send_security` using `DC_SOCKET` enum #1835
- parse multiple servers in Mozilla autoconfig #1860
- try multiple servers for each protocol #1871
- do IMAP and SMTP configuration in parallel #1891
- configuration cleanup and speedup #1858 #1875 #1889 #1904 #1906
- secure-join cleanup, testing, fixing #1876 #1877 #1887 #1888 #1896 #1899 #1900
- do not reset peerstate on encrypted messages,
ignore reordered autocrypt headers #1885 #1890
- always sort message replies after parent message #1852
- add an index to significantly speed up `get_fresh_msg_cnt()` #1881
- improve mimetype guessing for PDF and many other formats #1857 #1861
- improve accepting invalid html #1851
- improve tests, cleanup and ci #1850 #1856 #1859 #1861 #1884 #1894 #1895
- tweak HELO command #1908
- make `dc_accounts_get_all()` return accounts sorted #1909
- fix KML coordinates precision used for location streaming #1872
- fix cancelling import/export #1855
## 1.45.0
- add `dc_accounts_t` account manager object and related api functions #1784

1135
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.45.0"
version = "1.46.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.6.0", default-features = false }
pgp = { version = "0.7.0", default-features = false }
hex = "0.4.0"
sha2 = "0.9.0"
rand = "0.7.0"
@@ -20,12 +20,12 @@ smallvec = "1.0.0"
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
num-derive = "0.3.0"
num-traits = "0.2.6"
async-smtp = "0.3"
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
async-imap = "0.4.0"
async-native-tls = { version = "0.3.3" }
async-std = { version = "1.6.1", features = ["unstable"] }
async-std = { version = "1.6.4", features = ["unstable"] }
base64 = "0.12"
charset = "0.1"
percent-encoding = "2.0"
@@ -34,22 +34,22 @@ serde_json = "1.0"
chrono = "0.4.6"
indexmap = "1.3.0"
kamadak-exif = "0.5"
lazy_static = "1.4.0"
once_cell = "1.4.1"
regex = "1.1.6"
rusqlite = { version = "0.23", features = ["bundled"] }
r2d2_sqlite = "0.16.0"
rusqlite = { version = "0.24", features = ["bundled"] }
r2d2_sqlite = "0.17.0"
r2d2 = "0.8.5"
strum = "0.18.0"
strum_macros = "0.18.0"
strum = "0.19.0"
strum_macros = "0.19.0"
backtrace = "0.3.33"
byteorder = "1.3.1"
itertools = "0.9.0"
quick-xml = "0.18.1"
escaper = "0.1.0"
bitflags = "1.1.0"
sanitize-filename = "0.2.1"
sanitize-filename = "0.3.0"
stop-token = { version = "0.1.1", features = ["unstable"] }
mailparse = "0.12.1"
mailparse = "0.13.0"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
native-tls = "0.2.3"
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
@@ -75,8 +75,9 @@ tempfile = "3.0"
pretty_assertions = "0.6.1"
pretty_env_logger = "0.4.0"
proptest = "0.10"
async-std = { version = "1.6.0", features = ["unstable", "attributes"] }
smol = "0.1.10"
async-std = { version = "1.6.4", features = ["unstable", "attributes"] }
futures-lite = "1.7.0"
criterion = "0.3"
[workspace]
members = [
@@ -95,6 +96,10 @@ path = "examples/repl/main.rs"
required-features = ["repl"]
[[bench]]
name = "create_account"
harness = false
[features]
default = []
internals = []

26
benches/create_account.rs Normal file
View File

@@ -0,0 +1,26 @@
use async_std::path::PathBuf;
use async_std::task::block_on;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::accounts::Accounts;
use tempfile::tempdir;
async fn create_accounts(n: u32) {
let dir = tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
for expected_id in 2..n {
let id = accounts.add_account().await.unwrap();
assert_eq!(id, expected_id);
}
}
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("create 1 account", |b| {
b.iter(|| block_on(async { create_accounts(black_box(1)).await }))
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -4,8 +4,6 @@ export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
@@ -30,6 +28,7 @@ set +x
ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
set +x -e
shopt -s huponexit
cd $BUILDDIR
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL

View File

@@ -4,8 +4,6 @@ export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
@@ -24,10 +22,12 @@ echo "--- Running $CIRCLE_JOB remotely"
ssh $SSHTARGET <<_HERE
set +x -e
# make sure all processes exit when ssh dies
shopt -s huponexit
export RUSTC_WRAPPER=\`which sccache\`
cd $BUILDDIR
# let's share the target dir with our last run on this branch/job-type
# cargo will make sure to block/unblock us properly
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

View File

@@ -4,8 +4,6 @@ export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
set -e
@@ -20,10 +18,10 @@ echo "--- Running $CIRCLE_JOB remotely"
ssh $SSHTARGET <<_HERE
set +x -e
# make sure all processes exit when ssh dies
shopt -s huponexit
export RUSTC_WRAPPER=\`which sccache\`
cd $BUILDDIR
# let's share the target dir with our last run on this branch/job-type
# cargo will make sure to block/unblock us properly
export CARGO_TARGET_DIR=\`pwd\`/../target
export TARGET=x86_64-unknown-linux-gnu
export RUSTC_WRAPPER=sccache

View File

@@ -4,6 +4,7 @@
# and tox/pytest.
set -e -x
shopt -s huponexit
# for core-building and python install step
export DCC_RS_TARGET=debug

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env bash
set -ex
shopt -s huponexit
#export RUST_TEST_THREADS=1
export RUST_BACKTRACE=1

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.45.0"
version = "1.46.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"

View File

@@ -236,12 +236,6 @@ TAB_SIZE = 4
ALIASES =
# This tag can be used to specify a number of word-keyword mappings (TCL only).
# A mapping has the form "name=value". For example adding "class=itcl::class"
# will allow you to use the command class in the itcl::class meaning.
TCL_SUBST =
# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
# only. Doxygen will then generate output that is more tailored for C. For
# instance, some of the names that are used will be different. The list of all

View File

@@ -218,7 +218,6 @@ dc_context_t* dc_context_new (const char* os_name, const char* d
* @param context The context object as created by dc_context_new(),
* dc_accounts_get_account() or dc_accounts_get_selected_account().
* If NULL is given, nothing is done.
* @return None.
*/
void dc_context_unref (dc_context_t* context);
@@ -274,10 +273,12 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `mail_user` = IMAP-username, guessed if left out
* - `mail_pw` = IMAP-password (always needed)
* - `mail_port` = IMAP-port, guessed if left out
* - `mail_security`= IMAP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
* - `send_server` = SMTP-server, guessed if left out
* - `send_user` = SMTP-user, guessed if left out
* - `send_pw` = SMTP-password, guessed if left out
* - `send_port` = SMTP-port, guessed if left out
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
* - `imap_certificate_checks` = how to check IMAP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `smtp_certificate_checks` = how to check SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
@@ -348,6 +349,10 @@ char* dc_get_blobdir (const dc_context_t* context);
* https://github.com/cracker0dks/basicwebrtc which some UIs have native support for.
* The type `jitsi:` may be handled by external apps.
* If no type is prefixed, the videochat is handled completely in a browser.
* - `bot` = Set to "1" if this is a bot. E.g. prevents adding the "Device messages" and "Saved messages" chats.
* - `fetch_existing` = 1=fetch most recent existing messages on configure (default),
* 0=do not fetch existing messages on configure.
* In both cases, existing recipients are added to the contact database.
*
* If you want to retrieve a value, use dc_get_config().
*
@@ -430,7 +435,7 @@ int dc_set_config_from_qr (dc_context_t* context, const char* qr);
* @param context The context object.
* @return String which must be released using dc_str_unref() after usage. Never returns NULL.
*/
char* dc_get_info (dc_context_t* context);
char* dc_get_info (const dc_context_t* context);
/**
@@ -504,7 +509,6 @@ char* dc_get_oauth2_url (dc_context_t* context, const char*
*
* @memberof dc_context_t
* @param context The context object.
* @return None.
*
* There is no need to call dc_configure() on every program start,
* the configuration result is saved in the database
@@ -544,7 +548,6 @@ int dc_is_configured (const dc_context_t* context);
*
* @memberof dc_context_t
* @param context The context object as created by dc_context_new().
* @return None
*/
void dc_start_io (dc_context_t* context);
@@ -568,7 +571,6 @@ int dc_is_io_running(const dc_context_t* context);
*
* @memberof dc_context_t
* @param context The context object as created by dc_context_new().
* @return None
*/
void dc_stop_io(dc_context_t* context);
@@ -593,7 +595,6 @@ void dc_stop_io(dc_context_t* context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_maybe_network (dc_context_t* context);
@@ -934,7 +935,6 @@ uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
* NULL deletes the existing draft, if any, without sending it.
* Currently, also non-text-messages
* will delete the existing drafts.
* @return None.
*/
void dc_set_draft (dc_context_t* context, uint32_t chat_id, dc_msg_t* msg);
@@ -988,25 +988,6 @@ void dc_set_draft (dc_context_t* context, uint32_t ch
*/
uint32_t dc_add_device_msg (dc_context_t* context, const char* label, dc_msg_t* msg);
/**
* Init device-messages and saved-messages chat.
* This function adds the device-chat and saved-messages chat
* and adds one or more welcome or update-messages.
* The ui can add messages on its own using dc_add_device_msg() -
* for ordering, either before or after or even without calling this function.
*
* Chat and message creation is done only once.
* So if the user has manually deleted things, they won't be re-created
* (however, not seen device messages are added and may re-create the device-chat).
*
* @memberof dc_context_t
* @param context The context object.
* @return None.
*/
void dc_update_device_chats (dc_context_t* context);
/**
* Check if a device-message with a given label was ever added.
* Device-messages can be added dc_add_device_msg().
@@ -1088,7 +1069,8 @@ int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t ch
/**
* 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.
* This is typically used to show the estimated impact to the user
* before actually enabling deletion of old messages.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
@@ -1119,27 +1101,16 @@ dc_array_t* dc_get_fresh_msgs (dc_context_t* context);
* but are still waiting for being marked as "seen" using dc_markseen_msgs()
* (IMAP/MDNs is not done for noticed messages).
*
* Calling this function usually results in the event #DC_EVENT_MSGS_CHANGED.
* See also dc_marknoticed_all_chats(), dc_marknoticed_contact() and dc_markseen_msgs().
* Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
* See also dc_marknoticed_contact() and dc_markseen_msgs().
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The chat ID of which all messages should be marked as being noticed.
* @return None.
*/
void dc_marknoticed_chat (dc_context_t* context, uint32_t chat_id);
/**
* Same as dc_marknoticed_chat() but for _all_ chats.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @return None.
*/
void dc_marknoticed_all_chats (dc_context_t* context);
/**
* Returns all message IDs of the given types in a chat.
* Typically used to show a gallery.
@@ -1185,6 +1156,24 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
uint32_t dc_get_next_media (dc_context_t* context, uint32_t msg_id, int dir, int msg_type, int msg_type2, int msg_type3);
/**
* Enable or disable protection against active attacks.
* To enable protection, it is needed that all members are verified;
* if this condition is met, end-to-end-encryption is always enabled
* and only the verified keys are used.
*
* Sends out #DC_EVENT_CHAT_MODIFIED on changes
* and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat to change the protection for.
* @param protect 1=protect chat, 0=unprotect chat
* @return 1=success, 0=error, eg. some members may be unverified
*/
int dc_set_chat_protection (dc_context_t* context, uint32_t chat_id, int protect);
/**
* Set chat visibility to pinned, archived or normal.
*
@@ -1195,7 +1184,6 @@ uint32_t dc_get_next_media (dc_context_t* context, uint32_t ms
* @param context The context object as returned from dc_context_new().
* @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_set_chat_visibility (dc_context_t* context, uint32_t chat_id, int visibility);
@@ -1224,7 +1212,6 @@ void dc_set_chat_visibility (dc_context_t* context, uint32_t ch
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat to delete.
* @return None.
*/
void dc_delete_chat (dc_context_t* context, uint32_t chat_id);
@@ -1250,6 +1237,8 @@ dc_array_t* dc_get_chat_contacts (dc_context_t* context, uint32_t ch
/**
* Get the chat's ephemeral message timer.
* The ephemeral message timer is set by dc_set_chat_ephemeral_timer()
* on this or any other device participating in the chat.
*
* @memberof dc_context_t
* @param context The context object.
@@ -1314,15 +1303,15 @@ dc_chat_t* dc_get_chat (dc_context_t* context, uint32_t ch
*
* @memberof dc_context_t
* @param context The context object.
* @param verified If set to 1 the function creates a secure verified group.
* Only secure-verified members are allowed in these groups
* @param protect If set to 1 the function creates group with protection initially enabled.
* Only verified members are allowed in these groups
* and end-to-end-encryption is always enabled.
* @param name The name of the group chat to create.
* The name may be changed later using dc_set_chat_name().
* To find out the name of a group later, see dc_chat_get_name()
* @return The chat ID of the new group chat, 0 on errors.
*/
uint32_t dc_create_group_chat (dc_context_t* context, int verified, const char* name);
uint32_t dc_create_group_chat (dc_context_t* context, int protect, const char* name);
/**
@@ -1344,7 +1333,7 @@ int dc_is_contact_in_chat (dc_context_t* context, uint32_t ch
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* If the group is a verified group, only verified contacts can be added to the group.
* If the group has group protection enabled, only verified contacts can be added to the group.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*
@@ -1393,8 +1382,11 @@ int dc_set_chat_name (dc_context_t* context, uint32_t ch
/**
* Set the chat's ephemeral message timer.
*
* This timer is applied to all messages in a chat and starts when the
* message is read. The setting is synchronized to all clients
* This timer is applied to all messages in a chat and starts when the message is read.
* For outgoing messages, the timer starts once the message is sent,
* for incoming messages, the timer starts once dc_markseen_msgs() is called.
*
* The setting is synchronized to all clients
* participating in a chat.
*
* @memberof dc_context_t
@@ -1486,21 +1478,9 @@ char* dc_get_mime_headers (dc_context_t* context, uint32_t ms
* @param context The context object
* @param msg_ids an array of uint32_t containing all message IDs that should be deleted
* @param msg_cnt The number of messages IDs in the msg_ids array
* @return None.
*/
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.
* @param flags What to delete, a combination of the @ref DC_EMPTY flags
* @return None.
*/
void dc_empty_server (dc_context_t* context, uint32_t flags);
/**
* Forward messages to another chat.
@@ -1510,22 +1490,22 @@ void dc_empty_server (dc_context_t* context, uint32_t fl
* @param msg_ids An array of uint32_t containing all message IDs that should be forwarded
* @param msg_cnt The number of messages IDs in the msg_ids array
* @param chat_id The destination chat ID.
* @return None.
*/
void dc_forward_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt, uint32_t chat_id);
/**
* Mark all messages sent by the given contact
* as _noticed_. See also dc_marknoticed_chat() and
* dc_markseen_msgs()
* Mark all messages sent by the given contact as _noticed_.
* This function is typically used to ignore a user in the deaddrop temporarily ("Not now" button).
*
* Calling this function usually results in the event #DC_EVENT_MSGS_CHANGED.
* The contact is expected to belong to the deaddrop;
* only one #DC_EVENT_MSGS_NOTICED with chat_id=DC_CHAT_ID_DEADDROP may be emitted.
*
* See also dc_marknoticed_chat() and dc_markseen_msgs()
*
* @memberof dc_context_t
* @param context The context object.
* @param contact_id The contact ID of which all messages should be marked as noticed.
* @return None.
*/
void dc_marknoticed_contact (dc_context_t* context, uint32_t contact_id);
@@ -1536,30 +1516,19 @@ void dc_marknoticed_contact (dc_context_t* context, uint32_t co
* message is only marked as NOTICED and no IMAP/MDNs is done. See also
* dc_marknoticed_chat() and dc_marknoticed_contact()
*
* Moreover, if messages belong to a chat with ephemeral messages enabled,
* the ephemeral timer is started for these messages.
*
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_ids An array of uint32_t containing all the messages IDs that should be marked as seen.
* @param msg_cnt The number of message IDs in msg_ids.
* @return None.
*/
void dc_markseen_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
/**
* Star/unstar messages by setting the last parameter to 0 (unstar) or 1 (star).
* Starred messages are collected in a virtual chat that can be shown using
* dc_get_chat_msgs() using the chat_id DC_CHAT_ID_STARRED.
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_ids An array of uint32_t message IDs defining the messages to star or unstar
* @param msg_cnt The number of IDs in msg_ids
* @param star 0=unstar the messages in msg_ids, 1=star them
* @return None.
*/
void dc_star_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt, int star);
/**
* Get a single message object of the type dc_msg_t.
* For a list of messages in a chat, see dc_get_chat_msgs()
@@ -1710,7 +1679,6 @@ dc_array_t* dc_get_blocked_contacts (dc_context_t* context);
* @param context The context object.
* @param contact_id The ID of the contact to block or unblock.
* @param block 1=block contact, 0=unblock contact
* @return None.
*/
void dc_block_contact (dc_context_t* context, uint32_t contact_id, int block);
@@ -1806,7 +1774,6 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
* @param param1 Meaning depends on the DC_IMEX_* constants. If this parameter is a directory, it should not end with
* a slash (otherwise you'll get double slashes when receiving #DC_EVENT_IMEX_FILE_WRITTEN). Set to NULL if not used.
* @param param2 Meaning depends on the DC_IMEX_* constants. Set to NULL if not used.
* @return None.
*/
void dc_imex (dc_context_t* context, int what, const char* param1, const char* param2);
@@ -1949,7 +1916,6 @@ int dc_continue_key_transfer (dc_context_t* context, uint32_t ms
*
* @memberof dc_context_t
* @param context The context object.
* @return None.
*/
void dc_stop_ongoing_process (dc_context_t* context);
@@ -2011,7 +1977,7 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
* @param context The context object.
* @param chat_id If set to a group-chat-id,
* the Verified-Group-Invite protocol is offered in the QR code;
* works for verified groups as well as for normal groups.
* works for protected groups as well as for normal groups.
* 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.
@@ -2041,7 +2007,7 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
* 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
* This is because the protected 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
@@ -2053,8 +2019,8 @@ 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(),
* A returned chat-id does not guarantee that the chat is protected or the belonging contact is verified.
* If needed, this be checked with dc_chat_is_protected() 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);
@@ -2078,7 +2044,6 @@ uint32_t dc_join_securejoin (dc_context_t* context, const char*
* @param chat_id Chat id to enable location streaming for.
* @param seconds >0: enable location streaming for the given number of seconds;
* 0: disable location streaming.
* @return None.
*/
void dc_send_locations_to_chat (dc_context_t* context, uint32_t chat_id, int seconds);
@@ -2201,7 +2166,6 @@ dc_array_t* dc_get_locations (dc_context_t* context, uint32_t cha
*
* @memberof dc_context_t
* @param context The context object.
* @return None.
*/
void dc_delete_all_locations (dc_context_t* context);
@@ -2212,11 +2176,11 @@ void dc_delete_all_locations (dc_context_t* context);
* MUST NOT be released by the standard free() function;
* always use dc_str_unref() for this purpose.
* - dc_str_unref() MUST NOT be called for strings not returned by deltachat-core.
* - dc_str_unref() MUST NOT be called for other objectes returned by deltachat-core.
* - dc_str_unref() MUST NOT be called for other objects returned by deltachat-core.
*
* @memberof dc_context_t
* @param str The string to release.
* @return None.
* If NULL is given, nothing is done.
*/
void dc_str_unref (char* str);
@@ -2369,7 +2333,7 @@ dc_context_t* dc_accounts_get_account (dc_accounts_t* accounts, uint32
/**
* Get the currently selected account.
* If there is at least once account in the account-manager,
* If there is at least one account in the account-manager,
* there is always a selected one.
* To change the selected account, use dc_accounts_select_account();
* also adding/importing/migrating accounts may change the selection.
@@ -2403,7 +2367,6 @@ int dc_accounts_select_account (dc_accounts_t* accounts, uint32
*
* @memberof dc_accounts_t
* @param accounts Account manager as created by dc_accounts_new().
* @return None.
*/
void dc_accounts_start_io (dc_accounts_t* accounts);
@@ -2471,7 +2434,6 @@ dc_accounts_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accou
* @param array The array object to free,
* created eg. by dc_get_chatlist(), dc_get_contacts() and so on.
* If NULL is given, nothing is done.
* @return None.
*/
void dc_array_unref (dc_array_t* array);
@@ -2670,7 +2632,6 @@ int dc_array_search_id (const dc_array_t* array, uint32_t
* @memberof dc_chatlist_t
* @param chatlist The chatlist object to free, created eg. by dc_get_chatlist(), dc_search_msgs().
* If NULL is given, nothing is done.
* @return None.
*/
void dc_chatlist_unref (dc_chatlist_t* chatlist);
@@ -2757,7 +2718,7 @@ dc_lot_t* dc_chatlist_get_summary (const dc_chatlist_t* chatlist, siz
* @memberof dc_context_t
* @param context The context object.
* @param chat_id Chat to get a summary for.
* @param msg_id Messasge to get a summary for.
* @param msg_id Message to get a summary for.
* @return The summary as an dc_lot_t object, see dc_chatlist_get_summary() for details.
* Must be freed using dc_lot_unref(). NULL is never returned.
*/
@@ -2804,8 +2765,6 @@ char* dc_chat_get_info_json (dc_context_t* context, size_t chat
#define DC_CHAT_ID_DEADDROP 1 // virtual chat showing all messages belonging to chats flagged with chats.blocked=2
#define DC_CHAT_ID_TRASH 3 // messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
#define DC_CHAT_ID_MSGS_IN_CREATION 4 // a message is just in creation but not yet assigned to a chat (eg. we may need the message ID to set up blobs; this avoids unready message to be sent and shown)
#define DC_CHAT_ID_STARRED 5 // virtual chat showing all messages flagged with msgs.starred=2
#define DC_CHAT_ID_ARCHIVED_LINK 6 // only an indicator in a chatlist
#define DC_CHAT_ID_ALLDONE_HINT 7 // only an indicator in a chatlist
#define DC_CHAT_ID_LAST_SPECIAL 9 // larger chat IDs are "real" chats, their messages are "real" messages.
@@ -2814,7 +2773,6 @@ char* dc_chat_get_info_json (dc_context_t* context, size_t chat
#define DC_CHAT_TYPE_UNDEFINED 0
#define DC_CHAT_TYPE_SINGLE 100
#define DC_CHAT_TYPE_GROUP 120
#define DC_CHAT_TYPE_VERIFIED_GROUP 130
/**
@@ -2823,7 +2781,6 @@ char* dc_chat_get_info_json (dc_context_t* context, size_t chat
* @memberof dc_chat_t
* @param chat Chat object are returned eg. by dc_get_chat().
* If NULL is given, nothing is done.
* @return None.
*/
void dc_chat_unref (dc_chat_t* chat);
@@ -2833,7 +2790,6 @@ void dc_chat_unref (dc_chat_t* chat);
*
* Special IDs:
* - DC_CHAT_ID_DEADDROP (1) - Virtual chat containing messages which senders are not confirmed by the user.
* - DC_CHAT_ID_STARRED (5) - Virtual chat containing all starred messages-
* - DC_CHAT_ID_ARCHIVED_LINK (6) - A link at the end of the chatlist, if present the UI should show the button "Archived chats"-
*
* "Normal" chat IDs are larger than these special IDs (larger than DC_CHAT_ID_LAST_SPECIAL).
@@ -2857,9 +2813,6 @@ uint32_t dc_chat_get_id (const dc_chat_t* chat);
* - DC_CHAT_TYPE_GROUP (120) - a group chat, chats_contacts contain all group
* members, incl. DC_CONTACT_ID_SELF
*
* - DC_CHAT_TYPE_VERIFIED_GROUP (130) - a verified group chat. In verified groups,
* all members are verified and encryption is always active and cannot be disabled.
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return Chat type.
@@ -2989,15 +2942,16 @@ int dc_chat_can_send (const dc_chat_t* chat);
/**
* Check if a chat is verified. Verified chats contain only verified members
* and encryption is alwasy enabled. Verified chats are created using
* dc_create_group_chat() by setting the 'verified' parameter to true.
* Check if a chat is protected.
* Protected chats contain only verified members and encryption is always enabled.
* Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1.
* The status can be changed using dc_set_chat_protection().
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=chat verified, 0=chat is not verified
* @return 1=chat protected, 0=chat is not protected
*/
int dc_chat_is_verified (const dc_chat_t* chat);
int dc_chat_is_protected (const dc_chat_t* chat);
/**
@@ -3084,7 +3038,6 @@ dc_msg_t* dc_msg_new (dc_context_t* context, int viewty
* @memberof dc_msg_t
* @param msg The message object to free.
* If NULL is given, nothing is done.
* @return None.
*/
void dc_msg_unref (dc_msg_t* msg);
@@ -3363,6 +3316,7 @@ int dc_msg_get_showpadlock (const dc_msg_t* msg);
/**
* Get ephemeral timer duration for message.
* This is the value of dc_get_chat_ephemeral_timer() in the moment the message was sent.
*
* To check if the timer is started and calculate remaining time,
* use dc_msg_get_ephemeral_timestamp().
@@ -3382,7 +3336,8 @@ uint32_t dc_msg_get_ephemeral_timer (const dc_msg_t* msg);
*
* @memberof dc_msg_t
* @param msg The message object.
* @return Time of message removal, 0 if the timer is not started.
* @return Time of message removal, 0 if the timer is not yet started
* (the timer starts on sending messages or when dc_markseen_msgs() is called)
*/
int64_t dc_msg_get_ephemeral_timestamp (const dc_msg_t* msg);
@@ -3468,21 +3423,6 @@ int dc_msg_has_location (const dc_msg_t* msg);
int dc_msg_is_sent (const dc_msg_t* msg);
/**
* Check if a message is starred. Starred messages are "favorites" marked by the user
* with a "star" or something like that. Starred messages can typically be shown
* easily and are not deleted automatically.
*
* To star one or more messages, use dc_star_msgs(), to get a list of starred messages,
* use dc_get_chat_msgs() using DC_CHAT_ID_STARRED as the chat_id.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return 1=message is starred, 0=message not starred.
*/
int dc_msg_is_starred (const dc_msg_t* msg);
/**
* Check if the message is a forwarded message.
*
@@ -3505,7 +3445,8 @@ int dc_msg_is_forwarded (const dc_msg_t* msg);
/**
* Check if the message is an informational message, created by the
* device or by another users. Such messages are not "typed" by the user but
* created due to other actions, eg. dc_set_chat_name(), dc_set_chat_profile_image()
* created due to other actions,
* eg. dc_set_chat_name(), dc_set_chat_profile_image(), dc_set_chat_protection()
* or dc_add_contact_to_chat().
*
* These messages are typically shown in the center of the chat view,
@@ -3521,6 +3462,32 @@ int dc_msg_is_forwarded (const dc_msg_t* msg);
int dc_msg_is_info (const dc_msg_t* msg);
/**
* Get the type of an informational message.
* If dc_msg_is_info() returns 1, this function returns the type of the informational message.
* UIs can display eg. an icon based upon the type.
*
* Currently, the following types are defined:
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
*
* Even when you display an icon,
* you should still display the text of the informational message using dc_msg_get_text()
*
* @memberof dc_msg_t
* @param msg The message object.
* @return One of the DC_INFO* constants.
* 0 or other values indicate unspecified types
* or that the message is not an info-message.
*/
int dc_msg_get_info_type (const dc_msg_t* msg);
// DC_INFO* uses the same values as SystemMessage in rust-land
#define DC_INFO_PROTECTION_ENABLED 11
#define DC_INFO_PROTECTION_DISABLED 12
/**
* Check if a message is still in creation. A message is in creation between
* the calls to dc_prepare_msg() and dc_send_msg().
@@ -3585,6 +3552,27 @@ char* dc_msg_get_setupcodebegin (const dc_msg_t* msg);
*/
char* dc_msg_get_videochat_url (const dc_msg_t* msg);
/**
* Gets the error status of the message.
* If there is no error associated with the message, NULL is returned.
*
* A message can have an associated error status if something went wrong when sending or
* receiving message itself. The error status is free-form text and should not be further parsed,
* rather it's presence is meant to indicate *something* went wrong with the message and the
* text of the error is detailed information on what.
*
* Some common reasons error can be associated with messages are:
* * Lack of valid signature on an e2ee message, usually for received messages.
* * Failure to decrypt an e2ee message, usually for received messages.
* * When a message could not be delivered to one or more recipients the non-delivery
* notification text can be stored in the error status.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return Error or NULL. The result must be released using dc_str_unref().
*/
char* dc_msg_get_error (const dc_msg_t* msg);
/**
* Get type of videochat.
@@ -3628,7 +3616,6 @@ int dc_msg_get_videochat_type (const dc_msg_t* msg);
* @memberof dc_msg_t
* @param msg The message object.
* @param text Message text.
* @return None.
*/
void dc_msg_set_text (dc_msg_t* msg, const char* text);
@@ -3644,7 +3631,6 @@ void dc_msg_set_text (dc_msg_t* msg, const char* text);
* @param file If the message object is used in dc_send_msg() later,
* this must be the full path of the image file to send.
* @param filemime Mime type of the file. NULL if you don't know or don't care.
* @return None.
*/
void dc_msg_set_file (dc_msg_t* msg, const char* file, const char* filemime);
@@ -3658,7 +3644,6 @@ void dc_msg_set_file (dc_msg_t* msg, const char* file,
* @param msg The message object.
* @param width Width in pixels, if known. 0 if you don't know or don't care.
* @param height Height in pixels, if known. 0 if you don't know or don't care.
* @return None.
*/
void dc_msg_set_dimension (dc_msg_t* msg, int width, int height);
@@ -3671,7 +3656,6 @@ void dc_msg_set_dimension (dc_msg_t* msg, int width, int hei
* @memberof dc_msg_t
* @param msg The message object.
* @param duration Length in milliseconds. 0 if you don't know or don't care.
* @return None.
*/
void dc_msg_set_duration (dc_msg_t* msg, int duration);
@@ -3691,7 +3675,6 @@ void dc_msg_set_duration (dc_msg_t* msg, int duration);
* @param msg The message object.
* @param latitude North-south position of the location.
* @param longitude East-west position of the location.
* @return None.
*/
void dc_msg_set_location (dc_msg_t* msg, double latitude, double longitude);
@@ -3716,11 +3699,67 @@ void dc_msg_set_location (dc_msg_t* msg, double latitude, d
* @param width The new width to store in the message object. 0 if you do not want to change width and height.
* @param height The new height to store in the message object. 0 if you do not want to change width and height.
* @param duration The new duration to store in the message object. 0 if you do not want to change it.
* @return None.
*/
void dc_msg_latefiling_mediasize (dc_msg_t* msg, int width, int height, int duration);
/**
* Set the message replying to.
* This allows optionally to reply to an explicit message
* instead of replying implicitly to the end of the chat.
*
* dc_msg_set_quote() copies some basic data from the quoted message object
* so that dc_msg_get_quoted_text() will always work.
* dc_msg_get_quoted_msg() gets back the quoted message only if it is _not_ deleted.
*
* @memberof dc_msg_t
* @param msg The message object to set the reply to.
* @param quote The quote to set for msg.
*/
void dc_msg_set_quote (dc_msg_t* msg, const dc_msg_t* quote);
/**
* Get quoted text, if any.
* You can use this function also check if there is a quote for a message.
*
* The text is a summary of the original text,
* similar to what is shown in the chatlist.
*
* If available, you can get the whole quoted message object using dc_msg_get_quoted_msg().
*
* @memberof dc_msg_t
* @param msg The message object.
* @return The quoted text or NULL if there is no quote.
* Returned strings must be released using dc_str_unref().
*/
char* dc_msg_get_quoted_text (const dc_msg_t* msg);
/**
* Get quoted message, if available.
* UIs might use this information to offer "jumping back" to the quoted message
* or to enrich displaying the quote.
*
* If this function returns NULL,
* this does not mean there is no quote for the message -
* it might also mean that a quote exist but the quoted message is deleted meanwhile.
* Therefore, do not use this function to check if there is a quote for a message.
* To check if a message has a quote, use dc_msg_get_quoted_text().
*
* To display the quote in the chat, use dc_msg_get_quoted_text() as a primary source,
* however, one might add information from the message object (eg. an image).
*
* It is not guaranteed that the message belong to the same chat.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return The quoted message or NULL.
* Must be freed using dc_msg_unref() after usage.
*/
dc_msg_t* dc_msg_get_quoted_msg (const dc_msg_t* msg);
/**
* @class dc_contact_t
*
@@ -3754,7 +3793,6 @@ void dc_msg_latefiling_mediasize (dc_msg_t* msg, int width, int hei
* @memberof dc_contact_t
* @param contact The contact object as created eg. by dc_get_contact().
* If NULL is given, nothing is done.
* @return None.
*/
void dc_contact_unref (dc_contact_t* contact);
@@ -3994,7 +4032,6 @@ void dc_provider_unref (dc_provider_t* provider);
* @memberof dc_lot_t
* @param lot The object to free.
* If NULL is given, nothing is done.
* @return None.
*/
void dc_lot_unref (dc_lot_t* lot);
@@ -4167,6 +4204,44 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
#define DC_MSG_VIDEOCHAT_INVITATION 70
/**
* @}
*/
/**
* @defgroup DC_SOCKET DC_SOCKET
*
* These constants configure socket security.
* To set socket security, use dc_set_config() with the keys "mail_security" and/or "send_security".
* If no socket-configuration is explicitly specified, #DC_SOCKET_AUTO is used.
*
* @addtogroup DC_SOCKET
* @{
*/
/**
* Choose socket security automatically.
*/
#define DC_SOCKET_AUTO 0
/**
* Connect via SSL/TLS.
*/
#define DC_SOCKET_SSL 1
/**
* Connect via STARTTLS.
*/
#define DC_SOCKET_STARTTLS 2
/**
* Connect unencrypted, this should not be used.
*/
#define DC_SOCKET_PLAIN 3
/**
* @}
@@ -4201,54 +4276,11 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_LP_AUTH_NORMAL 0x4
/**
* Connect to IMAP via STARTTLS.
* If this flag is set, automatic configuration is skipped.
*/
#define DC_LP_IMAP_SOCKET_STARTTLS 0x100
/**
* Connect to IMAP via SSL.
* If this flag is set, automatic configuration is skipped.
*/
#define DC_LP_IMAP_SOCKET_SSL 0x200
/**
* Connect to IMAP unencrypted, this should not be used.
* If this flag is set, automatic configuration is skipped.
*/
#define DC_LP_IMAP_SOCKET_PLAIN 0x400
/**
* Connect to SMTP via STARTTLS.
* If this flag is set, automatic configuration is skipped.
*/
#define DC_LP_SMTP_SOCKET_STARTTLS 0x10000
/**
* Connect to SMTP via SSL.
* If this flag is set, automatic configuration is skipped.
*/
#define DC_LP_SMTP_SOCKET_SSL 0x20000
/**
* Connect to SMTP unencrypted, this should not be used.
* If this flag is set, automatic configuration is skipped.
*/
#define DC_LP_SMTP_SOCKET_PLAIN 0x40000 ///<
/**
* @}
*/
#define DC_LP_AUTH_FLAGS (DC_LP_AUTH_OAUTH2|DC_LP_AUTH_NORMAL) // if none of these flags are set, the default is chosen
#define DC_LP_IMAP_SOCKET_FLAGS (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
#define DC_LP_SMTP_SOCKET_FLAGS (DC_LP_SMTP_SOCKET_STARTTLS|DC_LP_SMTP_SOCKET_SSL|DC_LP_SMTP_SOCKET_PLAIN) // if none of these flags are set, the default is chosen
/**
* @defgroup DC_CERTCK DC_CERTCK
@@ -4284,10 +4316,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
#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
/**
* @class dc_event_emitter_t
*
@@ -4317,7 +4345,6 @@ dc_event_t* dc_get_next_event(dc_event_emitter_t* emitter);
* @memberof dc_event_emitter_t
* @param emitter Event emitter object as returned from dc_get_event_emitter().
* If NULL is given, nothing is done and an error is logged.
* @return None.
*/
void dc_event_emitter_unref(dc_event_emitter_t* emitter);
@@ -4351,7 +4378,6 @@ dc_event_t* dc_accounts_get_next_event (dc_accounts_event_emitter_t* emitter);
* @memberof dc_accounts_event_emitter_t
* @param emitter Event emitter object as returned from dc_accounts_get_event_emitter().
* If NULL is given, nothing is done and an error is logged.
* @return None.
*/
void dc_accounts_event_emitter_unref(dc_accounts_event_emitter_t* emitter);
@@ -4413,7 +4439,7 @@ int dc_event_get_data2_int(dc_event_t* event);
*
* @memberof dc_event_t
* @param event Event object as returned from dc_get_next_event().
* @return "data2" as a string,
* @return "data2" as a string or NULL.
* the meaning depends on the event type associated with this event.
* Once you're done with the string, you have to unref it using dc_unref_str().
*/
@@ -4438,7 +4464,6 @@ uint32_t dc_event_get_account_id(dc_event_t* event);
*
* @memberof dc_event_t
* @param event Event object as returned from dc_get_next_event().
* @return None.
*/
void dc_event_unref(dc_event_t* event);
@@ -4509,14 +4534,6 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_IMAP_MESSAGE_MOVED 105
/**
* Emitted when an IMAP folder was emptied.
*
* @param data1 0
* @param data2 (char*) Folder name.
*/
#define DC_EVENT_IMAP_FOLDER_EMPTIED 106
/**
* Emitted when a new blob file was successfully written
*
@@ -4622,6 +4639,21 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_INCOMING_MSG 2005
/**
* Messages were marked noticed or seen.
* The ui may update badge counters or stop showing a chatlist-item with a bold font.
*
* This event is emitted eg. when calling dc_markseen_msgs(), dc_marknoticed_chat() or dc_marknoticed_contact()
* or when a chat is answered on another device.
* Do not try to derive the state of an item from just the fact you received the event;
* use eg. dc_msg_get_state() or dc_get_fresh_msg_cnt() for this purpose.
*
* @param data1 (int) chat_id
* @param data2 0
*/
#define DC_EVENT_MSGS_NOTICED 2008
/**
* A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
* DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
@@ -4673,7 +4705,7 @@ void dc_event_unref(dc_event_t* event);
/**
* 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 data1 (int) contact_id of the changed contact or 0 on batch-changes or deletion.
* @param data2 0
*/
#define DC_EVENT_CONTACTS_CHANGED 2030
@@ -4695,7 +4727,7 @@ void dc_event_unref(dc_event_t* event);
* Inform about the configuration progress started by dc_configure().
*
* @param data1 (int) 0=error, 1-999=progress in permille, 1000=success and done
* @param data2 0
* @param data2 (char*) progress comment, error message or NULL if not applicable
*/
#define DC_EVENT_CONFIGURE_PROGRESS 2041
@@ -4758,18 +4790,8 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_FILE_COPIED 2055 // not used anymore
#define DC_EVENT_IS_OFFLINE 2081 // not used anymore
#define DC_EVENT_GET_STRING 2091 // not used anymore, use dc_set_stock_translation()
#define DC_ERROR_SEE_STRING 0 // not used anymore
#define DC_ERROR_SELF_NOT_IN_GROUP 1 // not used anymore
#define DC_STR_SELFNOTINGRP 21 // not used anymore
#define DC_EVENT_DATA1_IS_STRING(e) 0 // not used anymore
#define DC_EVENT_DATA2_IS_STRING(e) ((e)==DC_EVENT_IMEX_FILE_WRITTEN || ((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
#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
#define DC_EVENT_DATA2_IS_STRING(e) ((e)==DC_EVENT_CONFIGURE_PROGRESS || (e)==DC_EVENT_IMEX_FILE_WRITTEN || ((e)>=100 && (e)<=499))
/*
@@ -4933,7 +4955,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_STR_CONTACT_NOT_VERIFIED 36
#define DC_STR_CONTACT_SETUP_CHANGED 37
#define DC_STR_ARCHIVEDCHATS 40
#define DC_STR_STARREDMSGS 41
#define DC_STR_AC_SETUP_MSG_SUBJECT 42
#define DC_STR_AC_SETUP_MSG_BODY 43
#define DC_STR_CANNOT_LOGIN 60
@@ -4960,8 +4981,13 @@ void dc_event_unref(dc_event_t* event);
#define DC_STR_EPHEMERAL_FOUR_WEEKS 81
#define DC_STR_VIDEOCHAT_INVITATION 82
#define DC_STR_VIDEOCHAT_INVITE_MSG_BODY 83
#define DC_STR_COUNT 83
#define DC_STR_CONFIGURATION_FAILED 84
#define DC_STR_BAD_TIME_MSG_BODY 85
#define DC_STR_UPDATE_REMINDER_MSG_BODY 86
#define DC_STR_ERROR_NO_NETWORK 87
#define DC_STR_PROTECTION_ENABLED 88
#define DC_STR_PROTECTION_DISABLED 89
#define DC_STR_REPLY_NOUN 90 /* eg. "Reply", used in summaries, a noun, not a verb (not: "to reply") */
/*
* @}

View File

@@ -25,7 +25,7 @@ use async_std::task::{block_on, spawn};
use num_traits::{FromPrimitive, ToPrimitive};
use deltachat::accounts::Accounts;
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration};
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, Origin};
use deltachat::context::Context;
@@ -218,7 +218,7 @@ pub unsafe extern "C" fn dc_set_config_from_qr(
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_info(context: *mut dc_context_t) -> *mut libc::c_char {
pub unsafe extern "C" fn dc_get_info(context: *const dc_context_t) -> *mut libc::c_char {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_info()");
return "".strdup();
@@ -316,7 +316,6 @@ pub unsafe extern "C" fn dc_get_id(context: *mut dc_context_t) -> libc::c_int {
ctx.get_id() as libc::c_int
}
#[no_mangle]
pub type dc_event_t = Event;
#[no_mangle]
@@ -355,7 +354,6 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::SmtpMessageSent(_)
| EventType::ImapMessageDeleted(_)
| EventType::ImapMessageMoved(_)
| EventType::ImapFolderEmptied(_)
| EventType::NewBlobFile(_)
| EventType::DeletedBlobFile(_)
| EventType::Warning(_)
@@ -364,6 +362,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::ErrorSelfNotInGroup(_) => 0,
EventType::MsgsChanged { chat_id, .. }
| EventType::IncomingMsg { chat_id, .. }
| EventType::MsgsNoticed(chat_id)
| EventType::MsgDelivered { chat_id, .. }
| EventType::MsgFailed { chat_id, .. }
| EventType::MsgRead { chat_id, .. }
@@ -373,7 +372,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
let id = id.unwrap_or_default();
id as libc::c_int
}
EventType::ConfigureProgress(progress) | EventType::ImexProgress(progress) => {
EventType::ConfigureProgress { progress, .. } | EventType::ImexProgress(progress) => {
*progress as libc::c_int
}
EventType::ImexFileWritten(_) => 0,
@@ -398,7 +397,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::SmtpMessageSent(_)
| EventType::ImapMessageDeleted(_)
| EventType::ImapMessageMoved(_)
| EventType::ImapFolderEmptied(_)
| EventType::NewBlobFile(_)
| EventType::DeletedBlobFile(_)
| EventType::Warning(_)
@@ -407,9 +405,10 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::ErrorSelfNotInGroup(_)
| EventType::ContactsChanged(_)
| EventType::LocationChanged(_)
| EventType::ConfigureProgress(_)
| EventType::ConfigureProgress { .. }
| EventType::ImexProgress(_)
| EventType::ImexFileWritten(_)
| EventType::MsgsNoticed(_)
| EventType::ChatModified(_) => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
@@ -438,7 +437,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::SmtpMessageSent(msg)
| EventType::ImapMessageDeleted(msg)
| EventType::ImapMessageMoved(msg)
| EventType::ImapFolderEmptied(msg)
| EventType::NewBlobFile(msg)
| EventType::DeletedBlobFile(msg)
| EventType::Warning(msg)
@@ -450,17 +448,24 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
}
EventType::MsgsChanged { .. }
| EventType::IncomingMsg { .. }
| EventType::MsgsNoticed(_)
| EventType::MsgDelivered { .. }
| EventType::MsgFailed { .. }
| EventType::MsgRead { .. }
| EventType::ChatModified(_)
| EventType::ContactsChanged(_)
| EventType::LocationChanged(_)
| EventType::ConfigureProgress(_)
| EventType::ImexProgress(_)
| EventType::SecurejoinInviterProgress { .. }
| EventType::SecurejoinJoinerProgress { .. }
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
comment.to_c_string().unwrap_or_default().into_raw()
} else {
ptr::null_mut()
}
}
EventType::ImexFileWritten(file) => {
let data2 = file.to_c_string().unwrap_or_default();
data2.into_raw()
@@ -478,7 +483,6 @@ pub unsafe extern "C" fn dc_event_get_account_id(event: *mut dc_event_t) -> u32
(*event).id
}
#[no_mangle]
pub type dc_event_emitter_t = EventEmitter;
#[no_mangle]
@@ -496,7 +500,7 @@ pub unsafe extern "C" fn dc_get_event_emitter(
#[no_mangle]
pub unsafe extern "C" fn dc_event_emitter_unref(emitter: *mut dc_event_emitter_t) {
if emitter.is_null() {
eprintln!("ignoring careless call to dc_event_mitter_unref()");
eprintln!("ignoring careless call to dc_event_emitter_unref()");
return;
}
@@ -506,6 +510,7 @@ pub unsafe extern "C" fn dc_event_emitter_unref(emitter: *mut dc_event_emitter_t
#[no_mangle]
pub unsafe extern "C" fn dc_get_next_event(events: *mut dc_event_emitter_t) -> *mut dc_event_t {
if events.is_null() {
eprintln!("ignoring careless call to dc_get_next_event()");
return ptr::null_mut();
}
let events = &*events;
@@ -802,21 +807,6 @@ pub unsafe extern "C" fn dc_add_device_msg(
.to_u32()
}
#[no_mangle]
pub unsafe extern "C" fn dc_update_device_chats(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_update_device_chats()");
return;
}
let ctx = &mut *context;
block_on(async move {
ctx.update_device_chats()
.await
.unwrap_or_log_default(&ctx, "Failed to add device message")
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_was_device_msg_ever_added(
context: *mut dc_context_t,
@@ -969,22 +959,6 @@ pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_marknoticed_all_chats(context: *mut dc_context_t) {
if context.is_null() {
eprintln!("ignoring careless call to dc_marknoticed_all_chats()");
return;
}
let ctx = &*context;
block_on(async move {
chat::marknoticed_all_chats(&ctx)
.await
.log_err(ctx, "Failed marknoticed all chats")
.unwrap_or(())
})
}
fn from_prim<S, T>(s: S) -> Option<T>
where
T: FromPrimitive,
@@ -1068,6 +1042,32 @@ pub unsafe extern "C" fn dc_get_next_media(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_protection(
context: *mut dc_context_t,
chat_id: u32,
protect: libc::c_int,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_set_chat_protection()");
return 0;
}
let ctx = &*context;
let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
s
} else {
warn!(ctx, "bad protect-value for dc_set_chat_protection()");
return 0;
};
block_on(async move {
match ChatId::new(chat_id).set_protection(&ctx, protect).await {
Ok(()) => 1,
Err(_) => 0,
}
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_visibility(
context: *mut dc_context_t,
@@ -1181,7 +1181,7 @@ pub unsafe extern "C" fn dc_get_chat(context: *mut dc_context_t, chat_id: u32) -
#[no_mangle]
pub unsafe extern "C" fn dc_create_group_chat(
context: *mut dc_context_t,
verified: libc::c_int,
protect: libc::c_int,
name: *const libc::c_char,
) -> u32 {
if context.is_null() || name.is_null() {
@@ -1189,14 +1189,15 @@ pub unsafe extern "C" fn dc_create_group_chat(
return 0;
}
let ctx = &*context;
let verified = if let Some(s) = contact::VerifiedStatus::from_i32(verified) {
let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
s
} else {
warn!(ctx, "bad protect-value for dc_create_group_chat()");
return 0;
};
block_on(async move {
chat::create_group_chat(&ctx, verified, to_string_lossy(name))
chat::create_group_chat(&ctx, protect, to_string_lossy(name))
.await
.log_err(ctx, "Failed to create group chat")
.map(|id| id.to_u32())
@@ -1423,17 +1424,6 @@ pub unsafe extern "C" fn dc_delete_msgs(
block_on(message::delete_msgs(&ctx, &msg_ids))
}
#[no_mangle]
pub unsafe extern "C" fn dc_empty_server(context: *mut dc_context_t, flags: u32) {
if context.is_null() || flags == 0 {
eprintln!("ignoring careless call to dc_empty_server()");
return;
}
let ctx = &*context;
block_on(message::dc_empty_server(&ctx, flags))
}
#[no_mangle]
pub unsafe extern "C" fn dc_forward_msgs(
context: *mut dc_context_t,
@@ -1486,23 +1476,6 @@ pub unsafe extern "C" fn dc_markseen_msgs(
block_on(message::markseen_msgs(&ctx, msg_ids));
}
#[no_mangle]
pub unsafe extern "C" fn dc_star_msgs(
context: *mut dc_context_t,
msg_ids: *const u32,
msg_cnt: libc::c_int,
star: libc::c_int,
) {
if context.is_null() || msg_ids.is_null() || msg_cnt <= 0 {
eprintln!("ignoring careless call to dc_star_msgs()");
return;
}
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
let ctx = &*context;
block_on(message::star_msgs(&ctx, msg_ids, star == 1));
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) -> *mut dc_msg_t {
if context.is_null() {
@@ -1827,7 +1800,7 @@ pub unsafe extern "C" fn dc_continue_key_transfer(
{
Ok(()) => 1,
Err(err) => {
error!(&ctx, "dc_continue_key_transfer: {}", err);
warn!(&ctx, "dc_continue_key_transfer: {}", err);
0
}
}
@@ -1891,8 +1864,13 @@ pub unsafe extern "C" fn dc_join_securejoin(
}
let ctx = &*context;
block_on(async move { securejoin::dc_join_securejoin(&ctx, &to_string_lossy(qr)).await })
.to_u32()
block_on(async move {
securejoin::dc_join_securejoin(&ctx, &to_string_lossy(qr))
.await
.map(|chatid| chatid.to_u32())
.log_err(ctx, "failed dc_join_securejoin() call")
.unwrap_or_default()
})
}
#[no_mangle]
@@ -1992,7 +1970,6 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
// dc_array_t
#[no_mangle]
pub type dc_array_t = dc_array::dc_array_t;
#[no_mangle]
@@ -2175,7 +2152,6 @@ pub struct ChatlistWrapper {
list: chatlist::Chatlist,
}
#[no_mangle]
pub type dc_chatlist_t = ChatlistWrapper;
#[no_mangle]
@@ -2299,7 +2275,6 @@ pub struct ChatWrapper {
chat: chat::Chat,
}
#[no_mangle]
pub type dc_chat_t = ChatWrapper;
#[no_mangle]
@@ -2426,13 +2401,13 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_verified(chat: *mut dc_chat_t) -> libc::c_int {
pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_verified()");
eprintln!("ignoring careless call to dc_chat_is_protected()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_verified() as libc::c_int
ffi_chat.chat.is_protected() as libc::c_int
}
#[no_mangle]
@@ -2525,7 +2500,6 @@ pub struct MessageWrapper {
message: message::Message,
}
#[no_mangle]
pub type dc_msg_t = MessageWrapper;
#[no_mangle]
@@ -2835,16 +2809,6 @@ pub unsafe extern "C" fn dc_msg_is_sent(msg: *mut dc_msg_t) -> libc::c_int {
ffi_msg.message.is_sent().into()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_is_starred(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_is_starred()");
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.is_starred().into()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_is_forwarded(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
@@ -2865,6 +2829,16 @@ pub unsafe extern "C" fn dc_msg_is_info(msg: *mut dc_msg_t) -> libc::c_int {
ffi_msg.message.is_info().into()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_info_type(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_info_type()");
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.get_info_type() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_is_increation(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
@@ -3010,6 +2984,74 @@ pub unsafe extern "C" fn dc_msg_latefiling_mediasize(
});
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_error(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_error()");
return ptr::null_mut();
}
let ffi_msg = &*msg;
match ffi_msg.message.error() {
Some(error) => error.strdup(),
None => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_set_quote(msg: *mut dc_msg_t, quote: *const dc_msg_t) {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_set_quote()");
return;
}
let ffi_msg = &mut *msg;
let ffi_quote = &*quote;
block_on(async move {
ffi_msg
.message
.set_quote(&*ffi_msg.context, &ffi_quote.message)
.await
.log_err(&*ffi_msg.context, "failed to set quote")
.ok();
});
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_quoted_text(msg: *const dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_quoted_text()");
return ptr::null_mut();
}
let ffi_msg: &MessageWrapper = &*msg;
ffi_msg
.message
.quoted_text()
.map_or_else(ptr::null_mut, |s| s.strdup())
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_quoted_msg(msg: *const dc_msg_t) -> *mut dc_msg_t {
if msg.is_null() {
eprintln!("ignoring careless call to dc_get_quoted_msg()");
return ptr::null_mut();
}
let ffi_msg: &MessageWrapper = &*msg;
let context = &*ffi_msg.context;
let res = block_on(async move {
ffi_msg
.message
.quoted_message(context)
.await
.log_err(context, "failed to get quoted message")
.unwrap_or(None)
});
match res {
Some(message) => Box::into_raw(Box::new(MessageWrapper { context, message })),
None => ptr::null_mut(),
}
}
// dc_contact_t
/// FFI struct for [dc_contact_t]
@@ -3024,7 +3066,6 @@ pub struct ContactWrapper {
contact: contact::Contact,
}
#[no_mangle]
pub type dc_contact_t = ContactWrapper;
#[no_mangle]
@@ -3157,7 +3198,6 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l
// dc_lot_t
#[no_mangle]
pub type dc_lot_t = lot::Lot;
#[no_mangle]
@@ -3266,7 +3306,8 @@ impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
fn log_err(self, ctx: &Context, message: &str) -> Result<T, E> {
self.map_err(|err| {
warn!(ctx, "{}: {}", message, err);
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
warn!(ctx, "{}: {:#}", message, err);
err
})
}
@@ -3298,7 +3339,6 @@ fn convert_and_prune_message_ids(msg_ids: *const u32, msg_cnt: libc::c_int) -> V
// dc_provider_t
#[no_mangle]
pub type dc_provider_t = provider::Provider;
#[no_mangle]
@@ -3389,7 +3429,8 @@ pub unsafe extern "C" fn dc_accounts_new(
match accs {
Ok(accs) => Box::into_raw(Box::new(accs)),
Err(err) => {
eprintln!("failed to create accounts: {}", err);
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
eprintln!("failed to create accounts: {:#}", err);
ptr::null_mut()
}
}
@@ -3450,7 +3491,7 @@ pub unsafe extern "C" fn dc_accounts_select_account(
let accounts = &*accounts;
block_on(accounts.select_account(id))
.map(|_| 1)
.unwrap_or_else(|_| 0)
.unwrap_or(0)
}
#[no_mangle]
@@ -3462,7 +3503,7 @@ pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -
let accounts = &*accounts;
block_on(accounts.add_account()).unwrap_or_else(|_| 0)
block_on(accounts.add_account()).unwrap_or(0)
}
#[no_mangle]
@@ -3564,7 +3605,6 @@ pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *mut dc_accounts_t)
block_on(accounts.maybe_network());
}
#[no_mangle]
pub type dc_accounts_event_emitter_t = deltachat::accounts::EventEmitter;
#[no_mangle]
@@ -3598,9 +3638,10 @@ pub unsafe extern "C" fn dc_accounts_get_next_event(
emitter: *mut dc_accounts_event_emitter_t,
) -> *mut dc_event_t {
if emitter.is_null() {
eprintln!("ignoring careless call to dc_accounts_get_next_event()");
return ptr::null_mut();
}
let emitter = &*emitter;
let emitter = &mut *emitter;
emitter
.recv_sync()

View File

@@ -4,7 +4,7 @@ use std::str::FromStr;
use anyhow::{bail, ensure};
use async_std::path::Path;
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility};
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, ProtectionStatus};
use deltachat::chatlist::*;
use deltachat::constants::*;
use deltachat::contact::*;
@@ -185,7 +185,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
let msgtext = msg.get_text();
println!(
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{} [{}]",
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{} [{}]",
prefix.as_ref(),
msg.get_id(),
if msg.get_showpadlock() { "🔒" } else { "" },
@@ -193,7 +193,6 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
&contact_name,
contact_id,
msgtext.unwrap_or_default(),
if msg.is_starred() { "" } else { "" },
if msg.get_from_id() == 1 as libc::c_uint {
""
} else if msg.get_state() == MessageState::InSeen {
@@ -358,7 +357,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
createchat <contact-id>\n\
createchatbymsg <msg-id>\n\
creategroup <name>\n\
createverified <name>\n\
createprotected <name>\n\
addmember <contact-id>\n\
removemember <contact-id>\n\
groupname <name>\n\
@@ -380,6 +379,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
unarchive <chat-id>\n\
pin <chat-id>\n\
unpin <chat-id>\n\
protect <chat-id>\n\
unprotect <chat-id>\n\
delchat <chat-id>\n\
===========================Message commands==\n\
listmsgs <query>\n\
@@ -387,8 +388,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
listfresh\n\
forward <msg-id> <chat-id>\n\
markseen <msg-id>\n\
star <msg-id>\n\
unstar <msg-id>\n\
delmsg <msg-id>\n\
===========================Contact commands==\n\
listcontacts [<query>]\n\
@@ -405,7 +404,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
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\
============================================="
@@ -527,7 +525,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
println!(
"{}#{}: {} [{} fresh] {}",
"{}#{}: {} [{} fresh] {}{}",
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
@@ -537,6 +535,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ChatVisibility::Archived => "📦",
ChatVisibility::Pinned => "📌",
},
if chat.is_protected() { "🛡️" } else { "" },
);
let lot = chatlist.get_summary(&context, i, Some(&chat)).await;
let statestr = if chat.visibility == ChatVisibility::Archived {
@@ -611,7 +610,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
format!("{} member(s)", members.len())
};
println!(
"{}#{}: {} [{}]{}{}",
"{}#{}: {} [{}]{}{} {}",
chat_prefix(sel_chat),
sel_chat.get_id(),
sel_chat.get_name(),
@@ -628,6 +627,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
},
_ => "".to_string(),
},
if sel_chat.is_protected() {
"🛡️"
} else {
""
},
);
log_msglist(&context, &msglist).await?;
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
@@ -658,15 +662,16 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"creategroup" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
chat::create_group_chat(&context, VerifiedStatus::Unverified, arg1).await?;
chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?;
println!("Group#{} created successfully.", chat_id);
}
"createverified" => {
"createprotected" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id = chat::create_group_chat(&context, VerifiedStatus::Verified, arg1).await?;
let chat_id =
chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?;
println!("VerifiedGroup#{} created successfully.", chat_id);
println!("Group#{} created and protected successfully.", chat_id);
}
"addmember" => {
ensure!(sel_chat.is_some(), "No chat selected");
@@ -876,9 +881,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
msg.set_text(Some(arg1.to_string()));
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
}
"updatedevicechats" => {
context.update_device_chats().await?;
}
"listmedia" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -910,7 +912,21 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"archive" => ChatVisibility::Archived,
"unarchive" | "unpin" => ChatVisibility::Normal,
"pin" => ChatVisibility::Pinned,
_ => panic!("Unexpected command (This should never happen)"),
_ => unreachable!("arg0={:?}", arg0),
},
)
.await?;
}
"protect" | "unprotect" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id
.set_protection(
&context,
match arg0 {
"protect" => ProtectionStatus::Protected,
"unprotect" => ProtectionStatus::Unprotected,
_ => unreachable!("arg0={:?}", arg0),
},
)
.await?;
@@ -949,12 +965,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
msg_ids[0] = MsgId::new(arg1.parse()?);
message::markseen_msgs(&context, msg_ids).await;
}
"star" | "unstar" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut msg_ids = vec![MsgId::new(0); 1];
msg_ids[0] = MsgId::new(arg1.parse()?);
message::star_msgs(&context, msg_ids, arg0 == "star").await;
}
"delmsg" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut ids = [MsgId::new(0); 1];
@@ -1092,11 +1102,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
seconds, device_cnt, server_cnt
);
}
"emptyserver" => {
ensure!(!arg1.is_empty(), "Argument <flags> missing");
message::dc_empty_server(&context, arg1.parse()?).await;
}
"" => (),
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
}

View File

@@ -80,11 +80,21 @@ fn receive_event(event: EventType) {
yellow.paint(format!("Received LOCATION_CHANGED(contact={:?})", contact))
);
}
EventType::ConfigureProgress(progress) => {
info!(
"{}",
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
);
EventType::ConfigureProgress { progress, comment } => {
if let Some(comment) = comment {
info!(
"{}",
yellow.paint(format!(
"Received CONFIGURE_PROGRESS({} ‰, {})",
progress, comment
))
);
} else {
info!(
"{}",
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
);
}
}
EventType::ImexProgress(progress) => {
info!(
@@ -410,7 +420,7 @@ async fn handle_cmd(
"joinqr" => {
ctx.start_io().await;
if !arg0.is_empty() {
dc_join_securejoin(&ctx, arg1).await;
dc_join_securejoin(&ctx, arg1).await?;
}
}
"exit" | "quit" => return Ok(ExitResult::Exit),

View File

@@ -10,7 +10,7 @@ use deltachat::EventType;
fn cb(event: EventType) {
match event {
EventType::ConfigureProgress(progress) => {
EventType::ConfigureProgress { progress, .. } => {
log::info!("progress: {}", progress);
}
EventType::Info(msg) => {

View File

@@ -77,6 +77,7 @@ def run_cmdline(argv=None, account_plugins=None):
ac.set_config("mvbox_move", "0")
ac.set_config("mvbox_watch", "0")
ac.set_config("sentbox_watch", "0")
ac.set_config("bot", "1")
configtracker = ac.configure()
configtracker.wait_finish()

View File

@@ -179,17 +179,6 @@ class Account(object):
if not self.is_configured():
raise ValueError("need to configure first")
def empty_server_folders(self, inbox=False, mvbox=False):
""" empty server folders. """
flags = 0
if inbox:
flags |= const.DC_EMPTY_INBOX
if mvbox:
flags |= const.DC_EMPTY_MVBOX
if not flags:
raise ValueError("no flags set")
lib.dc_empty_server(self._dc_context, flags)
def get_latest_backupfile(self, backupdir):
""" return the latest backup file in a given directory.
"""
@@ -216,7 +205,7 @@ class Account(object):
def create_contact(self, obj, name=None):
""" create a (new) Contact or return an existing one.
Calling this method will always resulut in the same
Calling this method will always result in the same
underlying contact id. If there already is a Contact
with that e-mail address, it is unblocked and its display
`name` is updated if specified.
@@ -347,6 +336,9 @@ class Account(object):
def get_deaddrop_chat(self):
return Chat(self, const.DC_CHAT_ID_DEADDROP)
def get_device_chat(self):
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
def get_message_by_id(self, msg_id):
""" return Message instance.
:param msg_id: integer id of this message.
@@ -557,6 +549,9 @@ class Account(object):
You may call `stop_scheduler`, `wait_shutdown` or `shutdown` after the
account is started.
If you are using this from a test, you may want to call
wait_all_initial_fetches() afterwards.
:raises MissingCredentials: if `addr` and `mail_pw` values are not set.
:raises ConfigureFailed: if the account could not be configured.

View File

@@ -57,10 +57,7 @@ class Chat(object):
: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
)
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
def is_deaddrop(self):
""" return true if this chat is a deaddrop chat.
@@ -85,12 +82,20 @@ class Chat(object):
"""
return not lib.dc_chat_is_unpromoted(self._dc_chat)
def is_verified(self):
""" return True if this chat is a verified group.
def can_send(self):
"""Check if messages can be sent to a give chat.
This is not true eg. for the deaddrop or for the device-talk
:returns: True if chat is verified, False otherwise.
:returns: True if the chat is writable, False otherwise
"""
return lib.dc_chat_is_verified(self._dc_chat)
return lib.dc_chat_can_send(self._dc_chat)
def is_protected(self):
""" return True if this chat is a protected chat.
:returns: True if chat is protected, False otherwise.
"""
return lib.dc_chat_is_protected(self._dc_chat)
def get_name(self):
""" return name of this chat.

View File

@@ -51,6 +51,10 @@ class Contact(object):
""" Return True if the contact is blocked. """
return lib.dc_contact_is_blocked(self._dc_contact)
def set_blocked(self, block=True):
""" Block or unblock a contact. """
return lib.dc_block_contact(self.account._dc_context, self.id, block)
def is_verified(self):
""" Return True if the contact is verified. """
return lib.dc_contact_is_verified(self._dc_contact)

View File

@@ -17,7 +17,8 @@ def iter_array(dc_array_t, constructor):
def from_dc_charpointer(obj):
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
if obj != ffi.NULL:
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
class DCLot:

View File

@@ -24,12 +24,13 @@ def dc_account_extra_configure(account):
""" Reset the account (we reuse accounts across tests)
and make 'account.direct_imap' available for direct IMAP ops.
"""
imap = DirectImap(account)
if imap.select_config_folder("mvbox"):
if not hasattr(account, "direct_imap"):
imap = DirectImap(account)
if imap.select_config_folder("mvbox"):
imap.delete(ALL, expunge=True)
assert imap.select_config_folder("inbox")
imap.delete(ALL, expunge=True)
assert imap.select_config_folder("inbox")
imap.delete(ALL, expunge=True)
setattr(account, "direct_imap", imap)
setattr(account, "direct_imap", imap)
@deltachat.global_hookimpl

View File

@@ -1,6 +1,7 @@
import threading
import time
import re
import os
from queue import Queue, Empty
import deltachat
@@ -48,6 +49,15 @@ class FFIEventLogger:
if self.logid:
locname += "-" + self.logid
s = "{:2.2f} [{}] {}".format(elapsed, locname, message)
if os.name == "posix":
WARN = '\033[93m'
ERROR = '\033[91m'
ENDC = '\033[0m'
if message.startswith("DC_EVENT_WARNING"):
s = WARN + s + ENDC
if message.startswith("DC_EVENT_ERROR"):
s = ERROR + s + ENDC
with self._loglock:
print(s, flush=True)
@@ -111,6 +121,15 @@ class FFIEventTracker:
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), self.account)
break
def wait_all_initial_fetches(self):
"""Has to be called after start_io() to wait for fetch_existing_msgs to run
so that new messages are not mistaken for old ones:
- ac1 and ac2 are created
- ac1 sends a message to ac2
- ac2 is still running FetchExsistingMsgs job and thinks it's an existing, old message
- therefore no DC_EVENT_INCOMING_MSG is sent"""
self.get_info_contains("Done fetching existing messages")
def wait_next_incoming_message(self):
""" wait for and return next incoming message. """
ev = self.get_matching("DC_EVENT_INCOMING_MSG")

View File

@@ -175,6 +175,27 @@ class Message(object):
if ts:
return datetime.utcfromtimestamp(ts)
@property
def quoted_text(self):
"""Text inside the quote
:returns: Quoted text"""
return from_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
@property
def quote(self):
"""Quote getter
:returns: Quoted message, if found in the database"""
msg = lib.dc_msg_get_quoted_msg(self._dc_msg)
if msg:
return Message(self.account, ffi.gc(msg, lib.dc_msg_unref))
@quote.setter
def quote(self, quoted_message):
"""Quote setter"""
lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg)
def get_mime_headers(self):
""" return mime-header object for an incoming message.

View File

@@ -342,19 +342,29 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
mvbox_move=account.get_config("mvbox_move"),
sentbox_watch=account.get_config("sentbox_watch"),
))
if hasattr(account, "direct_imap"):
# Attach the existing direct_imap. If we did not do this, a new one would be created and
# delete existing messages (see dc_account_extra_configure(configure))
ac.direct_imap = account.direct_imap
ac._configtracker = ac.configure()
return ac
def wait_configure_and_start_io(self):
started_accounts = []
for acc in self._accounts:
if hasattr(acc, "_configtracker"):
acc._configtracker.wait_finish()
acc._evtracker.consume_events()
acc.get_device_chat().mark_noticed()
del acc._configtracker
acc.set_config("bcc_self", "0")
if acc.is_configured() and not acc.is_started():
acc.start_io()
started_accounts.append(acc)
print("{}: {} account was successfully setup".format(
acc.get_config("displayname"), acc.get_config("addr")))
for acc in started_accounts:
acc._evtracker.wait_all_initial_fetches()
def run_bot_process(self, module, ffi=True):
fn = module.__file__

View File

@@ -20,6 +20,14 @@ class ImexTracker:
elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN":
self._imex_events.put(ffi_event.data2)
def wait_progress(self, target_progress, progress_timeout=60):
while True:
ev = self._imex_events.get(timeout=progress_timeout)
if isinstance(ev, int) and ev >= target_progress:
return ev
if ev == 1000 or ev == 0:
return None
def wait_finish(self, progress_timeout=60):
""" Return list of written files, raise ValueError if ExportFailed. """
files_written = []

View File

@@ -6,6 +6,7 @@ import queue
import time
from deltachat import const, Account
from deltachat.message import Message
from deltachat.tracker import ImexTracker
from deltachat.hookspec import account_hookimpl
from datetime import datetime, timedelta
@@ -166,6 +167,16 @@ class TestOfflineContact:
with pytest.raises(ValueError):
ac1.create_chat(ac3)
def test_contact_rename(self, acfactory):
ac1 = acfactory.get_configured_offline_account()
contact = ac1.create_contact("some1@example.com", name="some1")
chat = ac1.create_chat(contact)
assert chat.get_name() == "some1"
ac1.create_contact("some1@example.com", name="renamed")
ev = ac1._evtracker.get_matching("DC_EVENT_CHAT_MODIFIED")
assert ev.data1 == chat.id
assert chat.get_name() == "renamed"
class TestOfflineChat:
@pytest.fixture
@@ -466,6 +477,21 @@ class TestOfflineChat:
assert not res.is_ask_verifygroup()
assert res.contact_id == 10
def test_quote(self, chat1):
"""Offline quoting test"""
msg = Message.new_empty(chat1.account, "text")
msg.set_text("Multi\nline\nmessage")
assert msg.quoted_text is None
# Prepare message to assign it a Message-Id.
# Messages without Message-Id cannot be quoted.
msg = chat1.prepare_message(msg)
reply_msg = Message.new_empty(chat1.account, "text")
reply_msg.set_text("reply")
reply_msg.quote = msg
assert reply_msg.quoted_text == "Multi\nline\nmessage"
def test_group_chat_many_members_add_remove(self, ac1, lp):
lp.sec("ac1: creating group chat with 10 other members")
chat = ac1.create_group_chat(name="title1")
@@ -819,18 +845,12 @@ class TestOnlineAccount:
assert msg_in.text == "message2"
assert msg_in.is_forwarded()
def test_send_self_message_and_empty_folder(self, acfactory, lp):
def test_send_self_message(self, acfactory, lp):
ac1 = acfactory.get_one_online_account(mvbox=True, move=True)
lp.sec("ac1: create self chat")
chat = ac1.get_self_contact().create_chat()
chat.send_text("hello")
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
ac1.empty_server_folders(inbox=True, mvbox=True)
ev1 = ac1._evtracker.get_matching("DC_EVENT_IMAP_FOLDER_EMPTIED")
ev2 = ac1._evtracker.get_matching("DC_EVENT_IMAP_FOLDER_EMPTIED")
boxes = [ev1.data2, ev2.data2]
boxes.remove("INBOX")
assert len(boxes) == 1 and boxes[0].endswith("DeltaChat")
def test_send_and_receive_message_markseen(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -878,7 +898,13 @@ class TestOnlineAccount:
lp.sec("mark messages as seen on ac2, wait for changes on ac1")
ac2.direct_imap.idle_start()
ac1.direct_imap.idle_start()
ac2.mark_seen_messages([msg2, msg4])
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
assert msg2.chat.id == msg4.chat.id
assert ev.data1 == msg2.chat.id
assert ev.data2 == 0
ac2.direct_imap.idle_check(terminate=True)
lp.step("1")
for i in range(2):
@@ -986,7 +1012,10 @@ class TestOnlineAccount:
chat = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("sending multi-line non-unicode message from ac1 to ac2")
text1 = "hello\nworld"
text1 = (
"hello\nworld\nthis is a very long message that should be"
+ " wrapped using format=flowed and unwrapped on the receiver"
)
msg_out = chat.send_text(text1)
assert not msg_out.is_encrypted()
@@ -1004,7 +1033,68 @@ class TestOnlineAccount:
assert msg_in.text == text2
assert ac1.get_config("addr") in [x.addr for x in msg_in.chat.get_contacts()]
def test_no_draft_if_cant_send(self, acfactory):
"""Tests that no quote can be set if the user can't send to this chat"""
ac1 = acfactory.get_one_online_account()
device_chat = ac1.get_device_chat()
msg = Message.new_empty(ac1, "text")
device_chat.set_draft(msg)
assert not device_chat.can_send()
assert device_chat.get_draft() is None
def test_prefer_encrypt(self, acfactory, lp):
"""Test quorum rule for encryption preference in 1:1 and group chat."""
ac1, ac2, ac3 = acfactory.get_many_online_accounts(3)
ac1.set_config("e2ee_enabled", "0")
ac2.set_config("e2ee_enabled", "1")
ac3.set_config("e2ee_enabled", "0")
# Make sure we do not send a copy to ourselves. This is to
# test that we count own preference even when we are not in
# the recipient list.
ac1.set_config("bcc_self", "0")
ac2.set_config("bcc_self", "0")
ac3.set_config("bcc_self", "0")
acfactory.introduce_each_other([ac1, ac2, ac3])
lp.sec("ac1: sending message to ac2")
chat1 = ac1.create_chat(ac2)
msg1 = chat1.send_text("message1")
assert not msg1.is_encrypted()
ac2._evtracker.wait_next_incoming_message()
lp.sec("ac2: sending message to ac1")
chat2 = ac2.create_chat(ac1)
msg2 = chat2.send_text("message2")
assert not msg2.is_encrypted()
ac1._evtracker.wait_next_incoming_message()
lp.sec("ac1: sending message to group chat with ac2 and ac3")
group = ac1.create_group_chat("hello")
group.add_contact(ac2)
group.add_contact(ac3)
msg3 = group.send_text("message3")
assert not msg3.is_encrypted()
ac2._evtracker.wait_next_incoming_message()
ac3._evtracker.wait_next_incoming_message()
lp.sec("ac3: start preferring encryption and inform ac1")
ac3.set_config("e2ee_enabled", "1")
chat3 = ac3.create_chat(ac1)
msg4 = chat3.send_text("message4")
# ac1 still does not prefer encryption
assert not msg4.is_encrypted()
ac1._evtracker.wait_next_incoming_message()
lp.sec("ac1: sending another message to group chat with ac2 and ac3")
msg5 = group.send_text("message5")
# Majority prefers encryption now
assert msg5.is_encrypted()
def test_reply_encrypted(self, acfactory, lp):
"""Test that replies to encrypted messages are encrypted."""
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create chat with ac2")
@@ -1032,26 +1122,26 @@ class TestOnlineAccount:
print("ac2: e2ee_enabled={}".format(ac2.get_config("e2ee_enabled")))
ac1.set_config("e2ee_enabled", "0")
# Set unprepared and unencrypted draft to test that it is not
# taken into account when determining whether last message is
# encrypted.
msg_draft = Message.new_empty(ac1, "text")
msg_draft.set_text("message2 -- should be encrypted")
chat.set_draft(msg_draft)
for quoted_msg in msg1, msg3:
# Save the draft with a quote.
# It should be encrypted if quoted message is encrypted.
msg_draft = Message.new_empty(ac1, "text")
msg_draft.set_text("message reply")
msg_draft.quote = quoted_msg
chat.set_draft(msg_draft)
# Get the draft, prepare and send it.
msg_draft = chat.get_draft()
msg_out = chat.prepare_message(msg_draft)
chat.send_prepared(msg_out)
# Get the draft, prepare and send it.
msg_draft = chat.get_draft()
msg_out = chat.prepare_message(msg_draft)
chat.send_prepared(msg_out)
chat.set_draft(None)
assert chat.get_draft() is None
chat.set_draft(None)
assert chat.get_draft() is None
lp.sec("wait for ac2 to receive message")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
msg_in = ac2.get_message_by_id(ev.data2)
assert msg_in.text == "message2 -- should be encrypted"
assert msg_in.is_encrypted()
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.text == "message reply"
assert msg_in.quoted_text == quoted_msg.text
assert msg_in.is_encrypted() == quoted_msg.is_encrypted()
def test_saved_mime_on_received_message(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -1156,8 +1246,16 @@ class TestOnlineAccount:
backupdir = tmpdir.mkdir("backup")
lp.sec("export all to {}".format(backupdir))
path = ac1.export_all(backupdir.strpath)
assert os.path.exists(path)
with ac1.temp_plugin(ImexTracker()) as imex_tracker:
path = ac1.export_all(backupdir.strpath)
assert os.path.exists(path)
# check progress events for export
imex_tracker.wait_progress(250)
imex_tracker.wait_progress(500)
imex_tracker.wait_progress(750)
imex_tracker.wait_progress(1000)
# return mex_tracker.wait_finish()
t = time.time()
lp.sec("get fresh empty account")
@@ -1168,7 +1266,15 @@ class TestOnlineAccount:
assert path2 == path
lp.sec("import backup and check it's proper")
ac2.import_all(path)
with ac2.temp_plugin(ImexTracker()) as imex_tracker:
ac2.import_all(path)
# check progress events for import
imex_tracker.wait_progress(250)
imex_tracker.wait_progress(500)
imex_tracker.wait_progress(750)
imex_tracker.wait_progress(1000)
contacts = ac2.get_contacts(query="some1")
assert len(contacts) == 1
contact2 = contacts[0]
@@ -1264,7 +1370,7 @@ class TestOnlineAccount:
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat1 = ac1.create_group_chat("hello", verified=True)
assert chat1.is_verified()
assert chat1.is_protected()
qr = chat1.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
@@ -1283,7 +1389,7 @@ class TestOnlineAccount:
lp.sec("ac2: read message and check it's verified chat")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
assert msg.chat.is_verified()
assert msg.chat.is_protected()
assert msg.is_encrypted()
lp.sec("ac2: send message and let ac1 read it")
@@ -1402,6 +1508,48 @@ class TestOnlineAccount:
assert ev.action == "removed"
assert ev.message.get_sender_contact().addr == ac1_addr
def test_system_group_msg_from_blocked_user(self, acfactory, lp):
"""
Tests that a blocked user removes you from a group.
The message has to be fetched even though the user is blocked
to avoid inconsistent group state.
Also tests blocking in general.
"""
lp.sec("Create a group chat with ac1 and ac2")
(ac1, ac2) = acfactory.get_two_online_accounts()
acfactory.introduce_each_other((ac1, ac2))
chat_on_ac1 = ac1.create_group_chat("title", contacts=[ac2])
chat_on_ac1.send_text("First group message")
chat_on_ac2 = ac2._evtracker.wait_next_incoming_message().chat
lp.sec("ac1 blocks ac2")
contact = ac1.create_contact(ac2)
contact.set_blocked()
assert contact.is_blocked()
ev = ac1._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
assert ev.data1 == contact.id
lp.sec("ac2 sends a message to ac1 that does not arrive because it is blocked")
ac2.create_chat(ac1).send_text("This will not arrive!")
lp.sec("ac2 sends a group message to ac1 that arrives")
# Groups would be hardly usable otherwise: If you have blocked some
# users, they write messages and you only see replies to them without context
chat_on_ac2.send_text("This will arrive")
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "This will arrive"
message_texts = [m.text for m in chat_on_ac1.get_messages()]
assert len(message_texts) == 2
assert "First group message" in message_texts
assert "This will arrive" in message_texts
lp.sec("ac2 removes ac1 from their group")
assert ac1.get_self_contact() in chat_on_ac1.get_contacts()
assert contact.is_blocked()
chat_on_ac2.remove_contact(ac1)
ac1._evtracker.get_matching("DC_EVENT_CHAT_MODIFIED")
assert not ac1.get_self_contact() in chat_on_ac1.get_contacts()
def test_set_get_group_image(self, acfactory, data, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -1523,6 +1671,7 @@ class TestOnlineAccount:
chat41 = ac4.create_chat(ac1)
chat42 = ac4.create_chat(ac2)
ac4.start_io()
ac4._evtracker.wait_all_initial_fetches()
lp.sec("ac1: creating group chat with 2 other members")
chat = ac1.create_group_chat("title", contacts=[ac2, ac3])
@@ -1690,6 +1839,155 @@ class TestOnlineAccount:
assert len(imap2.get_all_messages()) == 1
def test_configure_error_msgs(self, acfactory):
ac1, configdict = acfactory.get_online_config()
ac1.update_config(configdict)
ac1.set_config("mail_pw", "abc") # Wrong mail pw
ac1.configure()
while True:
ev = ac1._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
if ev.data1 == 0:
break
# Password is wrong so it definitely has to say something about "password"
assert "password" in ev.data2
ac2, configdict = acfactory.get_online_config()
ac2.update_config(configdict)
ac2.set_config("addr", "abc@def.invalid") # mail server can't be reached
ac2.configure()
while True:
ev = ac2._evtracker.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
if ev.data1 == 0:
break
# Can't connect so it probably should say something about "internet"
# again, should not repeat itself
# If this fails then probably `e.msg.to_lowercase().contains("could not resolve")`
# in configure/mod.rs returned false because the error message was changed
# (i.e. did not contain "could not resolve" anymore)
assert (ev.data2.count("internet") + ev.data2.count("network")) == 1
# Should mention that it can't connect:
assert ev.data2.count("connect") == 1
# The users do not know what "configuration" is
assert "configuration" not in ev.data2.lower()
def test_name_changes(self, acfactory):
ac1, ac2 = acfactory.get_two_online_accounts()
ac1.set_config("displayname", "Account 1")
chat12 = acfactory.get_accepted_chat(ac1, ac2)
contact = None
def update_name():
"""Send a message from ac1 to ac2 to update the name"""
nonlocal contact
chat12.send_text("Hello")
msg = ac2._evtracker.wait_next_incoming_message()
contact = msg.get_sender_contact()
return contact.name
assert update_name() == "Account 1"
ac1.set_config("displayname", "Account 1 revision 2")
assert update_name() == "Account 1 revision 2"
# Explicitly rename contact on ac2 to "Renamed"
ac2.create_contact(contact, name="Renamed")
assert contact.name == "Renamed"
ev = ac2._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
assert ev.data1 == contact.id
# ac1 also renames itself into "Renamed"
assert update_name() == "Renamed"
ac1.set_config("displayname", "Renamed")
assert update_name() == "Renamed"
# Contact name was set to "Renamed" explicitly before,
# so it should not be changed.
ac1.set_config("displayname", "Renamed again")
updated_name = update_name()
if updated_name == "Renamed again":
# Known bug, mark as XFAIL
pytest.xfail("Contact was renamed after explicit rename")
else:
# No renames should happen after explicit rename
assert updated_name == "Renamed"
def test_group_quote(self, acfactory, lp):
"""Test quoting in a group with a new member who have not seen the quoted message."""
ac1, ac2, ac3 = accounts = acfactory.get_many_online_accounts(3)
acfactory.introduce_each_other(accounts)
chat = ac1.create_group_chat(name="quote group")
chat.add_contact(ac2)
lp.sec("ac1: sending message")
out_msg = chat.send_text("hello")
lp.sec("ac2: receiving message")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
chat.add_contact(ac3)
ac2._evtracker.wait_next_incoming_message()
ac3._evtracker.wait_next_incoming_message()
lp.sec("ac2: sending reply with a quote")
reply_msg = Message.new_empty(msg.chat.account, "text")
reply_msg.set_text("reply")
reply_msg.quote = msg
reply_msg = msg.chat.prepare_message(reply_msg)
assert reply_msg.quoted_text == "hello"
msg.chat.send_prepared(reply_msg)
lp.sec("ac3: receiving reply")
received_reply = ac3._evtracker.wait_next_incoming_message()
assert received_reply.text == "reply"
assert received_reply.quoted_text == "hello"
# ac3 was not in the group and has not received quoted message
assert received_reply.quote is None
lp.sec("ac1: receiving reply")
received_reply = ac1._evtracker.wait_next_incoming_message()
assert received_reply.text == "reply"
assert received_reply.quoted_text == "hello"
assert received_reply.quote.id == out_msg.id
@pytest.mark.parametrize("mvbox_move", [False, True])
def test_add_all_recipients_as_contacts(self, acfactory, lp, mvbox_move):
"""Delta Chat reads the recipients from old emails sent by the user and adds them as contacts.
This way, we can already offer them some email addresses they can write to.
Also test that existing emails are fetched during onboarding.
Lastly, tests that bcc_self messages moved to the mvbox are marked as read."""
ac1 = acfactory.get_online_configuring_account(mvbox=mvbox_move, move=mvbox_move)
ac2 = acfactory.get_online_configuring_account()
acfactory.wait_configure_and_start_io()
chat = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("send out message with bcc to ourselves")
if mvbox_move:
ac1.direct_imap.select_config_folder("mvbox")
ac1.direct_imap.idle_start()
ac1.set_config("bcc_self", "1")
chat.send_text("message text")
# now wait until the bcc_self message arrives
# Also test that bcc_self messages moved to the mvbox are marked as read.
assert ac1.direct_imap.idle_wait_for_seen()
ac1_clone = acfactory.clone_online_account(ac1)
ac1_clone._configtracker.wait_finish()
ac1_clone.start_io()
ac1_clone._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
ac2_addr = ac2.get_config("addr")
assert any(c.addr == ac2_addr for c in ac1_clone.get_contacts())
msg = ac1_clone._evtracker.wait_next_messages_changed()
assert msg.text == "message text"
class TestGroupStressTests:
def test_group_many_members_add_leave_remove(self, acfactory, lp):
@@ -1815,8 +2113,7 @@ class TestOnlineConfigureFails:
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK")
assert "cannot login" in ev.data2.lower()
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
def test_invalid_user(self, acfactory):
ac1, configdict = acfactory.get_online_config()
@@ -1824,8 +2121,7 @@ class TestOnlineConfigureFails:
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK")
assert "cannot login" in ev.data2.lower()
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")
def test_invalid_domain(self, acfactory):
ac1, configdict = acfactory.get_online_config()
@@ -1833,5 +2129,4 @@ class TestOnlineConfigureFails:
configtracker = ac1.configure()
configtracker.wait_progress(500)
configtracker.wait_progress(0)
ev = ac1._evtracker.get_matching("DC_EVENT_ERROR_NETWORK")
assert "could not connect" in ev.data2.lower()
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR_NETWORK")

View File

@@ -32,7 +32,7 @@ Messages SHOULD be encrypted by the
`prefer-encrypt=mutual` MAY be set by default.
Meta data (at least the subject and all chat-headers) SHOULD be encrypted
by the [Protected Headers](https://www.ietf.org/id/draft-autocrypt-lamps-protected-headers-02.html) standard.
by the [Protected Headers](https://tools.ietf.org/id/draft-autocrypt-lamps-protected-headers-02.html) standard.
# Outgoing messages

View File

@@ -1,10 +1,8 @@
use std::collections::HashMap;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::task::{Context as TaskContext, Poll};
use std::collections::BTreeMap;
use async_std::fs;
use async_std::path::PathBuf;
use async_std::prelude::*;
use async_std::sync::{Arc, RwLock};
use uuid::Uuid;
@@ -20,7 +18,7 @@ use crate::events::Event;
pub struct Accounts {
dir: PathBuf,
config: Config,
accounts: Arc<RwLock<HashMap<u32, Context>>>,
accounts: Arc<RwLock<BTreeMap<u32, Context>>>,
}
impl Accounts {
@@ -225,61 +223,42 @@ impl Accounts {
/// Unified event emitter.
pub async fn get_event_emitter(&self) -> EventEmitter {
let emitters = self
let emitters: Vec<_> = self
.accounts
.read()
.await
.iter()
.map(|(id, a)| EmitterWrapper {
id: *id,
emitter: a.get_event_emitter(),
done: AtomicBool::new(false),
})
.map(|(_id, a)| a.get_event_emitter())
.collect();
EventEmitter(emitters)
EventEmitter(futures::stream::select_all(emitters))
}
}
#[derive(Debug)]
pub struct EventEmitter(futures::stream::SelectAll<crate::events::EventEmitter>);
impl EventEmitter {
/// Blocking recv of an event. Return `None` if the `Sender` has been droped.
pub fn recv_sync(&self) -> Option<Event> {
/// Blocking recv of an event. Return `None` if all `Sender`s have been droped.
pub fn recv_sync(&mut self) -> Option<Event> {
async_std::task::block_on(self.recv())
}
/// Async recv of an event. Return `None` if the `Sender` has been droped.
pub async fn recv(&self) -> Option<Event> {
futures::future::poll_fn(|cx| Pin::new(self).recv_poll(cx)).await
}
fn recv_poll(self: Pin<&Self>, _cx: &mut TaskContext<'_>) -> Poll<Option<Event>> {
for e in &*self.0 {
if e.done.load(Ordering::Acquire) {
// skip emitters that are already done
continue;
}
match e.emitter.try_recv() {
Ok(event) => return Poll::Ready(Some(event)),
Err(async_std::sync::TryRecvError::Disconnected) => {
e.done.store(true, Ordering::Release);
}
Err(async_std::sync::TryRecvError::Empty) => {}
}
}
Poll::Pending
/// Async recv of an event. Return `None` if all `Sender`s have been droped.
pub async fn recv(&mut self) -> Option<Event> {
self.0.next().await
}
}
#[derive(Debug)]
pub struct EventEmitter(Vec<EmitterWrapper>);
impl async_std::stream::Stream for EventEmitter {
type Item = Event;
#[derive(Debug)]
struct EmitterWrapper {
id: u32,
emitter: crate::events::EventEmitter,
done: AtomicBool,
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
std::pin::Pin::new(&mut self.0).poll_next(cx)
}
}
pub const CONFIG_NAME: &str = "accounts.toml";
@@ -342,9 +321,9 @@ impl Config {
})
}
pub async fn load_accounts(&self) -> Result<HashMap<u32, Context>> {
pub async fn load_accounts(&self) -> Result<BTreeMap<u32, Context>> {
let cfg = &*self.inner.read().await;
let mut accounts = HashMap::with_capacity(cfg.accounts.len());
let mut accounts = BTreeMap::new();
for account_config in &cfg.accounts {
let ctx = Context::new(
cfg.os_name.clone(),
@@ -530,4 +509,23 @@ mod tests {
ctx.get_config(crate::config::Config::Addr).await.unwrap()
);
}
/// Tests that accounts are sorted by ID.
#[async_std::test]
async fn test_accounts_sorted() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
for expected_id in 2..10 {
let id = accounts.add_account().await.unwrap();
assert_eq!(id, expected_id);
}
let ids = accounts.get_all().await;
for (i, expected_id) in (1..10).enumerate() {
assert_eq!(ids.get(i), Some(&expected_id));
}
}
}

View File

@@ -685,7 +685,7 @@ mod tests {
let (stem, ext) = BlobObject::sanitise_name("foo?.bar");
assert!(stem.contains("foo"));
assert!(!stem.contains("?"));
assert!(!stem.contains('?'));
assert_eq!(ext, ".bar");
let (stem, ext) = BlobObject::sanitise_name("no-extension");
@@ -698,10 +698,10 @@ mod tests {
assert!(!stem.contains("ignored"));
assert!(stem.contains("this"));
assert!(stem.contains("forbidden"));
assert!(!stem.contains("/"));
assert!(!stem.contains("\\"));
assert!(!stem.contains(":"));
assert!(!stem.contains("*"));
assert!(!stem.contains("?"));
assert!(!stem.contains('/'));
assert!(!stem.contains('\\'));
assert!(!stem.contains(':'));
assert!(!stem.contains('*'));
assert!(!stem.contains('?'));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -362,9 +362,7 @@ impl Chatlist {
let mut lastcontact = None;
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id).await {
if lastmsg.from_id != DC_CONTACT_ID_SELF
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
{
if lastmsg.from_id != DC_CONTACT_ID_SELF && chat.typ == Chattype::Group {
lastcontact = Contact::load_from_db(context, lastmsg.from_id).await.ok();
}
@@ -440,13 +438,13 @@ mod tests {
#[async_std::test]
async fn test_try_load() {
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
let chat_id1 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat")
let chat_id2 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "b chat")
.await
.unwrap();
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat")
let chat_id3 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "c chat")
.await
.unwrap();
@@ -489,7 +487,7 @@ mod tests {
async fn test_sort_self_talk_up_on_forward() {
let t = TestContext::new().await;
t.ctx.update_device_chats().await.unwrap();
create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
@@ -546,7 +544,7 @@ mod tests {
#[async_std::test]
async fn test_get_summary_unwrap() {
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
let chat_id1 = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();

View File

@@ -9,6 +9,7 @@ use crate::constants::DC_VERSION_STR;
use crate::context::Context;
use crate::dc_tools::*;
use crate::events::EventType;
use crate::job;
use crate::message::MsgId;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::stock::StockMessage;
@@ -24,11 +25,13 @@ pub enum Config {
MailUser,
MailPw,
MailPort,
MailSecurity,
ImapCertificateChecks,
SendServer,
SendUser,
SendPw,
SendPort,
SendSecurity,
SmtpCertificateChecks,
ServerFlags,
@@ -66,6 +69,9 @@ pub enum Config {
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
MediaQuality,
#[strum(props(default = "1"))]
FetchExisting,
#[strum(props(default = "0"))]
KeyGenType,
@@ -118,9 +124,11 @@ pub enum Config {
#[strum(serialize = "sys.config_keys")]
SysConfigKeys,
#[strum(props(default = "0"))]
Bot,
/// Whether we send a warning if the password is wrong (set to false when we send a warning
/// because we do not want to send a second warning)
#[strum(props(default = "0"))]
NotifyAboutWrongPw,
/// address to webrtc instance to use for videochats
@@ -232,6 +240,15 @@ impl Context {
});
ret
}
Config::Displayname => {
let value = value.map(improve_single_line_input);
self.sql.set_raw_config(self, key, value.as_deref()).await
}
Config::DeleteServerAfter => {
let ret = self.sql.set_raw_config(self, key, value).await;
job::schedule_resync(self).await;
ret
}
_ => self.sql.set_raw_config(self, key, value).await,
}
}

View File

@@ -1,32 +1,31 @@
//! # Thunderbird's Autoconfiguration implementation
//!
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration
use quick_xml::events::{BytesStart, Event};
use std::io::BufRead;
use std::str::FromStr;
use crate::constants::*;
use crate::context::Context;
use crate::login_param::LoginParam;
use crate::provider::{Protocol, Socket};
use super::read_url::read_url;
use super::Error;
use super::{Error, ServerParams};
#[derive(Debug)]
struct MozAutoconfigure<'a> {
pub in_emailaddr: &'a str,
pub in_emaildomain: &'a str,
pub in_emaillocalpart: &'a str,
pub out: LoginParam,
pub out_imap_set: bool,
pub out_smtp_set: bool,
pub tag_server: MozServer,
pub tag_config: MozConfigTag,
struct Server {
pub typ: String,
pub hostname: String,
pub port: u16,
pub sockettype: Socket,
pub username: String,
}
#[derive(Debug, PartialEq)]
enum MozServer {
Undefined,
Imap,
Smtp,
#[derive(Debug)]
struct MozAutoconfigure {
pub incoming_servers: Vec<Server>,
pub outgoing_servers: Vec<Server>,
}
#[derive(Debug)]
@@ -38,10 +37,147 @@ enum MozConfigTag {
Username,
}
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);
impl Default for MozConfigTag {
fn default() -> Self {
Self::Undefined
}
}
impl FromStr for MozConfigTag {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_lowercase().as_ref() {
"hostname" => Ok(MozConfigTag::Hostname),
"port" => Ok(MozConfigTag::Port),
"sockettype" => Ok(MozConfigTag::Sockettype),
"username" => Ok(MozConfigTag::Username),
_ => Err(()),
}
}
}
/// Parses a single IncomingServer or OutgoingServer section.
fn parse_server<B: BufRead>(
reader: &mut quick_xml::Reader<B>,
server_event: &BytesStart,
) -> Result<Option<Server>, quick_xml::Error> {
let end_tag = String::from_utf8_lossy(server_event.name())
.trim()
.to_lowercase();
let typ = server_event
.attributes()
.find(|attr| {
attr.as_ref()
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "type")
.unwrap_or_default()
})
.map(|typ| {
typ.unwrap()
.unescape_and_decode_value(reader)
.unwrap_or_default()
.to_lowercase()
})
.unwrap_or_default();
let mut hostname = None;
let mut port = None;
let mut sockettype = Socket::Automatic;
let mut username = None;
let mut tag_config = MozConfigTag::Undefined;
let mut buf = Vec::new();
loop {
match reader.read_event(&mut buf)? {
Event::Start(ref event) => {
tag_config = String::from_utf8_lossy(event.name())
.parse()
.unwrap_or_default();
}
Event::End(ref event) => {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == end_tag {
break;
}
}
Event::Text(ref event) => {
let val = event
.unescape_and_decode(reader)
.unwrap_or_default()
.trim()
.to_owned();
match tag_config {
MozConfigTag::Hostname => hostname = Some(val),
MozConfigTag::Port => port = Some(val.parse().unwrap_or_default()),
MozConfigTag::Username => username = Some(val),
MozConfigTag::Sockettype => {
sockettype = match val.to_lowercase().as_ref() {
"ssl" => Socket::SSL,
"starttls" => Socket::STARTTLS,
"plain" => Socket::Plain,
_ => Socket::Automatic,
}
}
_ => {}
}
}
Event::Eof => break,
_ => (),
}
}
if let (Some(hostname), Some(port), Some(username)) = (hostname, port, username) {
Ok(Some(Server {
typ,
hostname,
port,
sockettype,
username,
}))
} else {
Ok(None)
}
}
fn parse_xml_reader<B: BufRead>(
reader: &mut quick_xml::Reader<B>,
) -> Result<MozAutoconfigure, quick_xml::Error> {
let mut incoming_servers = Vec::new();
let mut outgoing_servers = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event(&mut buf)? {
Event::Start(ref event) => {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == "incomingserver" {
if let Some(incoming_server) = parse_server(reader, event)? {
incoming_servers.push(incoming_server);
}
} else if tag == "outgoingserver" {
if let Some(outgoing_server) = parse_server(reader, event)? {
outgoing_servers.push(outgoing_server);
}
}
}
Event::Eof => break,
_ => (),
}
buf.clear();
}
Ok(MozAutoconfigure {
incoming_servers,
outgoing_servers,
})
}
/// Parses XML and fills in address and domain placeholders.
fn parse_xml_with_address(in_emailaddr: &str, xml_raw: &str) -> Result<MozAutoconfigure, Error> {
// Split address into local part and domain part.
let parts: Vec<&str> = in_emailaddr.rsplitn(2, '@').collect();
let (in_emaillocalpart, in_emaildomain) = match &parts[..] {
@@ -49,59 +185,78 @@ fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam, Error> {
_ => return Err(Error::InvalidEmailAddress(in_emailaddr.to_string())),
};
let mut moz_ac = MozAutoconfigure {
in_emailaddr,
in_emaildomain,
in_emaillocalpart,
out: LoginParam::new(),
out_imap_set: false,
out_smtp_set: false,
tag_server: MozServer::Undefined,
tag_config: MozConfigTag::Undefined,
let mut reader = quick_xml::Reader::from_str(xml_raw);
reader.trim_text(true);
let moz_ac = parse_xml_reader(&mut reader).map_err(|error| Error::InvalidXml {
position: reader.buffer_position(),
error,
})?;
let fill_placeholders = |val: &str| -> String {
val.replace("%EMAILADDRESS%", in_emailaddr)
.replace("%EMAILLOCALPART%", in_emaillocalpart)
.replace("%EMAILDOMAIN%", in_emaildomain)
};
let mut buf = Vec::new();
loop {
let event = reader
.read_event(&mut buf)
.map_err(|error| Error::InvalidXml {
position: reader.buffer_position(),
error,
})?;
match event {
quick_xml::events::Event::Start(ref e) => {
moz_autoconfigure_starttag_cb(e, &mut moz_ac, &reader)
}
quick_xml::events::Event::End(ref e) => moz_autoconfigure_endtag_cb(e, &mut moz_ac),
quick_xml::events::Event::Text(ref e) => {
moz_autoconfigure_text_cb(e, &mut moz_ac, &reader)
}
quick_xml::events::Event::Eof => break,
_ => (),
let fill_server_placeholders = |server: Server| -> Server {
Server {
typ: server.typ,
hostname: fill_placeholders(&server.hostname),
port: server.port,
sockettype: server.sockettype,
username: fill_placeholders(&server.username),
}
buf.clear();
}
};
if moz_ac.out.mail_server.is_empty()
|| moz_ac.out.mail_port == 0
|| moz_ac.out.send_server.is_empty()
|| moz_ac.out.send_port == 0
{
Err(Error::IncompleteAutoconfig(moz_ac.out))
} else {
Ok(moz_ac.out)
}
Ok(MozAutoconfigure {
incoming_servers: moz_ac
.incoming_servers
.into_iter()
.map(fill_server_placeholders)
.collect(),
outgoing_servers: moz_ac
.outgoing_servers
.into_iter()
.map(fill_server_placeholders)
.collect(),
})
}
pub async fn moz_autoconfigure(
/// Parses XML into `ServerParams` vector.
fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerParams>, Error> {
let moz_ac = parse_xml_with_address(in_emailaddr, xml_raw)?;
let res = moz_ac
.incoming_servers
.into_iter()
.chain(moz_ac.outgoing_servers.into_iter())
.filter_map(|server| {
let protocol = match server.typ.as_ref() {
"imap" => Some(Protocol::IMAP),
"smtp" => Some(Protocol::SMTP),
_ => None,
};
Some(ServerParams {
protocol: protocol?,
socket: server.sockettype,
hostname: server.hostname,
port: server.port,
username: server.username,
})
})
.collect();
Ok(res)
}
pub(crate) async fn moz_autoconfigure(
context: &Context,
url: &str,
param_in: &LoginParam,
) -> Result<LoginParam, Error> {
) -> Result<Vec<ServerParams>, Error> {
let xml_raw = read_url(context, url).await?;
let res = parse_xml(&param_in.addr, &xml_raw);
let res = parse_serverparams(&param_in.addr, &xml_raw);
if let Err(err) = &res {
warn!(
context,
@@ -111,212 +266,62 @@ pub async fn moz_autoconfigure(
res
}
fn moz_autoconfigure_text_cb<B: std::io::BufRead>(
event: &BytesText,
moz_ac: &mut MozAutoconfigure,
reader: &quick_xml::Reader<B>,
) {
let val = event.unescape_and_decode(reader).unwrap_or_default();
let addr = moz_ac.in_emailaddr;
let email_local = moz_ac.in_emaillocalpart;
let email_domain = moz_ac.in_emaildomain;
let val = val
.trim()
.replace("%EMAILADDRESS%", addr)
.replace("%EMAILLOCALPART%", email_local)
.replace("%EMAILDOMAIN%", email_domain);
match moz_ac.tag_server {
MozServer::Imap => match moz_ac.tag_config {
MozConfigTag::Hostname => moz_ac.out.mail_server = val,
MozConfigTag::Port => moz_ac.out.mail_port = val.parse().unwrap_or_default(),
MozConfigTag::Username => moz_ac.out.mail_user = val,
MozConfigTag::Sockettype => {
let val_lower = val.to_lowercase();
if val_lower == "ssl" {
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32
}
if val_lower == "starttls" {
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS as i32
}
if val_lower == "plain" {
moz_ac.out.server_flags |= DC_LP_IMAP_SOCKET_PLAIN as i32
}
}
_ => {}
},
MozServer::Smtp => match moz_ac.tag_config {
MozConfigTag::Hostname => moz_ac.out.send_server = val,
MozConfigTag::Port => moz_ac.out.send_port = val.parse().unwrap_or_default(),
MozConfigTag::Username => moz_ac.out.send_user = val,
MozConfigTag::Sockettype => {
let val_lower = val.to_lowercase();
if val_lower == "ssl" {
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32
}
if val_lower == "starttls" {
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32
}
if val_lower == "plain" {
moz_ac.out.server_flags |= DC_LP_SMTP_SOCKET_PLAIN as i32
}
}
_ => {}
},
MozServer::Undefined => {}
}
}
fn moz_autoconfigure_endtag_cb(event: &BytesEnd, moz_ac: &mut MozAutoconfigure) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == "incomingserver" {
if moz_ac.tag_server == MozServer::Imap {
moz_ac.out_imap_set = true;
}
moz_ac.tag_server = MozServer::Undefined;
moz_ac.tag_config = MozConfigTag::Undefined;
} else if tag == "outgoingserver" {
if moz_ac.tag_server == MozServer::Smtp {
moz_ac.out_smtp_set = true;
}
moz_ac.tag_server = MozServer::Undefined;
moz_ac.tag_config = MozConfigTag::Undefined;
} else {
moz_ac.tag_config = MozConfigTag::Undefined;
}
}
fn moz_autoconfigure_starttag_cb<B: std::io::BufRead>(
event: &BytesStart,
moz_ac: &mut MozAutoconfigure,
reader: &quick_xml::Reader<B>,
) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == "incomingserver" {
moz_ac.tag_server = if let Some(typ) = event.attributes().find(|attr| {
attr.as_ref()
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "type")
.unwrap_or_default()
}) {
let typ = typ
.unwrap()
.unescape_and_decode_value(reader)
.unwrap_or_default()
.to_lowercase();
if typ == "imap" && !moz_ac.out_imap_set {
MozServer::Imap
} else {
MozServer::Undefined
}
} else {
MozServer::Undefined
};
moz_ac.tag_config = MozConfigTag::Undefined;
} else if tag == "outgoingserver" {
moz_ac.tag_server = if !moz_ac.out_smtp_set {
MozServer::Smtp
} else {
MozServer::Undefined
};
moz_ac.tag_config = MozConfigTag::Undefined;
} else if tag == "hostname" {
moz_ac.tag_config = MozConfigTag::Hostname;
} else if tag == "port" {
moz_ac.tag_config = MozConfigTag::Port;
} else if tag == "sockettype" {
moz_ac.tag_config = MozConfigTag::Sockettype;
} else if tag == "username" {
moz_ac.tag_config = MozConfigTag::Username;
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
#[test]
fn test_parse_outlook_autoconfig() {
// Copied from https://autoconfig.thunderbird.net/v1.1/outlook.com on 2019-10-11
let xml_raw =
"<clientConfig version=\"1.1\">
<emailProvider id=\"outlook.com\">
<domain>hotmail.com</domain>
<domain>hotmail.co.uk</domain>
<domain>hotmail.co.jp</domain>
<domain>hotmail.com.br</domain>
<domain>hotmail.de</domain>
<domain>hotmail.fr</domain>
<domain>hotmail.it</domain>
<domain>hotmail.es</domain>
<domain>live.com</domain>
<domain>live.co.uk</domain>
<domain>live.co.jp</domain>
<domain>live.de</domain>
<domain>live.fr</domain>
<domain>live.it</domain>
<domain>live.jp</domain>
<domain>msn.com</domain>
<domain>outlook.com</domain>
<displayName>Outlook.com (Microsoft)</displayName>
<displayShortName>Outlook</displayShortName>
<incomingServer type=\"exchange\">
<hostname>outlook.office365.com</hostname>
<port>443</port>
<username>%EMAILADDRESS%</username>
<socketType>SSL</socketType>
<authentication>OAuth2</authentication>
<owaURL>https://outlook.office365.com/owa/</owaURL>
<ewsURL>https://outlook.office365.com/ews/exchange.asmx</ewsURL>
<useGlobalPreferredServer>true</useGlobalPreferredServer>
</incomingServer>
<incomingServer type=\"imap\">
<hostname>outlook.office365.com</hostname>
<port>993</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<incomingServer type=\"pop3\">
<hostname>outlook.office365.com</hostname>
<port>995</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
<pop3>
<leaveMessagesOnServer>true</leaveMessagesOnServer>
<!-- Outlook.com docs specifically mention that POP3 deletes have effect on the main inbox on webmail and IMAP -->
</pop3>
</incomingServer>
<outgoingServer type=\"smtp\">
<hostname>smtp.office365.com</hostname>
<port>587</port>
<socketType>STARTTLS</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</outgoingServer>
<documentation url=\"http://windows.microsoft.com/en-US/windows/outlook/send-receive-from-app\">
<descr lang=\"en\">Set up an email app with Outlook.com</descr>
</documentation>
</emailProvider>
<webMail>
<loginPage url=\"https://www.outlook.com/\"/>
<loginPageInfo url=\"https://www.outlook.com/\">
<username>%EMAILADDRESS%</username>
<usernameField id=\"i0116\" name=\"login\"/>
<passwordField id=\"i0118\" name=\"passwd\"/>
<loginButton id=\"idSIButton9\" name=\"SI\"/>
</loginPageInfo>
</webMail>
</clientConfig>";
let res = parse_xml("example@outlook.com", xml_raw).expect("XML parsing failed");
assert_eq!(res.mail_server, "outlook.office365.com");
assert_eq!(res.mail_port, 993);
assert_eq!(res.send_server, "smtp.office365.com");
assert_eq!(res.send_port, 587);
let xml_raw = include_str!("../../test-data/autoconfig/outlook.com.xml");
let res = parse_serverparams("example@outlook.com", xml_raw).expect("XML parsing failed");
assert_eq!(res[0].protocol, Protocol::IMAP);
assert_eq!(res[0].hostname, "outlook.office365.com");
assert_eq!(res[0].port, 993);
assert_eq!(res[1].protocol, Protocol::SMTP);
assert_eq!(res[1].hostname, "smtp.office365.com");
assert_eq!(res[1].port, 587);
}
#[test]
fn test_parse_lakenet_autoconfig() {
let xml_raw = include_str!("../../test-data/autoconfig/lakenet.ch.xml");
let res =
parse_xml_with_address("example@lakenet.ch", xml_raw).expect("XML parsing failed");
assert_eq!(res.incoming_servers.len(), 4);
assert_eq!(res.incoming_servers[0].typ, "imap");
assert_eq!(res.incoming_servers[0].hostname, "mail.lakenet.ch");
assert_eq!(res.incoming_servers[0].port, 993);
assert_eq!(res.incoming_servers[0].sockettype, Socket::SSL);
assert_eq!(res.incoming_servers[0].username, "example@lakenet.ch");
assert_eq!(res.incoming_servers[1].typ, "imap");
assert_eq!(res.incoming_servers[1].hostname, "mail.lakenet.ch");
assert_eq!(res.incoming_servers[1].port, 143);
assert_eq!(res.incoming_servers[1].sockettype, Socket::STARTTLS);
assert_eq!(res.incoming_servers[1].username, "example@lakenet.ch");
assert_eq!(res.incoming_servers[2].typ, "pop3");
assert_eq!(res.incoming_servers[2].hostname, "mail.lakenet.ch");
assert_eq!(res.incoming_servers[2].port, 995);
assert_eq!(res.incoming_servers[2].sockettype, Socket::SSL);
assert_eq!(res.incoming_servers[2].username, "example@lakenet.ch");
assert_eq!(res.incoming_servers[3].typ, "pop3");
assert_eq!(res.incoming_servers[3].hostname, "mail.lakenet.ch");
assert_eq!(res.incoming_servers[3].port, 110);
assert_eq!(res.incoming_servers[3].sockettype, Socket::STARTTLS);
assert_eq!(res.incoming_servers[3].username, "example@lakenet.ch");
assert_eq!(res.outgoing_servers.len(), 1);
assert_eq!(res.outgoing_servers[0].typ, "smtp");
assert_eq!(res.outgoing_servers[0].hostname, "mail.lakenet.ch");
assert_eq!(res.outgoing_servers[0].port, 587);
assert_eq!(res.outgoing_servers[0].sockettype, Socket::STARTTLS);
assert_eq!(res.outgoing_servers[0].username, "example@lakenet.ch");
}
}

View File

@@ -1,122 +1,194 @@
//! Outlook's Autodiscover
//! # Outlook's Autodiscover
//!
//! This module implements autoconfiguration via POX (Plain Old XML) interface to Autodiscover
//! Service. Newer SOAP interface, introduced in Exchange 2010, is not used.
use quick_xml::events::BytesEnd;
use quick_xml::events::Event;
use std::io::BufRead;
use crate::constants::*;
use crate::context::Context;
use crate::login_param::LoginParam;
use crate::provider::{Protocol, Socket};
use super::read_url::read_url;
use super::Error;
use super::{Error, ServerParams};
struct OutlookAutodiscover {
pub out: LoginParam,
pub out_imap_set: bool,
pub out_smtp_set: bool,
pub config_type: Option<String>,
pub config_server: String,
pub config_port: i32,
pub config_ssl: String,
pub config_redirecturl: Option<String>,
/// Result of parsing a single `Protocol` tag.
///
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/protocol-pox
#[derive(Debug)]
struct ProtocolTag {
/// Server type, such as "IMAP", "SMTP" or "POP3".
///
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/type-pox
pub typ: String,
/// Server identifier, hostname or IP address for IMAP and SMTP.
///
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/server-pox
pub server: String,
/// Network port.
///
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/port-pox
pub port: u16,
/// Whether connection should be secure, "on" or "off", default is "on".
///
/// https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/ssl-pox
pub ssl: bool,
}
enum ParsingResult {
LoginParam(LoginParam),
Protocols(Vec<ProtocolTag>),
/// XML redirect via `RedirectUrl` tag.
RedirectUrl(String),
}
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
let mut outlk_ad = OutlookAutodiscover {
out: LoginParam::new(),
out_imap_set: false,
out_smtp_set: false,
config_type: None,
config_server: String::new(),
config_port: 0,
config_ssl: String::new(),
config_redirecturl: None,
};
let mut reader = quick_xml::Reader::from_str(&xml_raw);
reader.trim_text(true);
/// Parses a single Protocol section.
fn parse_protocol<B: BufRead>(
reader: &mut quick_xml::Reader<B>,
) -> Result<Option<ProtocolTag>, quick_xml::Error> {
let mut protocol_type = None;
let mut protocol_server = None;
let mut protocol_port = None;
let mut protocol_ssl = true;
let mut buf = Vec::new();
let mut current_tag: Option<String> = None;
loop {
let event = reader
.read_event(&mut buf)
.map_err(|error| Error::InvalidXml {
position: reader.buffer_position(),
error,
})?;
match event {
quick_xml::events::Event::Start(ref e) => {
let tag = String::from_utf8_lossy(e.name()).trim().to_lowercase();
match reader.read_event(&mut buf)? {
Event::Start(ref event) => {
current_tag = Some(String::from_utf8_lossy(event.name()).trim().to_lowercase());
}
Event::End(ref event) => {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == "protocol" {
outlk_ad.config_type = None;
outlk_ad.config_server = String::new();
outlk_ad.config_port = 0;
outlk_ad.config_ssl = String::new();
outlk_ad.config_redirecturl = None;
break;
}
if Some(tag) == current_tag {
current_tag = None;
} else {
current_tag = Some(tag);
}
}
quick_xml::events::Event::End(ref e) => {
outlk_autodiscover_endtag_cb(e, &mut outlk_ad);
current_tag = None;
}
quick_xml::events::Event::Text(ref e) => {
Event::Text(ref e) => {
let val = e.unescape_and_decode(&reader).unwrap_or_default();
if let Some(ref tag) = current_tag {
match tag.as_str() {
"type" => {
outlk_ad.config_type = Some(val.trim().to_lowercase().to_string())
"type" => protocol_type = Some(val.trim().to_string()),
"server" => protocol_server = Some(val.trim().to_string()),
"port" => protocol_port = Some(val.trim().parse().unwrap_or_default()),
"ssl" => {
protocol_ssl = match val.trim() {
"on" => true,
"off" => false,
_ => true,
}
}
"server" => outlk_ad.config_server = val.trim().to_string(),
"port" => outlk_ad.config_port = val.trim().parse().unwrap_or_default(),
"ssl" => outlk_ad.config_ssl = val.trim().to_string(),
"redirecturl" => outlk_ad.config_redirecturl = Some(val.trim().to_string()),
_ => {}
};
}
}
quick_xml::events::Event::Eof => break,
Event::Eof => break,
_ => {}
}
}
if let (Some(protocol_type), Some(protocol_server), Some(protocol_port)) =
(protocol_type, protocol_server, protocol_port)
{
Ok(Some(ProtocolTag {
typ: protocol_type,
server: protocol_server,
port: protocol_port,
ssl: protocol_ssl,
}))
} else {
Ok(None)
}
}
/// Parses `RedirectUrl` tag.
fn parse_redirecturl<B: BufRead>(
reader: &mut quick_xml::Reader<B>,
) -> Result<String, quick_xml::Error> {
let mut buf = Vec::new();
match reader.read_event(&mut buf)? {
Event::Text(ref e) => {
let val = e.unescape_and_decode(&reader).unwrap_or_default();
Ok(val.trim().to_string())
}
_ => Ok("".to_string()),
}
}
fn parse_xml_reader<B: BufRead>(
reader: &mut quick_xml::Reader<B>,
) -> Result<ParsingResult, quick_xml::Error> {
let mut protocols = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event(&mut buf)? {
Event::Start(ref e) => {
let tag = String::from_utf8_lossy(e.name()).trim().to_lowercase();
if tag == "protocol" {
if let Some(protocol) = parse_protocol(reader)? {
protocols.push(protocol);
}
} else if tag == "redirecturl" {
let redirecturl = parse_redirecturl(reader)?;
return Ok(ParsingResult::RedirectUrl(redirecturl));
}
}
Event::Eof => break,
_ => (),
}
buf.clear();
}
// XML redirect via redirecturl
let res = if outlk_ad.config_redirecturl.is_none()
|| outlk_ad.config_redirecturl.as_ref().unwrap().is_empty()
{
if outlk_ad.out.mail_server.is_empty()
|| outlk_ad.out.mail_port == 0
|| outlk_ad.out.send_server.is_empty()
|| outlk_ad.out.send_port == 0
{
return Err(Error::IncompleteAutoconfig(outlk_ad.out));
}
ParsingResult::LoginParam(outlk_ad.out)
} else {
ParsingResult::RedirectUrl(outlk_ad.config_redirecturl.unwrap())
};
Ok(res)
Ok(ParsingResult::Protocols(protocols))
}
pub async fn outlk_autodiscover(
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
let mut reader = quick_xml::Reader::from_str(&xml_raw);
reader.trim_text(true);
parse_xml_reader(&mut reader).map_err(|error| Error::InvalidXml {
position: reader.buffer_position(),
error,
})
}
fn protocols_to_serverparams(protocols: Vec<ProtocolTag>) -> Vec<ServerParams> {
protocols
.into_iter()
.filter_map(|protocol| {
Some(ServerParams {
protocol: match protocol.typ.to_lowercase().as_ref() {
"imap" => Some(Protocol::IMAP),
"smtp" => Some(Protocol::SMTP),
_ => None,
}?,
socket: match protocol.ssl {
true => Socket::Automatic,
false => Socket::Plain,
},
hostname: protocol.server,
port: protocol.port,
username: String::new(),
})
})
.collect()
}
pub(crate) async fn outlk_autodiscover(
context: &Context,
url: &str,
_param_in: &LoginParam,
) -> Result<LoginParam, Error> {
) -> Result<Vec<ServerParams>, 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 {
@@ -127,47 +199,18 @@ pub async fn outlk_autodiscover(
}
match res? {
ParsingResult::RedirectUrl(redirect_url) => url = redirect_url,
ParsingResult::LoginParam(login_param) => return Ok(login_param),
ParsingResult::Protocols(protocols) => {
return Ok(protocols_to_serverparams(protocols));
}
}
}
Err(Error::RedirectionError)
}
fn outlk_autodiscover_endtag_cb(event: &BytesEnd, outlk_ad: &mut OutlookAutodiscover) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
if tag == "protocol" {
if let Some(type_) = &outlk_ad.config_type {
let port = outlk_ad.config_port;
let ssl_on = outlk_ad.config_ssl == "on";
let ssl_off = outlk_ad.config_ssl == "off";
if type_ == "imap" && !outlk_ad.out_imap_set {
outlk_ad.out.mail_server =
std::mem::replace(&mut outlk_ad.config_server, String::new());
outlk_ad.out.mail_port = port;
if ssl_on {
outlk_ad.out.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32
} else if ssl_off {
outlk_ad.out.server_flags |= DC_LP_IMAP_SOCKET_PLAIN as i32
}
outlk_ad.out_imap_set = true
} else if type_ == "smtp" && !outlk_ad.out_smtp_set {
outlk_ad.out.send_server =
std::mem::replace(&mut outlk_ad.config_server, String::new());
outlk_ad.out.send_port = outlk_ad.config_port;
if ssl_on {
outlk_ad.out.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32
} else if ssl_off {
outlk_ad.out.server_flags |= DC_LP_SMTP_SOCKET_PLAIN as i32
}
outlk_ad.out_smtp_set = true
}
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
#[test]
@@ -184,16 +227,13 @@ mod tests {
</Response>
</Autodiscover>
").expect("XML is not parsed successfully");
match res {
ParsingResult::LoginParam(_lp) => {
panic!("redirecturl is not found");
}
ParsingResult::RedirectUrl(url) => {
assert_eq!(
url,
"https://mail.example.com/autodiscover/autodiscover.xml"
);
}
if let ParsingResult::RedirectUrl(url) = res {
assert_eq!(
url,
"https://mail.example.com/autodiscover/autodiscover.xml"
);
} else {
panic!("redirecturl is not found");
}
}
@@ -228,11 +268,16 @@ mod tests {
.expect("XML is not parsed successfully");
match res {
ParsingResult::LoginParam(lp) => {
assert_eq!(lp.mail_server, "example.com");
assert_eq!(lp.mail_port, 993);
assert_eq!(lp.send_server, "smtp.example.com");
assert_eq!(lp.send_port, 25);
ParsingResult::Protocols(protocols) => {
assert_eq!(protocols[0].typ, "IMAP");
assert_eq!(protocols[0].server, "example.com");
assert_eq!(protocols[0].port, 993);
assert_eq!(protocols[0].ssl, true);
assert_eq!(protocols[1].typ, "SMTP");
assert_eq!(protocols[1].server, "smtp.example.com");
assert_eq!(protocols[1].port, 25);
assert_eq!(protocols[1].ssl, false);
}
ParsingResult::RedirectUrl(_) => {
panic!("RedirectUrl is not expected");

View File

@@ -3,32 +3,45 @@
mod auto_mozilla;
mod auto_outlook;
mod read_url;
mod server_params;
use anyhow::{bail, ensure, Context as _, Result};
use async_std::prelude::*;
use async_std::task;
use itertools::Itertools;
use job::Action;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::imap::Imap;
use crate::login_param::{CertificateChecks, LoginParam};
use crate::login_param::{LoginParam, ServerLoginParam};
use crate::message::Message;
use crate::oauth2::*;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::smtp::Smtp;
use crate::stock::StockMessage;
use crate::{chat, e2ee, provider};
use crate::{constants::*, job};
use crate::{context::Context, param::Params};
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
use server_params::{expand_param_vector, ServerParams};
macro_rules! progress {
($context:tt, $progress:expr) => {
($context:tt, $progress:expr, $comment:expr) => {
assert!(
$progress <= 1000,
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
);
$context.emit_event($crate::events::EventType::ConfigureProgress($progress));
$context.emit_event($crate::events::EventType::ConfigureProgress {
progress: $progress,
comment: $comment,
});
};
($context:tt, $progress:expr) => {
progress!($context, $progress, None);
};
}
@@ -107,7 +120,18 @@ impl Context {
Ok(())
}
Err(err) => {
progress!(self, 0);
progress!(
self,
0,
Some(
self.stock_string_repl_str(
StockMessage::ConfigurationFailed,
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
format!("{:#}", err),
)
.await
)
);
Err(err)
}
}
@@ -115,20 +139,40 @@ impl Context {
}
async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let mut param_autoconfig: Option<LoginParam> = None;
let mut keep_flags = 0;
// Read login parameters from the database
progress!(ctx, 1);
// Check basic settings.
ensure!(!param.addr.is_empty(), "Please enter an email address.");
// Only check for IMAP password, SMTP password is an "advanced" setting.
ensure!(!param.imap.password.is_empty(), "Please enter a password.");
if param.smtp.password.is_empty() {
param.smtp.password = param.imap.password.clone()
}
// Normalize authentication flags.
let oauth2 = match param.server_flags & DC_LP_AUTH_FLAGS as i32 {
DC_LP_AUTH_OAUTH2 => true,
DC_LP_AUTH_NORMAL => false,
_ => false,
};
param.server_flags &= !(DC_LP_AUTH_FLAGS as i32);
param.server_flags |= if oauth2 {
DC_LP_AUTH_OAUTH2 as i32
} else {
DC_LP_AUTH_NORMAL as i32
};
let ctx2 = ctx.clone();
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
// Step 1: Load the parameters and check email-address and password
if 0 != param.server_flags & DC_LP_AUTH_OAUTH2 {
if oauth2 {
// the used oauth2 addr may differ, check this.
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
progress!(ctx, 10);
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, &param.addr, &param.mail_pw)
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, &param.addr, &param.imap.password)
.await
.and_then(|e| e.parse().ok())
{
@@ -149,95 +193,141 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// Step 2: Autoconfig
progress!(ctx, 200);
// param.mail_user.is_empty() -- the user can enter a loginname which is used by autoconfig then
// param.send_pw.is_empty() -- the password cannot be auto-configured and is no criterion for
// autoconfig or not
if param.mail_server.is_empty()
&& param.mail_port == 0
&& param.send_server.is_empty()
&& param.send_port == 0
&& param.send_user.is_empty()
&& (param.server_flags & !DC_LP_AUTH_OAUTH2) == 0
let param_autoconfig;
if param.imap.server.is_empty()
&& param.imap.port == 0
&& param.imap.security == Socket::Automatic
&& param.imap.user.is_empty()
&& param.smtp.server.is_empty()
&& param.smtp.port == 0
&& param.smtp.security == Socket::Automatic
&& param.smtp.user.is_empty()
{
// no advanced parameters entered by the user: query provider-database or do Autoconfig
keep_flags = param.server_flags & DC_LP_AUTH_OAUTH2;
if let Some(new_param) = get_offline_autoconfig(ctx, &param) {
// got parameters from our provider-database, skip Autoconfig, preserve the OAuth2 setting
param_autoconfig = Some(new_param);
}
if param_autoconfig.is_none() {
if let Some(servers) = get_offline_autoconfig(ctx, &param.addr) {
param_autoconfig = Some(servers);
} else {
param_autoconfig =
get_autoconfig(ctx, param, &param_domain, &param_addr_urlencoded).await;
}
} else {
param_autoconfig = None;
}
// C. Do we have any autoconfig result?
progress!(ctx, 500);
if let Some(ref cfg) = param_autoconfig {
info!(ctx, "Got autoconfig: {}", &cfg);
if !cfg.mail_user.is_empty() {
param.mail_user = cfg.mail_user.clone();
}
// all other values are always NULL when entering autoconfig
param.mail_server = cfg.mail_server.clone();
param.mail_port = cfg.mail_port;
param.send_server = cfg.send_server.clone();
param.send_port = cfg.send_port;
param.send_user = cfg.send_user.clone();
param.server_flags = cfg.server_flags;
// although param_autoconfig's data are no longer needed from,
// it is used to later to prevent trying variations of port/server/logins
}
param.server_flags |= keep_flags;
// Step 3: Fill missing fields with defaults
if param.send_user.is_empty() {
param.send_user = param.mail_user.clone();
}
if param.send_pw.is_empty() {
param.send_pw = param.mail_pw.clone()
}
if !dc_exactly_one_bit_set(param.server_flags & DC_LP_IMAP_SOCKET_FLAGS as i32) {
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32);
}
if !dc_exactly_one_bit_set(param.server_flags & (DC_LP_SMTP_SOCKET_FLAGS as i32)) {
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
}
if !dc_exactly_one_bit_set(param.server_flags & DC_LP_AUTH_FLAGS as i32) {
param.server_flags &= !(DC_LP_AUTH_FLAGS as i32);
param.server_flags |= DC_LP_AUTH_NORMAL as i32
}
// do we have a complete configuration?
ensure!(
!param.mail_pw.is_empty() && !param.send_pw.is_empty(),
"Account settings incomplete."
let servers = expand_param_vector(
param_autoconfig.unwrap_or_else(|| {
vec![
ServerParams {
protocol: Protocol::IMAP,
hostname: param.imap.server.clone(),
port: param.imap.port,
socket: param.imap.security,
username: param.imap.user.clone(),
},
ServerParams {
protocol: Protocol::SMTP,
hostname: param.smtp.server.clone(),
port: param.smtp.port,
socket: param.smtp.security,
username: param.smtp.user.clone(),
},
]
}),
&param.addr,
&param_domain,
);
progress!(ctx, 550);
// Spawn SMTP configuration task
let mut smtp = Smtp::new();
let context_smtp = ctx.clone();
let mut smtp_param = param.smtp.clone();
let smtp_addr = param.addr.clone();
let smtp_servers: Vec<ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::SMTP)
.cloned()
.collect();
let smtp_config_task = task::spawn(async move {
let mut smtp_configured = false;
let mut errors = Vec::new();
for smtp_server in smtp_servers {
smtp_param.user = smtp_server.username.clone();
smtp_param.server = smtp_server.hostname.clone();
smtp_param.port = smtp_server.port;
smtp_param.security = smtp_server.socket;
match try_smtp_one_param(&context_smtp, &smtp_param, &smtp_addr, oauth2, &mut smtp)
.await
{
Ok(_) => {
smtp_configured = true;
break;
}
Err(e) => errors.push(e),
}
}
if smtp_configured {
Ok(smtp_param)
} else {
Err(errors)
}
});
progress!(ctx, 600);
// try to connect to IMAP - if we did not got an autoconfig,
// do some further tries with different settings and username variations
// Configure IMAP
let (_s, r) = async_std::sync::channel(1);
let mut imap = Imap::new(r);
if param_autoconfig.is_some() {
if try_imap_one_param(ctx, &param, &mut imap).await.is_err() {
bail!("IMAP autoconfig did not succeed");
}
} else {
*param = try_imap_hostnames(ctx, param.clone(), &mut imap).await?;
}
progress!(ctx, 750);
let mut imap_configured = false;
let imap_servers: Vec<&ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::IMAP)
.collect();
let imap_servers_count = imap_servers.len();
let mut errors = Vec::new();
for (imap_server_index, imap_server) in imap_servers.into_iter().enumerate() {
param.imap.user = imap_server.username.clone();
param.imap.server = imap_server.hostname.clone();
param.imap.port = imap_server.port;
param.imap.security = imap_server.socket;
let mut smtp = Smtp::new();
if param_autoconfig.is_some() {
if try_smtp_one_param(ctx, &param, &mut smtp).await.is_err() {
bail!("SMTP autoconfig did not succeed");
match try_imap_one_param(ctx, &param.imap, &param.addr, oauth2, &mut imap).await {
Ok(_) => {
imap_configured = true;
break;
}
Err(e) => errors.push(e),
}
} else {
*param = try_smtp_hostnames(ctx, param.clone(), &mut smtp).await?;
progress!(
ctx,
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
);
}
if !imap_configured {
bail!(nicer_configuration_error(ctx, errors).await);
}
progress!(ctx, 850);
// Wait for SMTP configuration
match smtp_config_task.await {
Ok(smtp_param) => {
param.smtp = smtp_param;
}
Err(errors) => {
bail!(nicer_configuration_error(ctx, errors).await);
}
}
progress!(ctx, 900);
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await
@@ -263,7 +353,14 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
e2ee::ensure_secret_key_exists(ctx).await?;
info!(ctx, "key generation completed");
job::add(
ctx,
job::Job::new(Action::FetchExistingMsgs, 0, Params::new(), 0),
)
.await;
progress!(ctx, 940);
update_device_chats_handle.await?;
Ok(())
}
@@ -306,8 +403,8 @@ impl AutoconfigSource {
AutoconfigSource {
provider: AutoconfigProvider::Outlook,
url: format!(
"https://{}{}/autodiscover/autodiscover.xml",
"autodiscover.", domain
"https://autodiscover.{}/autodiscover/autodiscover.xml",
domain
),
},
// always SSL for Thunderbird's database
@@ -318,10 +415,10 @@ impl AutoconfigSource {
]
}
async fn fetch(&self, ctx: &Context, param: &LoginParam) -> Result<LoginParam> {
async fn fetch(&self, ctx: &Context, param: &LoginParam) -> Result<Vec<ServerParams>> {
let params = match self.provider {
AutoconfigProvider::Mozilla => moz_autoconfigure(ctx, &self.url, &param).await?,
AutoconfigProvider::Outlook => outlk_autodiscover(ctx, &self.url, &param).await?,
AutoconfigProvider::Outlook => outlk_autodiscover(ctx, &self.url).await?,
};
Ok(params)
@@ -337,7 +434,7 @@ async fn get_autoconfig(
param: &LoginParam,
param_domain: &str,
param_addr_urlencoded: &str,
) -> Option<LoginParam> {
) -> Option<Vec<ServerParams>> {
let sources = AutoconfigSource::all(param_domain, param_addr_urlencoded);
let mut progress = 300;
@@ -353,301 +450,134 @@ async fn get_autoconfig(
None
}
fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option<LoginParam> {
fn get_offline_autoconfig(context: &Context, addr: &str) -> Option<Vec<ServerParams>> {
info!(
context,
"checking internal provider-info for offline autoconfig"
);
if let Some(provider) = provider::get_provider_info(&param.addr) {
if let Some(provider) = provider::get_provider_info(&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 let Some(imap) = imap {
if let Some(smtp) = smtp {
let mut p = LoginParam::new();
p.addr = param.addr.clone();
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::Automatic;
p.server_flags |= match imap.socket {
provider::Socket::STARTTLS => DC_LP_IMAP_SOCKET_STARTTLS,
provider::Socket::SSL => DC_LP_IMAP_SOCKET_SSL,
};
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::Automatic;
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);
}
if provider.server.is_empty() {
info!(context, "offline autoconfig found, but no servers defined");
None
} else {
info!(context, "offline autoconfig found");
let servers = provider
.server
.iter()
.map(|s| ServerParams {
protocol: s.protocol,
socket: s.socket,
hostname: s.hostname.to_string(),
port: s.port,
username: match s.username_pattern {
UsernamePattern::EMAIL => addr.to_string(),
UsernamePattern::EMAILLOCALPART => {
if let Some(at) = addr.find('@') {
addr.split_at(at).0.to_string()
} else {
addr.to_string()
}
}
},
})
.collect();
Some(servers)
}
info!(context, "offline autoconfig found, but no servers defined");
return None;
}
provider::Status::BROKEN => {
info!(context, "offline autoconfig found, provider is broken");
return None;
None
}
}
}
info!(context, "no offline autoconfig found");
None
}
async fn try_imap_hostnames(
context: &Context,
mut param: LoginParam,
imap: &mut Imap,
) -> Result<LoginParam> {
if param.mail_server.is_empty() {
let parsed: EmailAddress = param.addr.parse().context("Bad email-address")?;
let param_domain = parsed.domain;
param.mail_server = param_domain.clone();
if let Ok(param) = try_imap_ports(context, param.clone(), imap).await {
return Ok(param);
}
progress!(context, 650);
param.mail_server = "imap.".to_string() + &param_domain;
if let Ok(param) = try_imap_ports(context, param.clone(), imap).await {
return Ok(param);
}
progress!(context, 700);
param.mail_server = "mail.".to_string() + &param_domain;
try_imap_ports(context, param, imap).await
} else {
progress!(context, 700);
try_imap_ports(context, param, imap).await
info!(context, "no offline autoconfig found");
None
}
}
// Try various IMAP ports and corresponding TLS settings.
async fn try_imap_ports(
async fn try_imap_one_param(
context: &Context,
mut param: LoginParam,
param: &ServerLoginParam,
addr: &str,
oauth2: bool,
imap: &mut Imap,
) -> Result<LoginParam> {
// Try to infer port from socket security.
if param.mail_port == 0 {
if 0 != param.server_flags & DC_LP_IMAP_SOCKET_SSL {
param.mail_port = 993
}
if 0 != param.server_flags & (DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_PLAIN) {
param.mail_port = 143
}
}
if param.mail_port == 0 {
// Neither port nor security is set.
//
// Try common secure combinations.
// Try TLS over port 993
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32);
param.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32;
param.mail_port = 993;
if let Ok(login_param) = try_imap_usernames(context, param.clone(), imap).await {
return Ok(login_param);
}
// Try STARTTLS over port 143
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32);
param.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS as i32;
param.mail_port = 143;
try_imap_usernames(context, param, imap).await
} else if 0 == param.server_flags & DC_LP_SMTP_SOCKET_FLAGS as i32 {
// Try TLS over user-provided port.
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32);
param.server_flags |= DC_LP_IMAP_SOCKET_SSL as i32;
if let Ok(login_param) = try_imap_usernames(context, param.clone(), imap).await {
return Ok(login_param);
}
// Try STARTTLS over user-provided port.
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS as i32);
param.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS as i32;
try_imap_usernames(context, param, imap).await
} else {
try_imap_usernames(context, param, imap).await
}
}
async fn try_imap_usernames(
context: &Context,
mut param: LoginParam,
imap: &mut Imap,
) -> Result<LoginParam> {
if param.mail_user.is_empty() {
param.mail_user = param.addr.clone();
if let Err(e) = try_imap_one_param(context, &param, imap).await {
if let Some(at) = param.mail_user.find('@') {
param.mail_user = param.mail_user.split_at(at).0.to_string();
try_imap_one_param(context, &param, imap).await?;
} else {
return Err(e);
}
}
Ok(param)
} else {
try_imap_one_param(context, &param, imap).await?;
Ok(param)
}
}
async fn try_imap_one_param(context: &Context, param: &LoginParam, imap: &mut Imap) -> Result<()> {
) -> Result<(), ConfigurationError> {
let inf = format!(
"imap: {}@{}:{} flags=0x{:x} certificate_checks={}",
param.mail_user,
param.mail_server,
param.mail_port,
param.server_flags,
param.imap_certificate_checks
"imap: {}@{}:{} security={} certificate_checks={} oauth2={}",
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
);
info!(context, "Trying: {}", inf);
if imap.connect(context, &param).await {
if let Err(err) = imap.connect(context, param, addr, oauth2).await {
info!(context, "failure: {}", err);
Err(ConfigurationError {
config: inf,
msg: err.to_string(),
})
} else {
info!(context, "success: {}", inf);
return Ok(());
Ok(())
}
bail!("Could not connect: {}", inf);
}
async fn try_smtp_hostnames(
async fn try_smtp_one_param(
context: &Context,
mut param: LoginParam,
param: &ServerLoginParam,
addr: &str,
oauth2: bool,
smtp: &mut Smtp,
) -> Result<LoginParam> {
if param.send_server.is_empty() {
let parsed: EmailAddress = param.addr.parse().context("Bad email-address")?;
let param_domain = parsed.domain;
param.send_server = param_domain.clone();
if let Ok(param) = try_smtp_ports(context, param.clone(), smtp).await {
return Ok(param);
}
progress!(context, 800);
param.send_server = "smtp.".to_string() + &param_domain;
if let Ok(param) = try_smtp_ports(context, param.clone(), smtp).await {
return Ok(param);
}
progress!(context, 850);
param.send_server = "mail.".to_string() + &param_domain;
try_smtp_ports(context, param, smtp).await
} else {
progress!(context, 850);
try_smtp_ports(context, param, smtp).await
}
}
// Try various SMTP ports and corresponding TLS settings.
async fn try_smtp_ports(
context: &Context,
mut param: LoginParam,
smtp: &mut Smtp,
) -> Result<LoginParam> {
// Try to infer port from socket security.
if param.send_port == 0 {
if 0 != param.server_flags & DC_LP_SMTP_SOCKET_STARTTLS as i32 {
param.send_port = 587;
}
if 0 != param.server_flags & DC_LP_SMTP_SOCKET_PLAIN as i32 {
param.send_port = 25;
}
if 0 != param.server_flags & DC_LP_SMTP_SOCKET_SSL as i32 {
param.send_port = 465;
}
}
if param.send_port == 0 {
// Neither port nor security is set.
//
// Try common secure combinations.
// Try TLS over port 465.
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
param.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32;
param.send_port = 465;
if let Ok(login_param) = try_smtp_usernames(context, param.clone(), smtp).await {
return Ok(login_param);
}
// Try STARTTLS over port 587.
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32;
param.send_port = 587;
try_smtp_usernames(context, param, smtp).await
} else if 0 == param.server_flags & DC_LP_SMTP_SOCKET_FLAGS as i32 {
// Try TLS over user-provided port.
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
param.server_flags |= DC_LP_SMTP_SOCKET_SSL as i32;
if let Ok(param) = try_smtp_usernames(context, param.clone(), smtp).await {
return Ok(param);
}
// Try STARTTLS over user-provided port.
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32;
try_smtp_usernames(context, param, smtp).await
} else {
try_smtp_usernames(context, param, smtp).await
}
}
async fn try_smtp_usernames(
context: &Context,
mut param: LoginParam,
smtp: &mut Smtp,
) -> Result<LoginParam> {
if param.send_user.is_empty() {
param.send_user = param.addr.clone();
if let Err(e) = try_smtp_one_param(context, &param, smtp).await {
if let Some(at) = param.send_user.find('@') {
param.send_user = param.send_user.split_at(at).0.to_string();
try_smtp_one_param(context, &param, smtp).await?;
} else {
return Err(e);
}
}
Ok(param)
} else {
try_smtp_one_param(context, &param, smtp).await?;
Ok(param)
}
}
async fn try_smtp_one_param(context: &Context, param: &LoginParam, smtp: &mut Smtp) -> Result<()> {
) -> Result<(), ConfigurationError> {
let inf = format!(
"smtp: {}@{}:{} flags: 0x{:x}",
param.send_user, param.send_server, param.send_port, param.server_flags
"smtp: {}@{}:{} security={} certificate_checks={} oauth2={}",
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
);
info!(context, "Trying: {}", inf);
if let Err(err) = smtp.connect(context, &param).await {
bail!("could not connect: {}", err);
if let Err(err) = smtp.connect(context, param, addr, oauth2).await {
info!(context, "failure: {}", err);
Err(ConfigurationError {
config: inf,
msg: err.to_string(),
})
} else {
info!(context, "success: {}", inf);
smtp.disconnect().await;
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
#[error("Trying {config}…\nError: {msg}")]
pub struct ConfigurationError {
config: String,
msg: String,
}
async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationError>) -> String {
let first_err = if let Some(f) = errors.first() {
f
} else {
return "".to_string();
};
if errors
.iter()
.all(|e| e.msg.to_lowercase().contains("could not resolve"))
{
return context
.stock_str(StockMessage::ErrorNoNetwork)
.await
.to_string();
}
info!(context, "success: {}", inf);
smtp.disconnect().await;
Ok(())
if errors.iter().all(|e| e.msg == first_err.msg) {
return first_err.msg.to_string();
}
errors.iter().map(|e| e.to_string()).join("\n\n")
}
#[derive(Debug, thiserror::Error)]
@@ -655,17 +585,14 @@ pub enum Error {
#[error("Invalid email address: {0:?}")]
InvalidEmailAddress(String),
#[error("XML error at position {position}")]
#[error("XML error at position {position}: {error}")]
InvalidXml {
position: usize,
#[source]
error: quick_xml::Error,
},
#[error("Bad or incomplete autoconfig")]
IncompleteAutoconfig(LoginParam),
#[error("Failed to get URL")]
#[error("Failed to get URL: {0}")]
ReadUrlError(#[from] self::read_url::Error),
#[error("Number of redirection is exceeded")]
@@ -674,6 +601,7 @@ pub enum Error {
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::config::*;
@@ -697,23 +625,15 @@ mod tests {
async fn test_get_offline_autoconfig() {
let context = TestContext::new().await.ctx;
let mut params = LoginParam::new();
params.addr = "someone123@example.org".to_string();
assert!(get_offline_autoconfig(&context, &params).is_none());
let addr = "someone123@example.org";
assert!(get_offline_autoconfig(&context, addr).is_none());
let mut params = LoginParam::new();
params.addr = "someone123@nauta.cu".to_string();
let found_params = get_offline_autoconfig(&context, &params).unwrap();
assert_eq!(found_params.mail_server, "imap.nauta.cu".to_string());
assert_eq!(found_params.send_server, "smtp.nauta.cu".to_string());
assert_eq!(
found_params.imap_certificate_checks,
CertificateChecks::Automatic
);
assert_eq!(
found_params.smtp_certificate_checks,
CertificateChecks::Automatic
);
let addr = "someone123@nauta.cu";
let found_params = get_offline_autoconfig(&context, addr).unwrap();
assert_eq!(found_params.len(), 2);
assert_eq!(found_params[0].protocol, Protocol::IMAP);
assert_eq!(found_params[0].hostname, "imap.nauta.cu".to_string());
assert_eq!(found_params[1].protocol, Protocol::SMTP);
assert_eq!(found_params[1].hostname, "smtp.nauta.cu".to_string());
}
}

View File

@@ -12,7 +12,7 @@ pub async fn read_url(context: &Context, url: &str) -> Result<String, Error> {
match surf::get(url).recv_string().await {
Ok(res) => Ok(res),
Err(err) => {
info!(context, "Can\'t read URL {}", url);
info!(context, "Can\'t read URL {}: {}", url, err);
Err(Error::GetError(err))
}

View File

@@ -0,0 +1,164 @@
//! Variable server parameters lists
use crate::provider::{Protocol, Socket};
/// Set of variable parameters to try during configuration.
///
/// Can be loaded from offline provider database, online configuraiton
/// or derived from user entered parameters.
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ServerParams {
/// Protocol, such as IMAP or SMTP.
pub protocol: Protocol,
/// Server hostname, empty if unknown.
pub hostname: String,
/// Server port, zero if unknown.
pub port: u16,
/// Socket security, such as TLS or STARTTLS, Socket::Automatic if unknown.
pub socket: Socket,
/// Username, empty if unknown.
pub username: String,
}
impl ServerParams {
pub(crate) fn expand_usernames(mut self, addr: &str) -> Vec<ServerParams> {
let mut res = Vec::new();
if self.username.is_empty() {
self.username = addr.to_string();
res.push(self.clone());
if let Some(at) = addr.find('@') {
self.username = addr.split_at(at).0.to_string();
res.push(self);
}
} else {
res.push(self)
}
res
}
pub(crate) fn expand_hostnames(mut self, param_domain: &str) -> Vec<ServerParams> {
let mut res = Vec::new();
if self.hostname.is_empty() {
self.hostname = param_domain.to_string();
res.push(self.clone());
self.hostname = match self.protocol {
Protocol::IMAP => "imap.".to_string() + param_domain,
Protocol::SMTP => "smtp.".to_string() + param_domain,
};
res.push(self.clone());
self.hostname = "mail.".to_string() + param_domain;
res.push(self);
} else {
res.push(self);
}
res
}
pub(crate) fn expand_ports(mut self) -> Vec<ServerParams> {
// Try to infer port from socket security.
if self.port == 0 {
self.port = match self.socket {
Socket::SSL => match self.protocol {
Protocol::IMAP => 993,
Protocol::SMTP => 465,
},
Socket::STARTTLS | Socket::Plain => match self.protocol {
Protocol::IMAP => 143,
Protocol::SMTP => 587,
},
Socket::Automatic => 0,
}
}
let mut res = Vec::new();
if self.port == 0 {
// Neither port nor security is set.
//
// Try common secure combinations.
// Try STARTTLS
self.socket = Socket::STARTTLS;
self.port = match self.protocol {
Protocol::IMAP => 143,
Protocol::SMTP => 587,
};
res.push(self.clone());
// Try TLS
self.socket = Socket::SSL;
self.port = match self.protocol {
Protocol::IMAP => 993,
Protocol::SMTP => 465,
};
res.push(self);
} else if self.socket == Socket::Automatic {
// Try TLS over user-provided port.
self.socket = Socket::SSL;
res.push(self.clone());
// Try STARTTLS over user-provided port.
self.socket = Socket::STARTTLS;
res.push(self);
} else {
res.push(self);
}
res
}
}
/// Expands vector of `ServerParams`, replacing placeholders with
/// variants to try.
pub(crate) fn expand_param_vector(
v: Vec<ServerParams>,
addr: &str,
domain: &str,
) -> Vec<ServerParams> {
v.into_iter()
// The order of expansion is important: ports are expanded the
// last, so they are changed the first. Username is only
// changed if default value (address with domain) didn't work
// for all available hosts and ports.
.flat_map(|params| params.expand_usernames(addr).into_iter())
.flat_map(|params| params.expand_hostnames(domain).into_iter())
.flat_map(|params| params.expand_ports().into_iter())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_expand_param_vector() {
let v = expand_param_vector(
vec![ServerParams {
protocol: Protocol::IMAP,
hostname: "example.net".to_string(),
port: 0,
socket: Socket::SSL,
username: "foobar".to_string(),
}],
"foobar@example.net",
"example.net",
);
assert_eq!(
v,
vec![ServerParams {
protocol: Protocol::IMAP,
hostname: "example.net".to_string(),
port: 993,
socket: Socket::SSL,
username: "foobar".to_string(),
}],
);
}
}

View File

@@ -1,20 +1,9 @@
//! # Constants
#![allow(dead_code)]
use deltachat_derive::*;
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
lazy_static! {
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
}
// some defaults
const DC_E2EE_DEFAULT_ENABLED: i32 = 1;
const DC_INBOX_WATCH_DEFAULT: i32 = 1;
const DC_SENTBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
#[derive(
Debug,
@@ -117,14 +106,16 @@ pub const DC_GCL_ADD_SELF: usize = 0x02;
// unchanged user avatars are resent to the recipients every some days
pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
// warn about an outdated app after a given number of days.
// as we use the "provider-db generation date" as reference (that might not be updated very often)
// and as not all system get speedy updates,
// do not use too small value that will annoy users checking for nonexistant updates.
pub const DC_OUTDATED_WARNING_DAYS: i64 = 365;
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
pub(crate) const DC_CHAT_ID_DEADDROP: u32 = 1;
pub const DC_CHAT_ID_DEADDROP: u32 = 1;
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
pub const DC_CHAT_ID_TRASH: u32 = 3;
/// a message is just in creation but not yet assigned to a chat (eg. we may need the message ID to set up blobs; this avoids unready message to be sent and shown)
const DC_CHAT_ID_MSGS_IN_CREATION: u32 = 4;
/// virtual chat showing all messages flagged with msgs.starred=2
pub const DC_CHAT_ID_STARRED: u32 = 5;
/// only an indicator in a chatlist
pub const DC_CHAT_ID_ARCHIVED_LINK: u32 = 6;
/// only an indicator in a chatlist
@@ -152,7 +143,6 @@ pub enum Chattype {
Undefined = 0,
Single = 100,
Group = 120,
VerifiedGroup = 130,
}
impl Default for Chattype {
@@ -166,9 +156,9 @@ pub const DC_MSG_ID_DAYMARKER: u32 = 9;
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
/// approx. max. length returned by dc_msg_get_text()
const DC_MAX_GET_TEXT_LEN: usize = 30000;
pub const DC_MAX_GET_TEXT_LEN: usize = 30000;
/// approx. max. length returned by dc_get_msg_info()
const DC_MAX_GET_INFO_LEN: usize = 100_000;
pub const DC_MAX_GET_INFO_LEN: usize = 100_000;
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
pub const DC_CONTACT_ID_SELF: u32 = 1;
@@ -199,44 +189,11 @@ pub const DC_LP_AUTH_OAUTH2: i32 = 0x2;
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_AUTH_NORMAL: i32 = 0x4;
/// Connect to IMAP via STARTTLS.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_IMAP_SOCKET_STARTTLS: i32 = 0x100;
/// Connect to IMAP via SSL.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_IMAP_SOCKET_SSL: i32 = 0x200;
/// Connect to IMAP unencrypted, this should not be used.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_IMAP_SOCKET_PLAIN: i32 = 0x400;
/// Connect to SMTP via STARTTLS.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_SMTP_SOCKET_STARTTLS: usize = 0x10000;
/// Connect to SMTP via SSL.
/// If this flag is set, automatic configuration is skipped.
pub const DC_LP_SMTP_SOCKET_SSL: usize = 0x20000;
/// Connect to SMTP unencrypted, this should not be used.
/// If this flag is set, automatic configuration is skipped.
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;
/// 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;
/// 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;
// QR code scanning (view from Bob, the joiner)
pub const DC_VC_AUTH_REQUIRED: i32 = 2;
pub const DC_VC_CONTACT_CONFIRM: i32 = 6;
pub const DC_BOB_ERROR: i32 = 0;
pub const DC_BOB_SUCCESS: i32 = 1;
/// How many existing messages shall be fetched after configuration.
pub const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
// max. width/height of an avatar
pub const AVATAR_SIZE: u32 = 192;
@@ -331,16 +288,6 @@ mod tests {
}
}
// 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.
const DC_EVENT_FILE_COPIED: usize = 2055; // deprecated;
const DC_EVENT_IS_OFFLINE: usize = 2081; // deprecated;
const DC_ERROR_SEE_STRING: usize = 0; // deprecated;
const DC_ERROR_SELF_NOT_IN_GROUP: usize = 1; // deprecated;
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]

View File

@@ -3,7 +3,7 @@
use async_std::path::PathBuf;
use deltachat_derive::*;
use itertools::Itertools;
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
use regex::Regex;
use crate::aheader::EncryptPreference;
@@ -16,10 +16,11 @@ use crate::error::{bail, ensure, format_err, Result};
use crate::events::EventType;
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::message::{MessageState, MsgId};
use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::*;
use crate::peerstate::*;
use crate::provider::Socket;
use crate::stock::StockMessage;
/// An object representing a single contact in memory.
@@ -235,6 +236,7 @@ impl Contact {
name: impl AsRef<str>,
addr: impl AsRef<str>,
) -> Result<u32> {
let name = improve_single_line_input(name);
ensure!(
!addr.as_ref().is_empty(),
"Cannot create contact with empty address"
@@ -245,13 +247,12 @@ impl Contact {
let (contact_id, sth_modified) =
Contact::add_or_lookup(context, name, addr, Origin::ManuallyCreated).await?;
let blocked = Contact::is_blocked_load(context, contact_id).await;
context.emit_event(EventType::ContactsChanged(
if sth_modified == Modifier::Created {
Some(contact_id)
} else {
None
},
));
match sth_modified {
Modifier::None => {}
Modifier::Modified | Modifier::Created => {
context.emit_event(EventType::ContactsChanged(Some(contact_id)))
}
}
if blocked {
Contact::unblock(context, contact_id).await;
}
@@ -259,10 +260,9 @@ impl Contact {
Ok(contact_id)
}
/// Mark all messages sent by the given contact
/// as *noticed*. See also dc_marknoticed_chat() and dc_markseen_msgs()
///
/// Calling this function usually results in the event `#DC_EVENT_MSGS_CHANGED`.
/// Mark messages from a contact as noticed.
/// The contact is expected to belong to the deaddrop,
/// therefore, DC_EVENT_MSGS_NOTICED(DC_CHAT_ID_DEADDROP) is emitted.
pub async fn mark_noticed(context: &Context, id: u32) {
if context
.sql
@@ -273,10 +273,7 @@ impl Contact {
.await
.is_ok()
{
context.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
context.emit_event(EventType::MsgsNoticed(ChatId::new(DC_CHAT_ID_DEADDROP)));
}
}
@@ -455,10 +452,20 @@ impl Contact {
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.
context.sql.execute(
"UPDATE chats SET name=? WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
paramsv![new_name, Chattype::Single, row_id]
).await.ok();
let chat_id = context.sql.query_get_value::<i32>(
context,
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
paramsv![Chattype::Single, row_id]
).await;
if let Some(chat_id) = chat_id {
match context.sql.execute("UPDATE chats SET name=? WHERE id=? AND name!=?1", paramsv![new_name, chat_id]).await {
Err(err) => warn!(context, "Can't update chat name: {}", err),
Ok(count) => if count > 0 {
// Chat name updated
context.emit_event(EventType::ChatModified(ChatId::new(chat_id as u32)));
}
}
}
}
sth_modified = Modifier::Modified;
}
@@ -728,8 +735,8 @@ impl Contact {
);
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
}
} else if 0 == loginparam.server_flags & DC_LP_IMAP_SOCKET_PLAIN as i32
&& 0 == loginparam.server_flags & DC_LP_SMTP_SOCKET_PLAIN as i32
} else if loginparam.imap.security != Socket::Plain
&& loginparam.smtp.security != Socket::Plain
{
ret += &context.stock_str(StockMessage::EncrTransp).await;
} else {
@@ -1046,9 +1053,7 @@ pub fn addr_normalize(addr: &str) -> &str {
}
fn sanitize_name_and_addr(name: impl AsRef<str>, addr: impl AsRef<str>) -> (String, String) {
lazy_static! {
static ref ADDR_WITH_NAME_REGEX: Regex = Regex::new("(.*)<(.*)>").unwrap();
}
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
(
if name.as_ref().is_empty() {
@@ -1089,12 +1094,12 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
// this would result in recreating the same group...)
if context.sql.execute(
"UPDATE chats SET blocked=? WHERE type=? AND id IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
paramsv![new_blocking, 100, contact_id as i32],
).await.is_ok() {
Contact::mark_noticed(context, contact_id).await;
context.emit_event(EventType::ContactsChanged(None));
}
"UPDATE chats SET blocked=? WHERE type=? AND id IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
paramsv![new_blocking, 100, contact_id as i32]).await.is_ok()
{
Contact::mark_noticed(context, contact_id).await;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
}
}
}
}

View File

@@ -17,9 +17,9 @@ use crate::error::*;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::lot::Lot;
use crate::message::{self, MsgId};
use crate::scheduler::Scheduler;
use crate::securejoin::Bob;
use crate::sql::Sql;
use std::time::SystemTime;
@@ -44,7 +44,7 @@ pub struct InnerContext {
pub(crate) blobdir: PathBuf,
pub(crate) sql: Sql,
pub(crate) os_name: Option<String>,
pub(crate) bob: RwLock<BobStatus>,
pub(crate) bob: RwLock<Bob>,
pub(crate) last_smeared_timestamp: RwLock<i64>,
pub(crate) running_state: RwLock<RunningState>,
/// Mutex to avoid generating the key for the user more than once.
@@ -136,10 +136,7 @@ impl Context {
let ctx = Context {
inner: Arc::new(inner),
};
ensure!(
ctx.sql.open(&ctx, &ctx.dbfile, false).await,
"Failed opening sqlite database"
);
ctx.sql.open(&ctx, &ctx.dbfile, false).await?;
Ok(ctx)
}
@@ -508,13 +505,6 @@ impl Default for RunningState {
}
}
#[derive(Debug, Default)]
pub(crate) struct BobStatus {
pub expects: i32,
pub status: i32,
pub qr_scan: Option<Lot>,
}
pub fn get_version_str() -> &'static str {
&DC_VERSION_STR
}
@@ -580,7 +570,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into(), 1).await;
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir, 1).await;
assert!(res.is_err());
}

View File

@@ -4,7 +4,7 @@ use sha2::{Digest, Sha256};
use mailparse::SingleInfo;
use crate::chat::{self, Chat, ChatId};
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
use crate::config::Config;
use crate::constants::*;
use crate::contact::*;
@@ -43,6 +43,17 @@ pub async fn dc_receive_imf(
server_folder: impl AsRef<str>,
server_uid: u32,
seen: bool,
) -> Result<()> {
dc_receive_imf_inner(context, imf_raw, server_folder, server_uid, seen, false).await
}
pub(crate) async fn dc_receive_imf_inner(
context: &Context,
imf_raw: &[u8],
server_folder: impl AsRef<str>,
server_uid: u32,
seen: bool,
fetching_existing_messages: bool,
) -> Result<()> {
info!(
context,
@@ -111,7 +122,7 @@ pub async fn dc_receive_imf(
// or if From: is equal to SELF (in this case, it is any outgoing messages,
// we do not check Return-Path any more as this is unreliable, see
// https://github.com/deltachat/deltachat-core/issues/150)
let (from_id, from_id_blocked, incoming_origin) =
let (from_id, _from_id_blocked, incoming_origin) =
from_field_to_contact_id(context, &mime_parser.from).await?;
let incoming = from_id != DC_CONTACT_ID_SELF;
@@ -162,7 +173,6 @@ pub async fn dc_receive_imf(
&rfc724_mid,
&mut sent_timestamp,
from_id,
from_id_blocked,
&mut hidden,
&mut chat_id,
seen,
@@ -170,6 +180,7 @@ pub async fn dc_receive_imf(
&mut insert_msg_id,
&mut created_db_entries,
&mut create_event_to_send,
fetching_existing_messages,
)
.await
{
@@ -329,7 +340,6 @@ async fn add_parts(
rfc724_mid: &str,
sent_timestamp: &mut i64,
from_id: u32,
from_id_blocked: bool,
hidden: &mut bool,
chat_id: &mut ChatId,
seen: bool,
@@ -337,6 +347,7 @@ async fn add_parts(
insert_msg_id: &mut MsgId,
created_db_entries: &mut Vec<(ChatId, MsgId)>,
create_event_to_send: &mut Option<CreateEvent>,
fetching_existing_messages: bool,
) -> Result<()> {
let mut state: MessageState;
let mut chat_id_blocked = Blocked::Not;
@@ -391,7 +402,7 @@ async fn add_parts(
let to_id: u32;
if incoming {
state = if seen {
state = if seen || fetching_existing_messages {
MessageState::InSeen
} else {
MessageState::InFresh
@@ -418,8 +429,6 @@ async fn add_parts(
}
Err(err) => {
*hidden = true;
context.bob.write().await.status = 0; // secure-join failed
context.stop_ongoing().await;
warn!(context, "Error in Secure-Join message handling: {}", err);
return Ok(());
@@ -432,6 +441,8 @@ async fn add_parts(
.await
.unwrap_or_default();
// get the chat_id - a chat_id here is no indicator that the chat is displayed in the normal list,
// it might also be blocked and displayed in the deaddrop as a result
if chat_id.is_unset() && mime_parser.failure_report.is_some() {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
info!(
@@ -440,11 +451,8 @@ async fn add_parts(
);
}
// get the chat_id - a chat_id here is no indicator that the chat is displayed in the normal list,
// it might also be blocked and displayed in the deaddrop as a result
if chat_id.is_unset() {
// try to create a group
// (groups appear automatically only if the _sender_ is known, see core issue #54)
let create_blocked =
if !test_normal_chat_id.is_unset() && test_normal_chat_id_blocked == Blocked::Not {
@@ -537,6 +545,10 @@ async fn add_parts(
&& show_emails != ShowEmails::All
{
state = MessageState::InNoticed;
} else if fetching_existing_messages && Blocked::Deaddrop == chat_id_blocked {
// The fetched existing message should be shown in the chatlist-contact-request because
// a new user won't find the contact request in the menu
state = MessageState::InFresh;
}
} else {
// Outgoing
@@ -633,6 +645,12 @@ async fn add_parts(
}
}
if fetching_existing_messages && mime_parser.decrypting_failed {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
// We are only gathering old messages on first start. We do not want to add loads of non-decryptable messages to the chats.
info!(context, "Dropping existing non-decipherable message.");
}
// Extract ephemeral timer from the message.
let mut ephemeral_timer = if let Some(value) = mime_parser.get(HeaderDef::EphemeralTimer) {
match value.parse::<EphemeralTimer>() {
@@ -696,11 +714,62 @@ async fn add_parts(
ephemeral_timer = EphemeralTimer::Disabled;
}
// if a chat is protected, check additional properties
if !chat_id.is_special() {
let chat = Chat::load_from_db(context, *chat_id).await?;
let new_status = match mime_parser.is_system_message {
SystemMessage::ChatProtectionEnabled => Some(ProtectionStatus::Protected),
SystemMessage::ChatProtectionDisabled => Some(ProtectionStatus::Unprotected),
_ => None,
};
if chat.is_protected() || new_status.is_some() {
if let Err(err) =
check_verified_properties(context, mime_parser, from_id as u32, to_ids).await
{
warn!(context, "verification problem: {}", err);
let s = format!("{}. See 'Info' for more details", err);
mime_parser.repl_msg_by_error(s);
} else {
// change chat protection only when verification check passes
if let Some(new_status) = new_status {
if let Err(e) = chat_id.inner_set_protection(context, new_status).await {
chat::add_info_msg(
context,
*chat_id,
format!("Cannot set protection: {}", e),
)
.await;
return Ok(()); // do not return an error as this would result in retrying the message
}
set_better_msg(
mime_parser,
context.stock_protection_msg(new_status, from_id).await,
);
}
}
}
}
// correct message_timestamp, it should not be used before,
// however, we cannot do this earlier as we need from_id to be set
let in_fresh = state == MessageState::InFresh;
let rcvd_timestamp = time();
let sort_timestamp = calc_sort_timestamp(context, *sent_timestamp, *chat_id, in_fresh).await;
// Ensure replies to messages are sorted after the parent message.
//
// This is useful in a case where sender clocks are not
// synchronized and parent message has a Date: header with a
// timestamp higher than reply timestamp.
//
// This does not help if parent message arrives later than the
// reply.
let parent_timestamp = mime_parser.get_parent_timestamp(context).await?;
let sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| {
std::cmp::max(sort_timestamp, parent_timestamp)
});
*sent_timestamp = std::cmp::min(*sent_timestamp, rcvd_timestamp);
// unarchive chat
@@ -806,7 +875,7 @@ async fn add_parts(
mime_headers,
mime_in_reply_to,
mime_references,
part.error,
part.error.take().unwrap_or_default(),
ephemeral_timer,
ephemeral_timestamp
])?;
@@ -836,13 +905,16 @@ async fn add_parts(
"Message has {} parts and is assigned to chat #{}.", icnt, chat_id,
);
// new outgoing message from another device marks the chat as noticed.
if !incoming && !*hidden && !chat_id.is_special() {
chat::marknoticed_chat_if_older_than(context, chat_id, sort_timestamp).await?;
}
// check event to send
if chat_id.is_trash() || *hidden {
*create_event_to_send = None;
} else if incoming && state == MessageState::InFresh {
if from_id_blocked {
*create_event_to_send = None;
} else if Blocked::Not != chat_id_blocked {
if Blocked::Not != chat_id_blocked {
*create_event_to_send = Some(CreateEvent::MsgsChanged);
} else {
*create_event_to_send = Some(CreateEvent::IncomingMsg);
@@ -1119,29 +1191,18 @@ async fn create_or_lookup_group(
set_better_msg(mime_parser, &better_msg);
// check, if we have a chat with this group ID
let (mut chat_id, chat_id_verified, _blocked) = chat::get_chat_id_by_grpid(context, &grpid)
let (mut chat_id, _, _blocked) = chat::get_chat_id_by_grpid(context, &grpid)
.await
.unwrap_or((ChatId::new(0), false, Blocked::Not));
if !chat_id.is_unset() {
if chat_id_verified {
if let Err(err) =
check_verified_properties(context, mime_parser, from_id as u32, to_ids).await
{
warn!(context, "verification problem: {}", err);
let s = format!("{}. See 'Info' for more details", err);
mime_parser.repl_msg_by_error(s);
}
}
if !chat::is_contact_in_chat(context, chat_id, from_id as u32).await {
// The From-address is not part of this group.
// It could be a new user or a DSN from a mailer-daemon.
// in any case we do not want to recreate the member list
// but still show the message as part of the chat.
// After all, the sender has a reference/in-reply-to that
// points to this chat.
let s = context.stock_str(StockMessage::UnknownSenderForChat).await;
mime_parser.repl_msg_by_error(s.to_string());
}
if !chat_id.is_unset() && !chat::is_contact_in_chat(context, chat_id, from_id as u32).await {
// The From-address is not part of this group.
// It could be a new user or a DSN from a mailer-daemon.
// in any case we do not want to recreate the member list
// but still show the message as part of the chat.
// After all, the sender has a reference/in-reply-to that
// points to this chat.
let s = context.stock_str(StockMessage::UnknownSenderForChat).await;
mime_parser.repl_msg_by_error(s.to_string());
}
// check if the group does not exist but should be created
@@ -1164,7 +1225,7 @@ async fn create_or_lookup_group(
|| X_MrAddToGrp.is_some() && addr_cmp(&self_addr, X_MrAddToGrp.as_ref().unwrap()))
{
// group does not exist but should be created
let create_verified = if mime_parser.get(HeaderDef::ChatVerified).is_some() {
let create_protected = if mime_parser.get(HeaderDef::ChatVerified).is_some() {
if let Err(err) =
check_verified_properties(context, mime_parser, from_id as u32, to_ids).await
{
@@ -1172,9 +1233,9 @@ async fn create_or_lookup_group(
let s = format!("{}. See 'Info' for more details", err);
mime_parser.repl_msg_by_error(&s);
}
VerifiedStatus::Verified
ProtectionStatus::Protected
} else {
VerifiedStatus::Unverified
ProtectionStatus::Unprotected
};
if !allow_creation {
@@ -1187,11 +1248,22 @@ async fn create_or_lookup_group(
&grpid,
grpname.as_ref().unwrap(),
create_blocked,
create_verified,
create_protected,
)
.await;
chat_id_blocked = create_blocked;
recreate_member_list = true;
// once, we have protected-chats explained in UI, we can uncomment the following lines.
// ("verified groups" did not add a message anyway)
//
//if create_protected == ProtectionStatus::Protected {
// set from_id=0 as it is not clear that the sender of this random group message
// actually really has enabled chat-protection at some point.
//chat_id
// .add_protection_msg(context, ProtectionStatus::Protected, false, 0)
// .await?;
//}
}
// again, check chat_id
@@ -1243,7 +1315,10 @@ async fn create_or_lookup_group(
}
}
}
} else if mime_parser.is_system_message == SystemMessage::ChatProtectionEnabled {
recreate_member_list = true;
}
if let Some(avatar_action) = &mime_parser.group_avatar {
info!(context, "group-avatar change for {}", chat_id);
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
@@ -1428,7 +1503,7 @@ async fn create_or_lookup_adhoc_group(
&grpid,
grpname,
create_blocked,
VerifiedStatus::Unverified,
ProtectionStatus::Unprotected,
)
.await;
for &member_id in &member_ids {
@@ -1445,20 +1520,17 @@ async fn create_group_record(
grpid: impl AsRef<str>,
grpname: impl AsRef<str>,
create_blocked: Blocked,
create_verified: VerifiedStatus,
create_protected: ProtectionStatus,
) -> ChatId {
if context.sql.execute(
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp) VALUES(?, ?, ?, ?, ?);",
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected) VALUES(?, ?, ?, ?, ?, ?);",
paramsv![
if VerifiedStatus::Unverified != create_verified {
Chattype::VerifiedGroup
} else {
Chattype::Group
},
Chattype::Group,
grpname.as_ref(),
grpid.as_ref(),
create_blocked,
time(),
create_protected,
],
).await
.is_err()
@@ -1523,7 +1595,7 @@ async fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> String {
},
)
.await
.unwrap_or_else(|_| member_cs);
.unwrap_or(member_cs);
hex_hash(&members)
}
@@ -1551,7 +1623,7 @@ async fn search_chat_ids_by_contact_ids(
}
}
if !contact_ids.is_empty() {
contact_ids.sort();
contact_ids.sort_unstable();
let contact_ids_str = join(contact_ids.iter().map(|x| x.to_string()), ",");
context.sql.query_map(
format!(
@@ -1610,6 +1682,17 @@ async fn check_verified_properties(
ensure!(mimeparser.was_encrypted(), "This message is not encrypted.");
if mimeparser.get(HeaderDef::ChatVerified).is_none() {
// we do not fail here currently, this would exclude (a) non-deltas
// and (b) deltas with different protection views across multiple devices.
// for group creation or protection enabled/disabled, however, Chat-Verified is respected.
warn!(
context,
"{} did not mark message as protected.",
contact.get_addr()
);
}
// ensure, the contact is verified
// and the message is signed with a verified key of the sender.
// this check is skipped for SELF as there is no proper SELF-peerstate
@@ -1699,7 +1782,7 @@ async fn check_verified_properties(
}
if !is_verified {
bail!(
"{} is not a member of this verified group",
"{} is not a member of this protected chat",
to_addr.to_string()
);
}
@@ -2147,7 +2230,7 @@ mod tests {
assert!(one2one.get_visibility() == ChatVisibility::Archived);
// create a group with bob, archive group
let group_id = chat::create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo")
let group_id = chat::create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
chat::add_contact_to_chat(&t.ctx, group_id, bob_id).await;
@@ -2202,7 +2285,7 @@ mod tests {
} else {
panic!("Wrong item type");
};
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
let msg = message::Message::load_from_db(&t.ctx, *msg_id)
.await
.unwrap();
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
@@ -2253,7 +2336,7 @@ mod tests {
chat::get_chat_msgs(&t.ctx, group_id, 0, None).await.len(),
1
);
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
let msg = message::Message::load_from_db(&t.ctx, *msg_id)
.await
.unwrap();
assert_eq!(msg.state, MessageState::OutMdnRcvd);
@@ -2341,7 +2424,7 @@ mod tests {
} else {
panic!("Wrong item type");
};
let msg = message::Message::load_from_db(&t.ctx, msg_id.clone())
let msg = message::Message::load_from_db(&t.ctx, *msg_id)
.await
.unwrap();
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
@@ -2448,7 +2531,7 @@ mod tests {
"shenauithz@testrun.org",
"Mr.un2NYERi1RM.lbQ5F9q-QyJ@tiscali.it",
include_bytes!("../test-data/message/tiscali_ndn.eml"),
"",
None,
)
.await;
}
@@ -2460,7 +2543,7 @@ mod tests {
"hcksocnsofoejx@five.chat",
"Mr.A7pTA5IgrUA.q4bP41vAJOp@testrun.org",
include_bytes!("../test-data/message/testrun_ndn.eml"),
"Undelivered Mail Returned to Sender This is the mail system at host hq5.merlinux.eu.\n\nI\'m sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It\'s attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n<hcksocnsofoejx@five.chat>: host mail.five.chat[195.62.125.103] said: 550 5.1.1\n <hcksocnsofoejx@five.chat>: Recipient address rejected: User unknown in\n virtual mailbox table (in reply to RCPT TO command)"
Some("Undelivered Mail Returned to Sender This is the mail system at host hq5.merlinux.eu.\n\nI\'m sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It\'s attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n<hcksocnsofoejx@five.chat>: host mail.five.chat[195.62.125.103] said: 550 5.1.1\n <hcksocnsofoejx@five.chat>: Recipient address rejected: User unknown in\n virtual mailbox table (in reply to RCPT TO command)"),
)
.await;
}
@@ -2472,7 +2555,7 @@ mod tests {
"haeclirth.sinoenrat@yahoo.com",
"1680295672.3657931.1591783872936@mail.yahoo.com",
include_bytes!("../test-data/message/yahoo_ndn.eml"),
"Failure Notice Sorry, we were unable to deliver your message to the following address.\n\n<haeclirth.sinoenrat@yahoo.com>:\n554: delivery error: dd Not a valid recipient - atlas117.free.mail.ne1.yahoo.com [...]"
Some("Failure Notice Sorry, we were unable to deliver your message to the following address.\n\n<haeclirth.sinoenrat@yahoo.com>:\n554: delivery error: dd Not a valid recipient - atlas117.free.mail.ne1.yahoo.com [...]"),
)
.await;
}
@@ -2484,7 +2567,7 @@ mod tests {
"assidhfaaspocwaeofi@gmail.com",
"CABXKi8zruXJc_6e4Dr087H5wE7sLp+u250o0N2q5DdjF_r-8wg@mail.gmail.com",
include_bytes!("../test-data/message/gmail_ndn.eml"),
"Delivery Status Notification (Failure) ** Die Adresse wurde nicht gefunden **\n\nIhre Nachricht wurde nicht an assidhfaaspocwaeofi@gmail.com zugestellt, weil die Adresse nicht gefunden wurde oder keine E-Mails empfangen kann.\n\nHier erfahren Sie mehr: https://support.google.com/mail/?p=NoSuchUser\n\nAntwort:\n\n550 5.1.1 The email account that you tried to reach does not exist. Please try double-checking the recipient\'s email address for typos or unnecessary spaces. Learn more at https://support.google.com/mail/?p=NoSuchUser i18sor6261697wrs.38 - gsmtp",
Some("Delivery Status Notification (Failure) ** Die Adresse wurde nicht gefunden **\n\nIhre Nachricht wurde nicht an assidhfaaspocwaeofi@gmail.com zugestellt, weil die Adresse nicht gefunden wurde oder keine E-Mails empfangen kann.\n\nHier erfahren Sie mehr: https://support.google.com/mail/?p=NoSuchUser\n\nAntwort:\n\n550 5.1.1 The email account that you tried to reach does not exist. Please try double-checking the recipient\'s email address for typos or unnecessary spaces. Learn more at https://support.google.com/mail/?p=NoSuchUser i18sor6261697wrs.38 - gsmtp"),
)
.await;
}
@@ -2496,7 +2579,7 @@ mod tests {
"snaerituhaeirns@gmail.com",
"9c9c2a32-056b-3592-c372-d7e8f0bd4bc2@gmx.de",
include_bytes!("../test-data/message/gmx_ndn.eml"),
"Mail delivery failed: returning message to sender This message was created automatically by mail delivery software.\n\nA message that you sent could not be delivered to one or more of\nits recipients. This is a permanent error. The following address(es)\nfailed:\n\nsnaerituhaeirns@gmail.com:\nSMTP error from remote server for RCPT TO command, host: gmail-smtp-in.l.google.com (66.102.1.27) reason: 550-5.1.1 The email account that you tried to reach does not exist. Please\n try\n550-5.1.1 double-checking the recipient\'s email address for typos or\n550-5.1.1 unnecessary spaces. Learn more at\n550 5.1.1 https://support.google.com/mail/?p=NoSuchUser f6si2517766wmc.21\n9 - gsmtp [...]"
Some("Mail delivery failed: returning message to sender This message was created automatically by mail delivery software.\n\nA message that you sent could not be delivered to one or more of\nits recipients. This is a permanent error. The following address(es)\nfailed:\n\nsnaerituhaeirns@gmail.com:\nSMTP error from remote server for RCPT TO command, host: gmail-smtp-in.l.google.com (66.102.1.27) reason: 550-5.1.1 The email account that you tried to reach does not exist. Please\n try\n550-5.1.1 double-checking the recipient\'s email address for typos or\n550-5.1.1 unnecessary spaces. Learn more at\n550 5.1.1 https://support.google.com/mail/?p=NoSuchUser f6si2517766wmc.21\n9 - gsmtp [...]"),
)
.await;
}
@@ -2508,7 +2591,7 @@ mod tests {
"hanerthaertidiuea@gmx.de",
"04422840-f884-3e37-5778-8192fe22d8e1@posteo.de",
include_bytes!("../test-data/message/posteo_ndn.eml"),
"Undelivered Mail Returned to Sender This is the mail system at host mout01.posteo.de.\n\nI\'m sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It\'s attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n<hanerthaertidiuea@gmx.de>: host mx01.emig.gmx.net[212.227.17.5] said: 550\n Requested action not taken: mailbox unavailable (in reply to RCPT TO\n command)",
Some("Undelivered Mail Returned to Sender This is the mail system at host mout01.posteo.de.\n\nI\'m sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It\'s attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n<hanerthaertidiuea@gmx.de>: host mx01.emig.gmx.net[212.227.17.5] said: 550\n Requested action not taken: mailbox unavailable (in reply to RCPT TO\n command)"),
)
.await;
}
@@ -2519,7 +2602,7 @@ mod tests {
foreign_addr: &str,
rfc724_mid_outgoing: &str,
raw_ndn: &[u8],
error_msg: &str,
error_msg: Option<&str>,
) {
let t = TestContext::new().await;
t.configure_addr(self_addr).await;
@@ -2562,7 +2645,8 @@ mod tests {
let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap();
assert_eq!(msg.state, MessageState::OutFailed);
assert_eq!(msg.error, error_msg);
assert_eq!(msg.error(), error_msg.map(|error| error.to_string()));
}
#[async_std::test]
@@ -2623,4 +2707,26 @@ mod tests {
);
assert_eq!(last_msg.from_id, DC_CONTACT_ID_INFO);
}
#[async_std::test]
async fn test_html_only_mail() {
let t = TestContext::new_alice().await;
t.ctx
.set_config(Config::ShowEmails, Some("2"))
.await
.unwrap();
dc_receive_imf(
&t.ctx,
include_bytes!("../test-data/message/wrong-html.eml"),
"INBOX",
0,
false,
)
.await
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
let msg_id = chats.get_msg_id(0).unwrap();
let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap();
assert_eq!(msg.text.unwrap(), " Guten Abend, \n\n Lots of text \n\n text with Umlaut ä... \n\n MfG [...]");
}
}

View File

@@ -9,17 +9,20 @@ use std::str::FromStr;
use std::time::{Duration, SystemTime};
use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use async_std::{fs, io};
use chrono::{Local, TimeZone};
use rand::{thread_rng, Rng};
use crate::chat::{add_device_msg, add_device_msg_with_importance};
use crate::constants::{Viewtype, DC_OUTDATED_WARNING_DAYS};
use crate::context::Context;
use crate::error::{bail, Error};
use crate::events::EventType;
pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
0 != v && 0 == v & (v - 1)
}
use crate::message::Message;
use crate::provider::get_provider_update_timestamp;
use crate::stock::StockMessage;
/// Shortens a string to a specified length and adds "[...]" to the
/// end of the shortened string.
@@ -153,6 +156,73 @@ pub(crate) async fn dc_create_smeared_timestamps(context: &Context, count: usize
start
}
// if the system time is not plausible, once a day, add a device message.
// for testing we're using time() as that is also used for message timestamps.
// moreover, add a warning if the app is outdated.
pub(crate) async fn maybe_add_time_based_warnings(context: &Context) {
if !maybe_warn_on_bad_time(context, time(), get_provider_update_timestamp()).await {
maybe_warn_on_outdated(context, time(), get_provider_update_timestamp()).await;
}
}
async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestamp: i64) -> bool {
if now < known_past_timestamp {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(
context
.stock_string_repl_str(
StockMessage::BadTimeMsgBody,
Local
.timestamp(now, 0)
.format("%Y-%m-%d %H:%M:%S")
.to_string(),
)
.await,
);
add_device_msg_with_importance(
context,
Some(
format!(
"bad-time-warning-{}",
chrono::NaiveDateTime::from_timestamp(now, 0).format("%Y-%m-%d") // repeat every day
)
.as_str(),
),
Some(&mut msg),
true,
)
.await
.ok();
return true;
}
false
}
async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time: i64) {
if now > approx_compile_time + DC_OUTDATED_WARNING_DAYS * 24 * 60 * 60 {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(
context
.stock_str(StockMessage::UpdateReminderMsgBody)
.await
.into(),
);
add_device_msg(
context,
Some(
format!(
"outdated-warning-{}",
chrono::NaiveDateTime::from_timestamp(now, 0).format("%Y-%m") // repeat every month
)
.as_str(),
),
Some(&mut msg),
)
.await
.ok();
}
}
/* Message-ID tools */
pub(crate) fn dc_create_id() -> String {
/* generate an id. the generated ID should be as short and as unique as possible:
@@ -296,6 +366,23 @@ pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) ->
}
}
pub async fn dc_delete_files_in_dir(context: &Context, path: impl AsRef<Path>) {
match async_std::fs::read_dir(path).await {
Ok(mut read_dir) => {
while let Some(entry) = read_dir.next().await {
match entry {
Ok(file) => {
dc_delete_file(context, file.file_name()).await;
}
Err(e) => warn!(context, "Could not read file to delete: {}", e),
}
}
}
Err(e) => warn!(context, "Could not read dir to delete: {}", e),
}
}
pub(crate) async fn dc_copy_file(
context: &Context,
src_path: impl AsRef<Path>,
@@ -612,8 +699,32 @@ pub(crate) fn listflags_has(listflags: u32, bitindex: usize) -> bool {
(listflags & bitindex) == bitindex
}
/// Makes sure that a user input that is not supposed to contain newlines does not contain newlines.
pub(crate) fn improve_single_line_input(input: impl AsRef<str>) -> String {
input
.as_ref()
.replace("\n", " ")
.replace("\r", " ")
.trim()
.to_string()
}
pub(crate) trait IsNoneOrEmpty<T> {
fn is_none_or_empty(&self) -> bool;
}
impl<T> IsNoneOrEmpty<T> for Option<T>
where
T: AsRef<str>,
{
fn is_none_or_empty(&self) -> bool {
!matches!(self, Some(s) if !s.as_ref().is_empty())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use std::convert::TryInto;
@@ -773,6 +884,9 @@ mod tests {
assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
}
use crate::chat;
use crate::chatlist::Chatlist;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use proptest::prelude::*;
proptest! {
@@ -960,4 +1074,129 @@ mod tests {
assert_eq!(w, 100);
assert_eq!(h, 50);
}
#[test]
fn test_improve_single_line_input() {
assert_eq!(improve_single_line_input("Hi\naiae "), "Hi aiae");
assert_eq!(improve_single_line_input("\r\nahte\n\r"), "ahte");
}
#[async_std::test]
async fn test_maybe_warn_on_bad_time() {
let t = TestContext::new().await;
let timestamp_now = time();
let timestamp_future = timestamp_now + 60 * 60 * 24 * 7;
let timestamp_past = NaiveDateTime::new(
NaiveDate::from_ymd(2020, 9, 1),
NaiveTime::from_hms(0, 0, 0),
)
.timestamp_millis()
/ 1_000;
// a correct time must not add a device message
maybe_warn_on_bad_time(&t.ctx, timestamp_now, get_provider_update_timestamp()).await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
// we cannot find out if a date in the future is wrong - a device message is not added
maybe_warn_on_bad_time(&t.ctx, timestamp_future, get_provider_update_timestamp()).await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
// a date in the past must add a device message
maybe_warn_on_bad_time(&t.ctx, timestamp_past, get_provider_update_timestamp()).await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), 1);
// the message should be added only once a day - test that an hour later and nearly a day later
maybe_warn_on_bad_time(
&t.ctx,
timestamp_past + 60 * 60,
get_provider_update_timestamp(),
)
.await;
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), 1);
maybe_warn_on_bad_time(
&t.ctx,
timestamp_past + 60 * 60 * 24 - 1,
get_provider_update_timestamp(),
)
.await;
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), 1);
// next day, there should be another device message
maybe_warn_on_bad_time(
&t.ctx,
timestamp_past + 60 * 60 * 24,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
assert_eq!(device_chat_id, chats.get_chat_id(0));
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), 2);
}
#[async_std::test]
async fn test_maybe_warn_on_outdated() {
let t = TestContext::new().await;
let timestamp_now: i64 = time();
// in about 6 months, the app should not be outdated
// (if this fails, provider-db is not updated since 6 months)
maybe_warn_on_outdated(
&t.ctx,
timestamp_now + 180 * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
// in 1 year, the app should be considered as outdated
maybe_warn_on_outdated(
&t.ctx,
timestamp_now + 365 * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), 1);
// do not repeat the warning every day ...
maybe_warn_on_outdated(
&t.ctx,
timestamp_now + (365 + 1) * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), 1);
// ... but every month
maybe_warn_on_outdated(
&t.ctx,
timestamp_now + (365 + 31) * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
assert_eq!(msgs.len(), 2);
}
}

View File

@@ -2,12 +2,10 @@
//!
//! A module to remove HTML tags from the email text
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
lazy_static! {
static ref LINE_RE: regex::Regex = regex::Regex::new(r"(\r?\n)+").unwrap();
}
static LINE_RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
struct Dehtml {
strbuilder: String,
@@ -24,8 +22,20 @@ enum AddText {
// dehtml() returns way too many newlines; however, an optimisation on this issue is not needed as
// the newlines are typically removed in further processing by the caller
pub fn dehtml(buf: &str) -> String {
let buf = buf.trim();
pub fn dehtml(buf: &str) -> Option<String> {
let s = dehtml_quick_xml(buf);
if !s.trim().is_empty() {
return Some(s);
}
let s = dehtml_manually(buf);
if !s.trim().is_empty() {
return Some(s);
}
None
}
pub fn dehtml_quick_xml(buf: &str) -> String {
let buf = buf.trim().trim_start_matches("<!doctype html>");
let mut dehtml = Dehtml {
strbuilder: String::with_capacity(buf.len()),
@@ -171,9 +181,28 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
}
}
pub fn dehtml_manually(buf: &str) -> String {
// Just strip out everything between "<" and ">"
let mut strbuilder = String::new();
let mut show_next_chars = true;
for c in buf.chars() {
match c {
'<' => show_next_chars = false,
'>' => show_next_chars = true,
_ => {
if show_next_chars {
strbuilder.push(c)
}
}
}
}
strbuilder
}
#[cfg(test)]
mod tests {
use super::*;
use crate::simplify::simplify;
#[test]
fn test_dehtml() {
@@ -182,27 +211,32 @@ mod tests {
"<a href='https://example.com'> Foo </a>",
"[ Foo ](https://example.com)",
),
("<img href='/foo.png'>", ""),
("<b> bar </b>", "* bar *"),
("<b> bar <i> foo", "* bar _ foo"),
("&amp; bar", "& bar"),
// Note missing '
("<a href='/foo.png>Hi</a> ", ""),
// Despite missing ', this should be shown:
("<a href='/foo.png>Hi</a> ", "Hi "),
(
"<a href='https://get.delta.chat/'/>",
"[](https://get.delta.chat/)",
),
("", ""),
("<!doctype html>\n<b>fat text</b>", "*fat text*"),
// Invalid html (at least DC should show the text if the html is invalid):
("<!some invalid html code>\n<b>some text</b>", "some text"),
];
for (input, output) in cases {
assert_eq!(dehtml(input), output);
assert_eq!(simplify(dehtml(input).unwrap(), true).0, output);
}
let none_cases = vec!["<html> </html>", ""];
for input in none_cases {
assert_eq!(dehtml(input), None);
}
}
#[test]
fn test_dehtml_parse_br() {
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2<br/>line3\n\r";
let plain = dehtml(html);
let plain = dehtml(html).unwrap();
assert_eq!(plain, "line1\n\r\r\rline2\nline3");
}
@@ -210,7 +244,7 @@ mod tests {
#[test]
fn test_dehtml_parse_href() {
let html = "<a href=url>text</a";
let plain = dehtml(html);
let plain = dehtml(html).unwrap();
assert_eq!(plain, "[text](url)");
}
@@ -218,7 +252,7 @@ mod tests {
#[test]
fn test_dehtml_bold_text() {
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
let plain = dehtml(html);
let plain = dehtml(html).unwrap();
assert_eq!(plain, "text *bold*<>");
}
@@ -228,7 +262,7 @@ mod tests {
let html =
"&lt;&gt;&quot;&apos;&amp; &auml;&Auml;&ouml;&Ouml;&uuml;&Uuml;&szlig; foo&AElig;&ccedil;&Ccedil; &diams;&lrm;&rlm;&zwnj;&noent;&zwj;";
let plain = dehtml(html);
let plain = dehtml(html).unwrap();
assert_eq!(
plain,
@@ -251,7 +285,7 @@ mod tests {
</body>
</html>
"##;
let txt = dehtml(input);
let txt = dehtml(input).unwrap();
assert_eq!(txt.trim(), "lots of text");
}
}

View File

@@ -51,23 +51,42 @@ impl EncryptHelper {
}
/// Determines if we can and should encrypt.
///
/// For encryption to be enabled, `e2ee_guaranteed` should be true, or strictly more than a half
/// of peerstates should prefer encryption. Own preference is counted equally to peer
/// preferences, even if message copy is not sent to self.
///
/// `e2ee_guaranteed` should be set to true for replies to encrypted messages (as required by
/// Autocrypt Level 1, version 1.1) and for messages sent in protected groups.
///
/// Returns an error if `e2ee_guaranteed` is true, but one or more keys are missing.
pub fn should_encrypt(
&self,
context: &Context,
e2ee_guaranteed: bool,
peerstates: &[(Option<Peerstate>, &str)],
) -> Result<bool> {
if !(self.prefer_encrypt == EncryptPreference::Mutual || e2ee_guaranteed) {
return Ok(false);
}
let mut prefer_encrypt_count = if self.prefer_encrypt == EncryptPreference::Mutual {
1
} else {
0
};
for (peerstate, addr) in peerstates {
match peerstate {
Some(peerstate) => {
if peerstate.prefer_encrypt != EncryptPreference::Mutual && !e2ee_guaranteed {
info!(context, "peerstate for {:?} is no-encrypt", addr);
return Ok(false);
}
info!(
context,
"peerstate for {:?} is {}", addr, peerstate.prefer_encrypt
);
match peerstate.prefer_encrypt {
EncryptPreference::NoPreference => {}
EncryptPreference::Mutual => prefer_encrypt_count += 1,
EncryptPreference::Reset => {
if !e2ee_guaranteed {
return Ok(false);
}
}
};
}
None => {
let msg = format!("peerstate for {:?} missing, cannot encrypt", addr);
@@ -81,7 +100,11 @@ impl EncryptHelper {
}
}
Ok(true)
// Count number of recipients, including self.
// This does not depend on whether we send a copy to self or not.
let recipients_count = peerstates.len() + 1;
Ok(e2ee_guaranteed || 2 * prefer_encrypt_count > recipients_count)
}
/// Tries to encrypt the passed in `mail`.
@@ -135,41 +158,31 @@ pub async fn try_decrypt(
.map(|from| from.addr)
.unwrap_or_default();
let mut peerstate = None;
let autocryptheader = Aheader::from_headers(context, &from, &mail.headers);
if message_time > 0 {
peerstate = Peerstate::from_addr(context, &from).await?;
let mut peerstate = Peerstate::from_addr(context, &from).await?;
// Apply Autocrypt header
if let Some(ref header) = Aheader::from_headers(context, &from, &mail.headers) {
if let Some(ref mut peerstate) = peerstate {
if let Some(ref header) = autocryptheader {
peerstate.apply_header(&header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else if message_time > peerstate.last_seen_autocrypt && !contains_report(mail) {
peerstate.degrade_encryption(message_time);
peerstate.save_to_db(&context.sql, false).await?;
}
} else if let Some(ref header) = autocryptheader {
peerstate.apply_header(&header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else {
let p = Peerstate::from_header(context, header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
}
/* possibly perform decryption */
// Possibly perform decryption
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context).await?;
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
let mut signatures = HashSet::default();
if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 {
peerstate = Peerstate::from_addr(&context, &from).await?;
}
if let Some(peerstate) = peerstate {
if let Some(ref mut peerstate) = peerstate {
peerstate.handle_fingerprint_change(context).await?;
if let Some(key) = peerstate.public_key {
public_keyring_for_validate.add(key);
} else if let Some(key) = peerstate.gossip_key {
public_keyring_for_validate.add(key);
if let Some(key) = &peerstate.public_key {
public_keyring_for_validate.add(key.clone());
} else if let Some(key) = &peerstate.gossip_key {
public_keyring_for_validate.add(key.clone());
}
}
@@ -181,6 +194,18 @@ pub async fn try_decrypt(
&mut signatures,
)
.await?;
if let Some(mut peerstate) = peerstate {
// If message is not encrypted and it is not a read receipt, degrade encryption.
if out_mail.is_none()
&& message_time > peerstate.last_seen_autocrypt
&& !contains_report(mail)
{
peerstate.degrade_encryption(message_time);
peerstate.save_to_db(&context.sql, false).await?;
}
}
Ok((out_mail, signatures))
}
@@ -210,9 +235,9 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail
}
}
async fn decrypt_if_autocrypt_message<'a>(
async fn decrypt_if_autocrypt_message(
context: &Context,
mail: &ParsedMail<'a>,
mail: &ParsedMail<'_>,
private_keyring: Keyring<SignedSecretKey>,
public_keyring_for_validate: Keyring<SignedPublicKey>,
ret_valid_signatures: &mut HashSet<Fingerprint>,
@@ -319,6 +344,11 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
mod tests {
use super::*;
use crate::chat;
use crate::constants::Viewtype;
use crate::contact::{Contact, Origin};
use crate::message::Message;
use crate::param::Param;
use crate::test_utils::*;
mod ensure_secret_key_exists {
@@ -379,4 +409,161 @@ Sent with my Delta Chat Messenger: https://delta.chat";
let data = b"blas";
assert_eq!(has_decrypted_pgp_armor(data), false);
}
#[async_std::test]
async fn test_encrypted_no_autocrypt() -> crate::error::Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let (contact_alice_id, _modified) = Contact::add_or_lookup(
&bob.ctx,
"Alice",
"alice@example.com",
Origin::ManuallyCreated,
)
.await?;
let (contact_bob_id, _modified) = Contact::add_or_lookup(
&alice.ctx,
"Bob",
"bob@example.net",
Origin::ManuallyCreated,
)
.await?;
let chat_alice = chat::create_by_contact_id(&alice.ctx, contact_bob_id).await?;
let chat_bob = chat::create_by_contact_id(&bob.ctx, contact_alice_id).await?;
// Alice sends unencrypted message to Bob
let mut msg = Message::new(Viewtype::Text);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
// Bob receives unencrypted message from Alice
let msg = bob.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
// Parsing a message is enough to update peerstate
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
.await?
.expect("no peerstate found in the database");
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
// Bob sends encrypted message to Alice
let mut msg = Message::new(Viewtype::Text);
chat::prepare_msg(&bob.ctx, chat_bob, &mut msg).await?;
chat::send_msg(&bob.ctx, chat_bob, &mut msg).await?;
let sent = bob.pop_sent_msg().await;
// Alice receives encrypted message from Bob
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
let peerstate_bob = Peerstate::from_addr(&alice.ctx, "bob@example.net")
.await?
.expect("no peerstate found in the database");
assert_eq!(peerstate_bob.prefer_encrypt, EncryptPreference::Mutual);
// Now Alice and Bob have established keys.
// Alice sends encrypted message without Autocrypt header.
let mut msg = Message::new(Viewtype::Text);
msg.param.set_int(Param::SkipAutocrypt, 1);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
.await?
.expect("no peerstate found in the database");
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
// Alice sends plaintext message with Autocrypt header.
let mut msg = Message::new(Viewtype::Text);
msg.param.set_int(Param::ForcePlaintext, 1);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
.await?
.expect("no peerstate found in the database");
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Mutual);
// Alice sends plaintext message without Autocrypt header.
let mut msg = Message::new(Viewtype::Text);
msg.param.set_int(Param::ForcePlaintext, 1);
msg.param.set_int(Param::SkipAutocrypt, 1);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
let peerstate_alice = Peerstate::from_addr(&bob.ctx, "alice@example.com")
.await?
.expect("no peerstate found in the database");
assert_eq!(peerstate_alice.prefer_encrypt, EncryptPreference::Reset);
Ok(())
}
fn new_peerstates(
ctx: &Context,
prefer_encrypt: EncryptPreference,
) -> Vec<(Option<Peerstate<'_>>, &str)> {
let addr = "bob@foo.bar";
let pub_key = bob_keypair().public;
let peerstate = Peerstate {
context: &ctx,
addr: addr.into(),
last_seen: 13,
last_seen_autocrypt: 14,
prefer_encrypt,
public_key: Some(pub_key.clone()),
public_key_fingerprint: Some(pub_key.fingerprint()),
gossip_key: Some(pub_key.clone()),
gossip_timestamp: 15,
gossip_key_fingerprint: Some(pub_key.fingerprint()),
verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.fingerprint()),
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
let mut peerstates = Vec::new();
peerstates.push((Some(peerstate), addr));
peerstates
}
#[async_std::test]
async fn test_should_encrypt() {
let t = TestContext::new_alice().await;
let encrypt_helper = EncryptHelper::new(&t.ctx).await.unwrap();
// test with EncryptPreference::NoPreference:
// if e2ee_eguaranteed is unset, there is no encryption as not more than half of peers want encryption
let ps = new_peerstates(&t.ctx, EncryptPreference::NoPreference);
assert!(encrypt_helper.should_encrypt(&t.ctx, true, &ps).unwrap());
assert!(!encrypt_helper.should_encrypt(&t.ctx, false, &ps).unwrap());
// test with EncryptPreference::Reset
let ps = new_peerstates(&t.ctx, EncryptPreference::Reset);
assert!(encrypt_helper.should_encrypt(&t.ctx, true, &ps).unwrap());
assert!(!encrypt_helper.should_encrypt(&t.ctx, false, &ps).unwrap());
// test with EncryptPreference::Mutual (self is also Mutual)
let ps = new_peerstates(&t.ctx, EncryptPreference::Mutual);
assert!(encrypt_helper.should_encrypt(&t.ctx, true, &ps).unwrap());
assert!(encrypt_helper.should_encrypt(&t.ctx, false, &ps).unwrap());
// test with missing peerstate
let mut ps = Vec::new();
ps.push((None, "bob@foo.bar"));
assert!(encrypt_helper.should_encrypt(&t.ctx, true, &ps).is_err());
assert!(!encrypt_helper.should_encrypt(&t.ctx, false, &ps).unwrap());
}
}

View File

@@ -58,12 +58,18 @@ impl EventEmitter {
/// Async recv of an event. Return `None` if the `Sender` has been droped.
pub async fn recv(&self) -> Option<Event> {
// TODO: change once we can use async channels internally.
self.0.recv().await.ok()
}
}
pub fn try_recv(&self) -> Result<Event, async_std::sync::TryRecvError> {
self.0.try_recv()
impl async_std::stream::Stream for EventEmitter {
type Item = Event;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
std::pin::Pin::new(&mut self.0).poll_next(cx)
}
}
@@ -119,10 +125,6 @@ pub enum EventType {
#[strum(props(id = "105"))]
ImapMessageMoved(String),
/// Emitted when an IMAP folder was emptied
#[strum(props(id = "106"))]
ImapFolderEmptied(String),
/// Emitted when an new file in the $BLOBDIR was created
#[strum(props(id = "150"))]
NewBlobFile(String),
@@ -190,6 +192,11 @@ pub enum EventType {
#[strum(props(id = "2005"))]
IncomingMsg { chat_id: ChatId, msg_id: MsgId },
/// Messages were seen or noticed.
/// chat id is always set.
#[strum(props(id = "2008"))]
MsgsNoticed(ChatId),
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
#[strum(props(id = "2010"))]
@@ -237,10 +244,16 @@ pub enum EventType {
LocationChanged(Option<u32>),
/// Inform about the configuration progress started by configure().
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
#[strum(props(id = "2041"))]
ConfigureProgress(usize),
ConfigureProgress {
/// Progress.
///
/// 0=error, 1-999=progress in permille, 1000=success and done
progress: usize,
/// Progress comment or error, something to display to the user.
comment: Option<String>,
},
/// Inform about the import/export progress started by imex().
///

177
src/format_flowed.rs Normal file
View File

@@ -0,0 +1,177 @@
///! # format=flowed support
///!
///! Format=flowed is defined in
///! [RFC 3676](https://tools.ietf.org/html/rfc3676).
///!
///! Older [RFC 2646](https://tools.ietf.org/html/rfc2646) is used
///! during formatting, i.e., DelSp parameter introduced in RFC 3676
///! is assumed to be set to "no".
///!
///! For received messages, DelSp parameter is honoured.
/// Wraps line to 72 characters using format=flowed soft breaks.
///
/// 72 characters is the limit recommended by RFC 3676.
///
/// The function breaks line only after SP and before non-whitespace
/// characters. It also does not insert breaks before ">" to avoid the
/// need to do space stuffing (see RFC 3676) for quotes.
///
/// If there are long words, line may still exceed the limits on line
/// length. However, this should be rare and should not result in
/// immediate mail rejection: SMTP (RFC 2821) limit is 998 characters,
/// and Spam Assassin limit is 78 characters.
fn format_line_flowed(line: &str, prefix: &str) -> String {
let mut result = String::new();
let mut buffer = prefix.to_string();
let mut after_space = false;
for c in line.chars() {
if c == ' ' {
buffer.push(c);
after_space = true;
} else if c == '>' {
if buffer.is_empty() {
// Space stuffing, see RFC 3676
buffer.push(' ');
}
buffer.push(c);
after_space = false;
} else {
if after_space && buffer.len() >= 72 && !c.is_whitespace() {
// Flush the buffer and insert soft break (SP CRLF).
result += &buffer;
result += "\r\n";
buffer = prefix.to_string();
}
buffer.push(c);
after_space = false;
}
}
result + &buffer
}
fn format_flowed_prefix(text: &str, prefix: &str) -> String {
let mut result = String::new();
for line in text.split('\n') {
if !result.is_empty() {
result += "\r\n";
}
let line = line.trim_end();
if prefix.len() + line.len() > 78 {
result += &format_line_flowed(line, prefix);
} else {
result += prefix;
if prefix.is_empty() && line.starts_with('>') {
// Space stuffing, see RFC 3676
result.push(' ');
}
result += line;
}
}
result
}
/// Returns text formatted according to RFC 3767 (format=flowed).
///
/// This function accepts text separated by LF, but returns text
/// separated by CRLF.
///
/// RFC 2646 technique is used to insert soft line breaks, so DelSp
/// SHOULD be set to "no" when sending.
pub fn format_flowed(text: &str) -> String {
format_flowed_prefix(text, "")
}
/// Same as format_flowed(), but adds "> " prefix to each line.
pub fn format_flowed_quote(text: &str) -> String {
format_flowed_prefix(text, "> ")
}
/// Joins lines in format=flowed text.
///
/// Lines must be separated by single LF.
///
/// Quote processing is not supported, it is assumed that they are
/// deleted during simplification.
///
/// Signature separator line is not processed here, it is assumed to
/// be stripped beforehand.
pub fn unformat_flowed(text: &str, delsp: bool) -> String {
let mut result = String::new();
let mut skip_newline = true;
for line in text.split('\n') {
// Revert space-stuffing
let line = line.strip_prefix(" ").unwrap_or(line);
if !skip_newline {
result.push('\n');
}
if let Some(line) = line.strip_suffix(" ") {
// Flowed line
result += line;
if !delsp {
result.push(' ');
}
skip_newline = true;
} else {
// Fixed line
result += line;
skip_newline = false;
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_flowed() {
let text = "Foo bar baz";
assert_eq!(format_flowed(text), "Foo bar baz");
let text = "This is the Autocrypt Setup Message used to transfer your key between clients.\n\
\n\
To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
let expected = "This is the Autocrypt Setup Message used to transfer your key between clients.\r\n\
\r\n\
To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
client and enter the setup code presented on the generating device.";
assert_eq!(format_flowed(text), expected);
let text = "> Not a quote";
assert_eq!(format_flowed(text), " > Not a quote");
}
#[test]
fn test_unformat_flowed() {
let text = "this is a very long message that should be wrapped using format=flowed and \n\
unwrapped on the receiver";
let expected =
"this is a very long message that should be wrapped using format=flowed and \
unwrapped on the receiver";
assert_eq!(unformat_flowed(text, false), expected);
}
#[test]
fn test_format_flowed_quote() {
let quote = "this is a quoted line";
let expected = "> this is a quoted line";
assert_eq!(format_flowed_quote(quote), expected);
let quote = "> foo bar baz";
let expected = "> > foo bar baz";
assert_eq!(format_flowed_quote(quote), expected);
let quote = "this is a very long quote that should be wrapped using format=flowed and unwrapped on the receiver";
let expected =
"> this is a very long quote that should be wrapped using format=flowed and \r\n\
> unwrapped on the receiver";
assert_eq!(format_flowed_quote(quote), expected);
}
}

View File

@@ -3,7 +3,6 @@ use mailparse::{MailHeader, MailHeaderMap};
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
#[strum(serialize_all = "kebab_case")]
#[allow(dead_code)]
pub enum HeaderDef {
MessageId,
Subject,

View File

@@ -5,31 +5,11 @@ use async_imap::types::UnsolicitedResponse;
use async_std::prelude::*;
use std::time::{Duration, SystemTime};
use crate::error::{bail, format_err, Result};
use crate::{context::Context, scheduler::InterruptInfo};
use super::select_folder;
use super::session::Session;
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("IMAP IDLE protocol failed to init/complete: {0}")]
IdleProtocolFailed(#[from] async_imap::error::Error),
#[error("IMAP IDLE protocol timed out: {0}")]
IdleTimeout(#[from] async_std::future::TimeoutError),
#[error("IMAP server does not have IDLE capability")]
IdleAbilityMissing,
#[error("IMAP select folder error: {0}")]
SelectFolderError(#[from] select_folder::Error),
#[error("Setup handle error: {0}")]
SetupHandleError(#[from] super::Error),
}
impl Imap {
pub fn can_idle(&self) -> bool {
self.config.can_idle
@@ -43,9 +23,9 @@ impl Imap {
use futures::future::FutureExt;
if !self.can_idle() {
return Err(Error::IdleAbilityMissing);
bail!("IMAP server does not have IDLE capability");
}
self.setup_handle_if_needed(context).await?;
self.setup_handle(context).await?;
self.select_folder(context, watch_folder.clone()).await?;
@@ -72,7 +52,7 @@ impl Imap {
let mut handle = session.idle();
if let Err(err) = handle.init().await {
return Err(Error::IdleProtocolFailed(err));
bail!("IMAP IDLE protocol failed to init/complete: {}", err);
}
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
@@ -115,7 +95,7 @@ impl Imap {
.done()
.timeout(Duration::from_secs(15))
.await
.map_err(Error::IdleTimeout)??;
.map_err(|err| format_err!("IMAP IDLE protocol timed out: {}", err))??;
self.session = Some(Session { inner: session });
} else {
warn!(context, "Attempted to idle without a session");
@@ -133,12 +113,13 @@ impl Imap {
// in this case, we're waiting for a configure job (and an interrupt).
let fake_idle_start_time = SystemTime::now();
info!(context, "IMAP-fake-IDLEing...");
// Do not poll, just wait for an interrupt when no folder is passed in.
if watch_folder.is_none() {
info!(context, "IMAP-fake-IDLE: no folder, waiting for interrupt");
return self.idle_interrupt.recv().await.unwrap_or_default();
}
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
// check every minute if there are new messages
// TODO: grow sleep durations / make them more flexible
@@ -180,7 +161,7 @@ impl Imap {
// will not find any new.
if let Some(ref watch_folder) = watch_folder {
match self.fetch_new_messages(context, watch_folder).await {
match self.fetch_new_messages(context, watch_folder, false).await {
Ok(res) => {
info!(context, "fetch_new_messages returned {:?}", res);
if res {

View File

@@ -3,8 +3,9 @@
//! uses [async-email/async-imap](https://github.com/async-email/async-imap)
//! to implement connect, fetch, delete functionality with standard IMAP servers.
use std::collections::BTreeMap;
use std::{cmp, collections::BTreeMap};
use anyhow::Context as _;
use async_imap::{
error::Result as ImapResult,
types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute},
@@ -13,76 +14,35 @@ use async_std::prelude::*;
use async_std::sync::Receiver;
use num_traits::FromPrimitive;
use crate::config::*;
use crate::constants::*;
use crate::context::Context;
use crate::dc_receive_imf::{
dc_receive_imf, from_field_to_contact_id, is_msgrmsg_rfc724_mid_in_list,
};
use crate::dc_receive_imf::{from_field_to_contact_id, is_msgrmsg_rfc724_mid_in_list};
use crate::error::{bail, format_err, Result};
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job::{self, Action};
use crate::login_param::{CertificateChecks, LoginParam};
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::message::{self, update_server_uid, MessageState};
use crate::mimeparser;
use crate::oauth2::dc_get_oauth2_access_token;
use crate::param::Params;
use crate::provider::get_provider_info;
use crate::{chat, scheduler::InterruptInfo, stock::StockMessage};
use crate::provider::{get_provider_info, Socket};
use crate::{
chat, dc_tools::dc_extract_grpid_from_rfc724_mid, scheduler::InterruptInfo, stock::StockMessage,
};
use crate::{config::*, dc_receive_imf::dc_receive_imf_inner};
mod client;
mod idle;
pub mod select_folder;
mod session;
use chat::get_chat_id_by_grpid;
use client::Client;
use mailparse::SingleInfo;
use message::Message;
use session::Session;
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("IMAP Connect without configured params")]
ConnectWithoutConfigure,
#[error("IMAP Connection Failed params: {0}")]
ConnectionFailed(String),
#[error("IMAP No Connection established")]
NoConnection,
#[error("IMAP Could not get OAUTH token")]
OauthError,
#[error("IMAP Could not login as {0}")]
LoginFailed(String),
#[error("IMAP Could not fetch")]
FetchFailed(#[from] async_imap::error::Error),
#[error("IMAP operation attempted while it is torn down")]
InTeardown,
#[error("IMAP operation attempted while it is torn down")]
SqlError(#[from] crate::sql::Error),
#[error("IMAP got error from elsewhere")]
WrappedError(#[from] crate::error::Error),
#[error("IMAP select folder error")]
SelectFolderError(#[from] select_folder::Error),
#[error("Mail parse error")]
MailParseError(#[from] mailparse::MailParseError),
#[error("No mailbox selected, folder: {0}")]
NoMailbox(String),
#[error("IMAP other error: {0}")]
Other(String),
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq)]
pub enum ImapActionResult {
Failed,
@@ -105,9 +65,9 @@ const PREFETCH_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
AUTOCRYPT-SETUP-MESSAGE\
)])";
const DELETE_CHECK_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])";
const RFC724MID_UID: &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:*";
#[derive(Debug)]
pub struct Imap {
@@ -147,12 +107,9 @@ enum FolderMeaning {
#[derive(Debug)]
struct ImapConfig {
pub addr: String,
pub imap_server: String,
pub imap_port: u16,
pub imap_user: String,
pub imap_pw: String,
pub lp: ServerLoginParam,
pub strict_tls: bool,
pub server_flags: usize,
pub oauth2: bool,
pub selected_folder: Option<String>,
pub selected_mailbox: Option<Mailbox>,
pub selected_folder_needs_expunge: bool,
@@ -167,12 +124,9 @@ impl Default for ImapConfig {
fn default() -> Self {
ImapConfig {
addr: "".into(),
imap_server: "".into(),
imap_port: 0,
imap_user: "".into(),
imap_pw: "".into(),
lp: Default::default(),
strict_tls: false,
server_flags: 0,
oauth2: false,
selected_folder: None,
selected_mailbox: None,
selected_folder_needs_expunge: false,
@@ -207,9 +161,13 @@ impl Imap {
self.should_reconnect = true;
}
async fn setup_handle_if_needed(&mut self, context: &Context) -> Result<()> {
if self.config.imap_server.is_empty() {
return Err(Error::InTeardown);
/// Connects or reconnects if needed.
///
/// It is safe to call this function if already connected, actions
/// are performed only as needed.
async fn try_setup_handle(&mut self, context: &Context) -> Result<()> {
if self.config.lp.server.is_empty() {
bail!("IMAP operation attempted while it is torn down");
}
if self.should_reconnect() {
@@ -219,40 +177,40 @@ impl Imap {
return Ok(());
}
let server_flags = self.config.server_flags as i32;
let oauth2 = self.config.oauth2;
let connection_res: ImapResult<Client> =
if (server_flags & (DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_PLAIN)) != 0 {
let config = &mut self.config;
let imap_server: &str = config.imap_server.as_ref();
let imap_port = config.imap_port;
let connection_res: ImapResult<Client> = if self.config.lp.security == Socket::STARTTLS
|| self.config.lp.security == Socket::Plain
{
let config = &mut self.config;
let imap_server: &str = config.lp.server.as_ref();
let imap_port = config.lp.port;
match Client::connect_insecure((imap_server, imap_port)).await {
Ok(client) => {
if (server_flags & DC_LP_IMAP_SOCKET_STARTTLS) != 0 {
client.secure(imap_server, config.strict_tls).await
} else {
Ok(client)
}
match Client::connect_insecure((imap_server, imap_port)).await {
Ok(client) => {
if config.lp.security == Socket::STARTTLS {
client.secure(imap_server, config.strict_tls).await
} else {
Ok(client)
}
Err(err) => Err(err),
}
} else {
let config = &self.config;
let imap_server: &str = config.imap_server.as_ref();
let imap_port = config.imap_port;
Err(err) => Err(err),
}
} else {
let config = &self.config;
let imap_server: &str = config.lp.server.as_ref();
let imap_port = config.lp.port;
Client::connect_secure((imap_server, imap_port), imap_server, config.strict_tls)
.await
};
Client::connect_secure((imap_server, imap_port), imap_server, config.strict_tls).await
};
let login_res = match connection_res {
Ok(client) => {
let config = &self.config;
let imap_user: &str = config.imap_user.as_ref();
let imap_pw: &str = config.imap_pw.as_ref();
let imap_user: &str = config.lp.user.as_ref();
let imap_pw: &str = config.lp.password.as_ref();
if (server_flags & DC_LP_AUTH_OAUTH2) != 0 {
if oauth2 {
let addr: &str = config.addr.as_ref();
if let Some(token) =
@@ -264,28 +222,14 @@ impl Imap {
};
client.authenticate("XOAUTH2", auth).await
} else {
return Err(Error::OauthError);
bail!("IMAP Could not get OAUTH token");
}
} else {
client.login(imap_user, imap_pw).await
}
}
Err(err) => {
let message = {
let config = &self.config;
let imap_server: &str = config.imap_server.as_ref();
let imap_port = config.imap_port;
context
.stock_string_repl_str2(
StockMessage::ServerResponse,
format!("IMAP {}:{}", imap_server, imap_port),
err.to_string(),
)
.await
};
// IMAP connection failures are reported to users
emit_event!(context, EventType::ErrorNetwork(message));
return Err(Error::ConnectionFailed(err.to_string()));
bail!(err);
}
};
@@ -301,13 +245,12 @@ impl Imap {
}
Err((err, _)) => {
let imap_user = self.config.imap_user.to_owned();
let imap_user = self.config.lp.user.to_owned();
let message = context
.stock_string_repl_str(StockMessage::CannotLogin, &imap_user)
.await;
warn!(context, "{} ({})", message, err);
emit_event!(context, EventType::ErrorNetwork(message.clone()));
let lock = context.wrong_pw_warning_mutex.lock().await;
if self.login_failed_once
@@ -319,7 +262,7 @@ impl Imap {
drop(lock);
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(message);
msg.text = Some(message.clone());
if let Err(e) =
chat::add_device_msg_with_importance(context, None, Some(&mut msg), true)
.await
@@ -331,11 +274,24 @@ impl Imap {
}
self.trigger_reconnect();
Err(Error::LoginFailed(format!("cannot login as {}", imap_user)))
Err(format_err!("{}\n\n{}", message, err))
}
}
}
/// Connects or reconnects if not already connected.
///
/// This function emits network error if it fails. It should not
/// be used during configuration to avoid showing failed attempt
/// errors to the user.
async fn setup_handle(&mut self, context: &Context) -> Result<()> {
let res = self.try_setup_handle(context).await;
if let Err(ref err) = res {
emit_event!(context, EventType::ErrorNetwork(err.to_string()));
}
res
}
async fn unsetup_handle(&mut self, context: &Context) {
// Close folder if messages should be expunged
if let Err(err) = self.close_folder(context).await {
@@ -357,56 +313,64 @@ impl Imap {
let mut cfg = &mut self.config;
cfg.addr = "".into();
cfg.imap_server = "".into();
cfg.imap_user = "".into();
cfg.imap_pw = "".into();
cfg.imap_port = 0;
cfg.lp = Default::default();
cfg.can_idle = false;
cfg.can_move = false;
}
/// Connects to imap account using already-configured parameters.
/// Connects to IMAP account using already-configured parameters.
///
/// Emits network error if connection fails.
pub async fn connect_configured(&mut self, context: &Context) -> Result<()> {
if self.is_connected() && !self.should_reconnect() {
return Ok(());
}
if !context.is_configured().await {
return Err(Error::ConnectWithoutConfigure);
bail!("IMAP Connect without configured params");
}
let param = LoginParam::from_database(context, "configured_").await;
// the trailing underscore is correct
if self.connect(context, &param).await {
self.ensure_configured_folders(context, true).await
if let Err(err) = self
.connect(
context,
&param.imap,
&param.addr,
param.server_flags & DC_LP_AUTH_OAUTH2 != 0,
)
.await
{
bail!("IMAP Connection Failed with params {}: {}", param, err);
} else {
Err(Error::ConnectionFailed(format!("{}", param)))
self.ensure_configured_folders(context, true).await
}
}
/// Tries connecting to imap account using the specific login parameters.
pub async fn connect(&mut self, context: &Context, lp: &LoginParam) -> bool {
if lp.mail_server.is_empty() || lp.mail_user.is_empty() || lp.mail_pw.is_empty() {
return false;
///
/// `addr` is used to renew token if OAuth2 authentication is used.
///
/// Does not emit network errors, can be used to try various
/// parameters during autoconfiguration.
pub async fn connect(
&mut self,
context: &Context,
lp: &ServerLoginParam,
addr: &str,
oauth2: bool,
) -> Result<()> {
if lp.server.is_empty() || lp.user.is_empty() || lp.password.is_empty() {
bail!("Incomplete IMAP connection parameters");
}
{
let addr = &lp.addr;
let imap_server = &lp.mail_server;
let imap_port = lp.mail_port as u16;
let imap_user = &lp.mail_user;
let imap_pw = &lp.mail_pw;
let server_flags = lp.server_flags as usize;
let mut config = &mut self.config;
config.addr = addr.to_string();
config.imap_server = imap_server.to_string();
config.imap_port = imap_port;
config.imap_user = imap_user.to_string();
config.imap_pw = imap_pw.to_string();
let provider = get_provider_info(&lp.addr);
config.strict_tls = match lp.imap_certificate_checks {
config.lp = lp.clone();
let provider = get_provider_info(&addr);
config.strict_tls = match lp.certificate_checks {
CertificateChecks::Automatic => {
provider.map_or(false, |provider| provider.strict_tls)
}
@@ -414,20 +378,20 @@ impl Imap {
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => false,
};
config.server_flags = server_flags;
config.oauth2 = oauth2;
}
if let Err(err) = self.setup_handle_if_needed(context).await {
warn!(context, "failed to setup imap handle: {}", err);
if let Err(err) = self.try_setup_handle(context).await {
warn!(context, "try_setup_handle: {}", err);
self.free_connect_params().await;
return false;
return Err(err);
}
let teardown = match &mut self.session {
Some(ref mut session) => match session.capabilities().await {
Ok(caps) => {
if !context.sql.is_open().await {
warn!(context, "IMAP-LOGIN as {} ok but ABORTING", lp.mail_user,);
warn!(context, "IMAP-LOGIN as {} ok but ABORTING", lp.user,);
true
} else {
let can_idle = caps.has_str("IDLE");
@@ -447,7 +411,7 @@ impl Imap {
context,
EventType::ImapConnected(format!(
"IMAP-LOGIN as {}, capabilities: {}",
lp.mail_user, caps_list,
lp.user, caps_list,
))
);
false
@@ -464,10 +428,12 @@ impl Imap {
if teardown {
self.disconnect(context).await;
false
} else {
true
warn!(
context,
"IMAP disconnected immediately after connecting due to error"
);
}
Ok(())
}
pub async fn disconnect(&mut self, context: &Context) {
@@ -478,11 +444,14 @@ impl Imap {
pub async fn fetch(&mut self, context: &Context, watch_folder: &str) -> Result<()> {
if !context.sql.is_open().await {
// probably shutdown
return Err(Error::InTeardown);
bail!("IMAP operation attempted while it is torn down");
}
self.setup_handle_if_needed(context).await?;
self.setup_handle(context).await?;
while self.fetch_new_messages(context, &watch_folder).await? {
while self
.fetch_new_messages(context, &watch_folder, false)
.await?
{
// We fetch until no more new messages are there.
}
Ok(())
@@ -498,22 +467,89 @@ impl Imap {
// the entry has the format `imap.mailbox.<folder>=<uidvalidity>:<lastseenuid>`
let mut parts = entry.split(':');
(
parts
.next()
.unwrap_or_default()
.parse()
.unwrap_or_else(|_| 0),
parts
.next()
.unwrap_or_default()
.parse()
.unwrap_or_else(|_| 0),
parts.next().unwrap_or_default().parse().unwrap_or(0),
parts.next().unwrap_or_default().parse().unwrap_or(0),
)
} else {
(0, 0)
}
}
/// Synchronizes UIDs in the database with UIDs on the server.
///
/// It is assumed that no operations are taking place on the same
/// folder at the moment. Make sure to run it in the same
/// thread/task as other network operations on this folder to
/// avoid race conditions.
pub(crate) async fn resync_folder_uids(
&mut self,
context: &Context,
folder: String,
) -> Result<()> {
// Collect pairs of UID and Message-ID.
let mut msg_ids = BTreeMap::new();
self.select_folder(context, Some(&folder)).await?;
let session = if let Some(ref mut session) = &mut self.session {
session
} else {
bail!("IMAP No Connection established");
};
match session.uid_fetch("1:*", RFC724MID_UID).await {
Ok(mut list) => {
while let Some(fetch) = list.next().await {
let msg = fetch?;
// Get Message-ID
let message_id = get_fetch_headers(&msg)
.and_then(|headers| prefetch_get_message_id(&headers))
.ok();
if let (Some(uid), Some(rfc724_mid)) = (msg.uid, message_id) {
msg_ids.insert(uid, rfc724_mid);
}
}
}
Err(err) => {
bail!("Can't resync folder {}: {}", folder, err);
}
}
info!(
context,
"Resync: collected {} message IDs in folder {}",
msg_ids.len(),
&folder
);
// Write collected UIDs to SQLite database.
context
.sql
.with_conn(move |mut conn| {
let conn2 = &mut conn;
let tx = conn2.transaction()?;
tx.execute(
"UPDATE msgs SET server_uid=0 WHERE server_folder=?",
params![folder],
)?;
for (uid, rfc724_mid) in &msg_ids {
// This may detect previously undetected moved
// messages, so we update server_folder too.
tx.execute(
"UPDATE msgs \
SET server_folder=?,server_uid=? WHERE rfc724_mid=?",
params![folder, uid, rfc724_mid],
)?;
}
tx.commit()?;
Ok(())
})
.await?;
Ok(())
}
/// return Result with (uid_validity, last_seen_uid) tuple.
pub(crate) async fn select_with_uidvalidity(
&mut self,
@@ -529,13 +565,12 @@ impl Imap {
let mailbox = config
.selected_mailbox
.as_ref()
.ok_or_else(|| Error::NoMailbox(folder.to_string()))?;
.ok_or_else(|| format_err!("No mailbox selected, folder: {}", folder))?;
let new_uid_validity = match mailbox.uid_validity {
Some(v) => v,
None => {
let s = format!("No UIDVALIDITY for folder {:?}", folder);
return Err(Error::Other(s));
bail!("No UIDVALIDITY for folder {:?}", folder);
}
};
@@ -582,21 +617,24 @@ impl Imap {
if let Some(new_last_seen_uid) = new_last_seen_uid {
new_last_seen_uid
} else {
return Err(Error::Other("failed to fetch".into()));
bail!("failed to fetch");
}
}
Err(err) => {
return Err(Error::FetchFailed(err));
bail!("IMAP Could not fetch: {}", err);
}
}
} else {
return Err(Error::NoConnection);
bail!("IMAP No Connection established");
}
}
};
self.set_config_last_seen_uid(context, &folder, new_uid_validity, new_last_seen_uid)
.await;
if uid_validity != 0 || last_seen_uid != 0 {
job::schedule_resync(context).await;
}
info!(
context,
"uid/validity change: new {}/{} current {}/{}",
@@ -608,10 +646,11 @@ impl Imap {
Ok((new_uid_validity, new_last_seen_uid))
}
async fn fetch_new_messages<S: AsRef<str>>(
pub(crate) async fn fetch_new_messages<S: AsRef<str>>(
&mut self,
context: &Context,
folder: S,
fetch_existing_msgs: bool,
) -> Result<bool> {
let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await)
.unwrap_or_default();
@@ -620,7 +659,11 @@ impl Imap {
.select_with_uidvalidity(context, folder.as_ref())
.await?;
let msgs = self.fetch_after(context, last_seen_uid).await?;
let msgs = if fetch_existing_msgs {
self.fetch_existing_msgs_prefetch().await?
} else {
self.fetch_after(context, last_seen_uid).await?
};
let read_cnt = msgs.len();
let folder: &str = folder.as_ref();
@@ -660,8 +703,9 @@ impl Imap {
}
// check passed, go fetch the emails
let (new_last_seen_uid_processed, error_cnt) =
self.fetch_many_msgs(context, &folder, &uids).await;
let (new_last_seen_uid_processed, error_cnt) = self
.fetch_many_msgs(context, &folder, &uids, fetch_existing_msgs)
.await;
read_errors += error_cnt;
// determine which last_seen_uid to use to update to
@@ -686,17 +730,66 @@ impl Imap {
Ok(read_cnt > 0)
}
/// Gets the from, to and bcc addresses from all existing outgoing emails.
pub async fn get_all_recipients(&mut self, context: &Context) -> Result<Vec<SingleInfo>> {
if self.session.is_none() {
bail!("IMAP No Connection established");
}
let session = self.session.as_mut().unwrap();
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await
.ok_or_else(|| format_err!("Not configured"))?;
let search_command = format!("FROM \"{}\"", self_addr);
let uids = session.uid_search(search_command).await?;
let uid_strings: Vec<String> = uids.into_iter().map(|s| s.to_string()).collect();
let mut result = Vec::new();
// We fetch the emails in chunks of 100 because according to https://tools.ietf.org/html/rfc2683#section-3.2.1.5
// command lines should not be much more than 1000 chars and UIDs can get up to 9- or 10-digit
// (servers should allow at least 8000 chars)
for uid_chunk in uid_strings.chunks(100) {
let uid_set = uid_chunk.join(",");
let mut list = session
.uid_fetch(uid_set, "(UID BODY.PEEK[HEADER.FIELDS (FROM TO CC BCC)])")
.await
.map_err(|err| {
format_err!("IMAP Could not fetch (get_all_recipients()): {}", err)
})?;
while let Some(fetch) = list.next().await {
let msg = fetch?;
match get_fetch_headers(&msg) {
Ok(headers) => {
let (from_id, _, _) =
from_field_to_contact_id(context, &mimeparser::get_from(&headers))
.await?;
if from_id == DC_CONTACT_ID_SELF {
result.extend(mimeparser::get_recipients(&headers));
}
}
Err(err) => {
warn!(context, "{}", err);
continue;
}
};
}
}
Ok(result)
}
/// Fetch all uids larger than the passed in. Returns a sorted list of fetch results.
async fn fetch_after(
&mut self,
context: &Context,
uid: u32,
) -> Result<BTreeMap<u32, async_imap::types::Fetch>> {
if self.session.is_none() {
return Err(Error::NoConnection);
}
let session = self.session.as_mut().unwrap();
let session = self.session.as_mut();
let session = session.context("fetch_after(): IMAP No Connection established")?;
// fetch messages with larger UID than the last one seen
// `(UID FETCH lastseenuid+1:*)`, see RFC 4549
@@ -704,11 +797,11 @@ impl Imap {
let mut list = session
.uid_fetch(set, PREFETCH_FLAGS)
.await
.map_err(Error::FetchFailed)?;
.map_err(|err| format_err!("IMAP Could not fetch: {}", err))?;
let mut msgs = BTreeMap::new();
while let Some(fetch) = list.next().await {
let msg = fetch.map_err(|err| Error::Other(err.to_string()))?;
let msg = fetch?;
if let Some(msg_uid) = msg.uid {
msgs.insert(msg_uid, msg);
}
@@ -734,6 +827,40 @@ impl Imap {
Ok(new_msgs)
}
/// Like fetch_after(), but not for new messages but existing ones (the DC_FETCH_EXISTING_MSGS_COUNT newest messages)
async fn fetch_existing_msgs_prefetch(
&mut self,
) -> Result<BTreeMap<u32, async_imap::types::Fetch>> {
let exists: i64 = {
let mailbox = self.config.selected_mailbox.as_ref();
let mailbox = mailbox.context("fetch_existing_msgs_prefetch(): no mailbox selected")?;
mailbox.exists.into()
};
let session = self.session.as_mut();
let session =
session.context("fetch_existing_msgs_prefetch(): IMAP No Connection established")?;
// Fetch last DC_FETCH_EXISTING_MSGS_COUNT (100) messages.
// Sequence numbers are sequential. If there are 1000 messages in the inbox,
// we can fetch the sequence numbers 900-1000 and get the last 100 messages.
let first = cmp::max(1, exists - DC_FETCH_EXISTING_MSGS_COUNT);
let set = format!("{}:*", first);
let mut list = session
.fetch(&set, PREFETCH_FLAGS)
.await
.map_err(|err| format_err!("IMAP Could not fetch: {}", err))?;
let mut msgs = BTreeMap::new();
while let Some(fetch) = list.next().await {
let msg = fetch?;
if let Some(msg_uid) = msg.uid {
msgs.insert(msg_uid, msg);
}
}
Ok(msgs)
}
async fn set_config_last_seen_uid<S: AsRef<str>>(
&self,
context: &Context,
@@ -760,6 +887,7 @@ impl Imap {
context: &Context,
folder: S,
server_uids: &[u32],
fetching_existing_messages: bool,
) -> (Option<u32>, usize) {
let set = match server_uids {
[] => return (None, 0),
@@ -833,7 +961,16 @@ impl Imap {
let body = msg.body().unwrap();
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
match dc_receive_imf(&context, &body, &folder, server_uid, is_seen).await {
match dc_receive_imf_inner(
&context,
&body,
&folder,
server_uid,
is_seen,
fetching_existing_messages,
)
.await
{
Ok(_) => last_uid = Some(server_uid),
Err(err) => {
warn!(context, "dc_receive_imf error: {}", err);
@@ -1170,14 +1307,14 @@ impl Imap {
pub async fn configure_folders(&mut self, context: &Context, create_mvbox: bool) -> Result<()> {
if !self.is_connected() {
return Err(Error::NoConnection);
bail!("IMAP No Connection established");
}
if let Some(ref mut session) = &mut self.session {
let mut folders = match session.list(Some(""), Some("*")).await {
Ok(f) => f,
Err(err) => {
return Err(Error::Other(format!("list_folders failed {:?}", err)));
bail!("list_folders failed: {}", err);
}
};
@@ -1188,7 +1325,7 @@ impl Imap {
let mut fallback_folder = get_fallback_folder(&delimiter);
while let Some(folder) = folders.next().await {
let folder = folder.map_err(|err| Error::Other(err.to_string()))?;
let folder = folder?;
info!(context, "Scanning folder: {:?}", folder);
// Update the delimiter iff there is a different one, but only once.
@@ -1211,7 +1348,9 @@ impl Imap {
} else if let FolderMeaning::SentObjects = get_folder_meaning(&folder) {
// Always takes precedent
sentbox_folder = Some(folder.name().to_string());
} else if let FolderMeaning::SentObjects = get_folder_meaning_by_name(&folder) {
} else if let FolderMeaning::SentObjects =
get_folder_meaning_by_name(&folder.name())
{
// only set iff none has been already set
if sentbox_folder.is_none() {
sentbox_folder = Some(folder.name().to_string());
@@ -1282,60 +1421,6 @@ impl Imap {
info!(context, "FINISHED configuring IMAP-folders.");
Ok(())
}
pub async fn empty_folder(&mut self, context: &Context, folder: &str) {
info!(context, "emptying folder {}", folder);
// we want to report all error to the user
// (no retry should be attempted)
if folder.is_empty() {
error!(context, "cannot perform empty, folder not set");
return;
}
if let Err(err) = self.setup_handle_if_needed(context).await {
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
);
return;
}
if !self
.add_flag_finalized_with_set(context, SELECT_ALL, "\\Deleted")
.await
{
error!(context, "Cannot mark messages for deletion {}", folder);
return;
}
// we now trigger expunge to actually delete messages
self.config.selected_folder_needs_expunge = true;
match self.select_folder::<String>(context, None).await {
Ok(()) => {
emit_event!(context, EventType::ImapFolderEmptied(folder.to_string()));
}
Err(err) => {
error!(context, "expunge failed {}: {:?}", folder, err);
}
}
if let Err(err) = context
.sql
.execute(
"UPDATE msgs SET server_folder='',server_uid=0 WHERE server_folder=?",
paramsv![folder],
)
.await
{
warn!(
context,
"Failed to reset server_uid and server_folder for deleted messages: {}", err
);
}
}
}
/// Try to get the folder meaning by the name of the folder only used if the server does not support XLIST.
@@ -1344,11 +1429,43 @@ impl Imap {
// only watching this folder is not working. at least, this is no show stopper.
// CAVE: if possible, take care not to add a name here that is "sent" in one language
// but sth. different in others - a hard job.
fn get_folder_meaning_by_name(folder_name: &Name) -> FolderMeaning {
let sent_names = vec!["sent", "sentmail", "sent objects", "gesendet"];
let lower = folder_name.name().to_lowercase();
fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
// source: https://stackoverflow.com/questions/2185391/localized-gmail-imap-folders
let sent_names = vec![
"sent",
"sentmail",
"sent objects",
"gesendet",
"Sent Mail",
"Sendte e-mails",
"Enviados",
"Messages envoyés",
"Messages envoyes",
"Posta inviata",
"Verzonden berichten",
"Wyslane",
"E-mails enviados",
"Correio enviado",
"Enviada",
"Enviado",
"Gönderildi",
"Inviati",
"Odeslaná pošta",
"Sendt",
"Skickat",
"Verzonden",
"Wysłane",
"Éléments envoyés",
"Απεσταλμένα",
"Отправленные",
"寄件備份",
"已发送邮件",
"送信済み",
"보낸편지함",
];
let lower = folder_name.to_lowercase();
if sent_names.into_iter().any(|s| s == lower) {
if sent_names.into_iter().any(|s| s.to_lowercase() == lower) {
FolderMeaning::SentObjects
} else {
FolderMeaning::Unknown
@@ -1442,17 +1559,16 @@ async fn precheck_imf(
if old_server_folder != server_folder || old_server_uid != server_uid {
update_server_uid(context, rfc724_mid, server_folder, server_uid).await;
if let Ok(MessageState::InSeen) = msg_id.get_state(context).await {
job::add(
context,
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
)
.await;
};
context
.interrupt_inbox(InterruptInfo::new(false, Some(msg_id)))
.await;
info!(context, "Updating server_uid and interrupting")
if let Ok(message_state) = msg_id.get_state(context).await {
if message_state == MessageState::InSeen || message_state.is_outgoing() {
job::add(
context,
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
)
.await;
}
}
info!(context, "Updating server_uid and adding markseen job");
}
Ok(true)
} else {
@@ -1473,7 +1589,7 @@ 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()))
bail!("prefetch: No message ID found");
}
}
@@ -1501,6 +1617,18 @@ pub(crate) async fn prefetch_should_download(
headers: &[mailparse::MailHeader<'_>],
show_emails: ShowEmails,
) -> Result<bool> {
if let Some(rfc724_mid) = headers.get_header_value(HeaderDef::MessageId) {
if let Some(group_id) = dc_extract_grpid_from_rfc724_mid(&rfc724_mid) {
if let Ok((chat_id, _, _)) = get_chat_id_by_grpid(context, group_id).await {
if !chat_id.is_unset() {
// This might be a group command, like removing a group member.
// We really need to fetch this to avoid inconsistent group state.
return Ok(true);
}
}
}
}
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).await;
@@ -1582,3 +1710,32 @@ async fn message_needs_processing(
fn get_fallback_folder(delimiter: &str) -> String {
format!("INBOX{}DeltaChat", delimiter)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_folder_meaning_by_name() {
assert_eq!(
get_folder_meaning_by_name("Gesendet"),
FolderMeaning::SentObjects
);
assert_eq!(
get_folder_meaning_by_name("GESENDET"),
FolderMeaning::SentObjects
);
assert_eq!(
get_folder_meaning_by_name("gesendet"),
FolderMeaning::SentObjects
);
assert_eq!(
get_folder_meaning_by_name("Messages envoyés"),
FolderMeaning::SentObjects
);
assert_eq!(
get_folder_meaning_by_name("mEsSaGes envoyÉs"),
FolderMeaning::SentObjects
);
assert_eq!(get_folder_meaning_by_name("xxx"), FolderMeaning::Unknown);
}
}

View File

@@ -6,6 +6,7 @@ use std::{
ffi::OsStr,
};
use anyhow::Context as _;
use async_std::path::{Path, PathBuf};
use async_std::{
fs::{self, File},
@@ -82,18 +83,49 @@ pub async fn imex(
what: ImexMode,
param1: Option<impl AsRef<Path>>,
) -> Result<()> {
use futures::future::FutureExt;
let cancel = context.alloc_ongoing().await?;
let res = imex_inner(context, what, param1)
.race(cancel.recv().map(|_| Err(format_err!("canceled"))))
.await;
let res = async {
let success = imex_inner(context, what, param1).await;
match success {
Ok(()) => {
info!(context, "IMEX successfully completed");
context.emit_event(EventType::ImexProgress(1000));
Ok(())
}
Err(err) => {
cleanup_aborted_imex(context, what).await;
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
error!(context, "{:#}", err);
context.emit_event(EventType::ImexProgress(0));
bail!("IMEX FAILED to complete: {}", err);
}
}
}
.race(async {
cancel.recv().await.ok();
cleanup_aborted_imex(context, what).await;
Err(format_err!("canceled"))
})
.await;
context.free_ongoing().await;
res
}
async fn cleanup_aborted_imex(context: &Context, what: ImexMode) {
if what == ImexMode::ImportBackup {
dc_delete_file(context, context.get_dbfile()).await;
dc_delete_files_in_dir(context, context.get_blobdir()).await;
}
if what == ImexMode::ExportBackup || what == ImexMode::ImportBackup {
if let Err(e) = context.sql.open(context, context.get_dbfile(), false).await {
warn!(context, "Re-opening db after imex failed: {}", e);
}
}
}
/// Returns the filename of the backup found (otherwise an error)
pub async fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result<String> {
let dir_name = dir_name.as_ref();
@@ -138,17 +170,23 @@ pub async fn has_backup_old(context: &Context, dir_name: impl AsRef<Path>) -> Re
let name = name.to_string_lossy();
if name.starts_with("delta-chat") && name.ends_with(".bak") {
let sql = Sql::new();
if sql.open(context, &path, true).await {
let curr_backup_time = sql
.get_raw_config_int(context, "backup_time")
.await
.unwrap_or_default();
if curr_backup_time > newest_backup_time {
newest_backup_path = Some(path);
newest_backup_time = curr_backup_time;
match sql.open(context, &path, true).await {
Ok(_) => {
let curr_backup_time = sql
.get_raw_config_int(context, "backup_time")
.await
.unwrap_or_default();
if curr_backup_time > newest_backup_time {
newest_backup_path = Some(path);
newest_backup_time = curr_backup_time;
}
info!(context, "backup_time of {} is {}", name, curr_backup_time);
sql.close().await;
}
info!(context, "backup_time of {} is {}", name, curr_backup_time);
sql.close().await;
Err(e) => warn!(
context,
"Found backup file {} which could not be opened: {}", name, e
),
}
}
}
@@ -192,10 +230,8 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
msg.param
.set(Param::MimeType, "application/autocrypt-setup");
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
msg.param.set_int(
Param::ForcePlaintext,
ForcePlaintext::NoAutocryptHeader as i32,
);
msg.param.set_int(Param::ForcePlaintext, 1);
msg.param.set_int(Param::SkipAutocrypt, 1);
let msg_id = chat::send_msg(context, chat_id, &mut msg).await?;
info!(context, "Wait for setup message being sent ...",);
@@ -425,7 +461,7 @@ async fn imex_inner(
}
}
let success = match what {
match what {
ImexMode::ExportSelfKeys => export_self_keys(context, path).await,
ImexMode::ImportSelfKeys => import_self_keys(context, path).await,
@@ -434,18 +470,6 @@ async fn imex_inner(
ImexMode::ExportBackup => export_backup_old(context, path).await,
// import_backup() will call import_backup_old() if this is an old backup.
ImexMode::ImportBackup => import_backup(context, path).await,
};
match success {
Ok(()) => {
info!(context, "IMEX successfully completed");
context.emit_event(EventType::ImexProgress(1000));
Ok(())
}
Err(err) => {
context.emit_event(EventType::ImexProgress(0));
bail!("IMEX FAILED to complete: {}", err);
}
}
}
@@ -479,10 +503,20 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
);
let backup_file = File::open(backup_to_import).await?;
let file_size = backup_file.metadata().await?.len();
let archive = Archive::new(backup_file);
let mut entries = archive.entries()?;
while let Some(file) = entries.next().await {
let f = &mut file?;
let current_pos = f.raw_file_position();
let progress = 1000 * current_pos / file_size;
if progress > 10 && progress < 1000 {
// We already emitted ImexProgress(10) above
context.emit_event(EventType::ImexProgress(progress as usize));
}
if f.path()?.file_name() == Some(OsStr::new(DBFILE_BACKUP_NAME)) {
// async_tar can't unpack to a specified file name, so we just unpack to the blobdir and then move the unpacked file.
f.unpack_in(context.get_blobdir()).await?;
@@ -491,7 +525,6 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
context.get_dbfile(),
)
.await?;
context.emit_event(EventType::ImexProgress(400)); // Just guess the progress, we at least have the dbfile by now
} else {
// async_tar will unpack to blobdir/BLOBS_BACKUP_NAME, so we move the file afterwards.
f.unpack_in(context.get_blobdir()).await?;
@@ -506,13 +539,11 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
}
}
ensure!(
context
.sql
.open(&context, &context.get_dbfile(), false)
.await,
"could not re-open db"
);
context
.sql
.open(&context, &context.get_dbfile(), false)
.await
.context("Could not re-open db")?;
delete_and_reset_all_device_msgs(&context).await?;
@@ -544,13 +575,11 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
);
/* error already logged */
/* re-open copied database file */
ensure!(
context
.sql
.open(&context, &context.get_dbfile(), false)
.await,
"could not re-open db"
);
context
.sql
.open(&context, &context.get_dbfile(), false)
.await
.context("Could not re-open db")?;
delete_and_reset_all_device_msgs(&context).await?;
@@ -632,6 +661,7 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
let now = time();
let (temp_path, dest_path) = get_next_backup_path_new(dir, now).await?;
let _d = DeleteOnDrop(temp_path.clone());
context
.sql
@@ -670,13 +700,19 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
}
Err(e) => {
error!(context, "backup failed: {}", e);
// Not using dc_delete_file() here because it would send a DeletedBlobFile event
fs::remove_file(temp_path).await?;
}
}
res
}
struct DeleteOnDrop(PathBuf);
impl Drop for DeleteOnDrop {
fn drop(&mut self) {
let file = self.0.clone();
// Not using dc_delete_file() here because it would send a DeletedBlobFile event
async_std::task::block_on(async move { fs::remove_file(file).await.ok() });
}
}
async fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> Result<()> {
let file = File::create(temp_path).await?;
@@ -688,11 +724,32 @@ async fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> Result<(
.append_path_with_name(context.get_dbfile(), DBFILE_BACKUP_NAME)
.await?;
context.emit_event(EventType::ImexProgress(500));
let read_dir: Vec<_> = fs::read_dir(context.get_blobdir()).await?.collect().await;
let count = read_dir.len();
let mut written_files = 0;
builder
.append_dir_all(BLOBS_BACKUP_NAME, context.get_blobdir())
.await?;
for entry in read_dir.into_iter() {
let entry = entry?;
let name = entry.file_name();
if !entry.file_type().await?.is_file() {
warn!(
context,
"Export: Found dir entry {} that is not a file, ignoring",
name.to_string_lossy()
);
continue;
}
let mut file = File::open(entry.path()).await?;
let path_in_archive = PathBuf::from(BLOBS_BACKUP_NAME).join(name);
builder.append_file(path_in_archive, &mut file).await?;
written_files += 1;
let progress = 1000 * written_files / count;
if progress > 10 && progress < 1000 {
// We already emitted ImexProgress(10) above
emit_event!(context, EventType::ImexProgress(progress));
}
}
builder.finish().await?;
Ok(())
@@ -722,7 +779,7 @@ async fn export_backup_old(context: &Context, dir: impl AsRef<Path>) -> Result<(
context
.sql
.open(&context, &context.get_dbfile(), false)
.await;
.await?;
if !copied {
bail!(
@@ -732,11 +789,11 @@ async fn export_backup_old(context: &Context, dir: impl AsRef<Path>) -> Result<(
);
}
let dest_sql = Sql::new();
ensure!(
dest_sql.open(context, &dest_path_filename, false).await,
"could not open exported database {}",
dest_path_string
);
dest_sql
.open(context, &dest_path_filename, false)
.await
.with_context(|| format!("could not open exported database {}", dest_path_string))?;
let res = match add_files_to_export(context, &dest_sql).await {
Err(err) => {
dc_delete_file(context, &dest_path_filename).await;
@@ -1027,6 +1084,21 @@ mod tests {
assert_eq!(bytes, key.to_asc(None).into_bytes());
}
#[async_std::test]
async fn test_export_and_import_key() {
let context = TestContext::new().await;
context.configure_alice().await;
let blobdir = "$BLOBDIR";
assert!(imex(&context.ctx, ImexMode::ExportSelfKeys, Some(blobdir))
.await
.is_ok());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
assert!(imex(&context.ctx, ImexMode::ImportSelfKeys, Some(blobdir))
.await
.is_ok());
}
#[test]
fn test_normalize_setup_code() {
let norm = normalize_setup_code("123422343234423452346234723482349234");

View File

@@ -14,10 +14,8 @@ use async_smtp::smtp::response::Category;
use async_smtp::smtp::response::Code;
use async_smtp::smtp::response::Detail;
use crate::blob::BlobObject;
use crate::chat::{self, ChatId};
use crate::config::Config;
use crate::constants::*;
use crate::contact::Contact;
use crate::context::Context;
use crate::dc_tools::*;
@@ -26,12 +24,12 @@ use crate::error::{bail, ensure, format_err, Error, Result};
use crate::events::EventType;
use crate::imap::*;
use crate::location;
use crate::login_param::LoginParam;
use crate::message::MsgId;
use crate::message::{self, Message, MessageState};
use crate::mimefactory::MimeFactory;
use crate::param::*;
use crate::smtp::Smtp;
use crate::{blob::BlobObject, contact::normalize_name, contact::Modifier, contact::Origin};
use crate::{scheduler::InterruptInfo, sql};
// results in ~3 weeks for the last backoff timespan
@@ -94,7 +92,7 @@ pub enum Action {
// Jobs in the INBOX-thread, range from DC_IMAP_THREAD..DC_IMAP_THREAD+999
Housekeeping = 105, // low priority ...
EmptyServer = 107,
FetchExistingMsgs = 110,
MarkseenMsgOnImap = 130,
// Moving message is prioritized lower than deletion so we don't
@@ -102,6 +100,10 @@ pub enum Action {
MoveMsg = 200,
DeleteMsgOnImap = 210,
// UID synchronization is high-priority to make sure correct UIDs
// are used by message moving/deletion.
ResyncFolders = 300,
// Jobs in the SMTP-thread, range from DC_SMTP_THREAD..DC_SMTP_THREAD+999
MaybeSendLocations = 5005, // low priority ...
MaybeSendLocationsEnded = 5007,
@@ -123,8 +125,9 @@ impl From<Action> for Thread {
Unknown => Thread::Unknown,
Housekeeping => Thread::Imap,
FetchExistingMsgs => Thread::Imap,
DeleteMsgOnImap => Thread::Imap,
EmptyServer => Thread::Imap,
ResyncFolders => Thread::Imap,
MarkseenMsgOnImap => Thread::Imap,
MoveMsg => Thread::Imap,
@@ -338,12 +341,9 @@ impl Job {
pub(crate) async fn send_msg_to_smtp(&mut self, context: &Context, smtp: &mut Smtp) -> Status {
// SMTP server, if not yet done
if !smtp.is_connected().await {
let loginparam = LoginParam::from_database(context, "configured_").await;
if let Err(err) = smtp.connect(context, &loginparam).await {
warn!(context, "SMTP connection failure: {:?}", err);
return Status::RetryLater;
}
if let Err(err) = smtp.connect_configured(context).await {
warn!(context, "SMTP connection failure: {:?}", err);
return Status::RetryLater;
}
let filename = job_try!(job_try!(self
@@ -486,12 +486,9 @@ impl Job {
let recipients = vec![recipient];
// connect to SMTP server, if not yet done
if !smtp.is_connected().await {
let loginparam = LoginParam::from_database(context, "configured_").await;
if let Err(err) = smtp.connect(context, &loginparam).await {
warn!(context, "SMTP connection failure: {:?}", err);
return Status::RetryLater;
}
if let Err(err) = smtp.connect_configured(context).await {
warn!(context, "SMTP connection failure: {:?}", err);
return Status::RetryLater;
}
self.smtp_send(context, recipients, body, self.job_id, smtp, || {
@@ -565,6 +562,11 @@ impl Job {
context,
"The message is deleted from the server when all parts are deleted.",
);
} else if cnt == 0 {
warn!(
context,
"The message {} has no UID on the server to delete", &msg.rfc724_mid
);
} else {
/* if this is the last existing part of the message,
we delete the message from the server */
@@ -619,21 +621,74 @@ impl Job {
}
}
async fn empty_server(&mut self, context: &Context, imap: &mut Imap) -> Status {
/// Read the recipients from old emails sent by the user and add them as contacts.
/// This way, we can already offer them some email addresses they can write to.
///
/// Then, Fetch the last messages DC_FETCH_EXISTING_MSGS_COUNT emails from the server
/// and show them in the chat list.
async fn fetch_existing_msgs(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
if self.foreign_id & DC_EMPTY_MVBOX > 0 {
if let Some(mvbox_folder) = &context.get_config(Config::ConfiguredMvboxFolder).await {
imap.empty_folder(context, &mvbox_folder).await;
add_all_recipients_as_contacts(context, imap, Config::ConfiguredSentboxFolder).await;
add_all_recipients_as_contacts(context, imap, Config::ConfiguredMvboxFolder).await;
add_all_recipients_as_contacts(context, imap, Config::ConfiguredInboxFolder).await;
if context.get_config_bool(Config::FetchExisting).await {
for config in &[
Config::ConfiguredMvboxFolder,
Config::ConfiguredInboxFolder,
Config::ConfiguredSentboxFolder,
] {
if let Some(folder) = context.get_config(*config).await {
if let Err(e) = imap.fetch_new_messages(context, folder, true).await {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
warn!(context, "Could not fetch messages, retrying: {:#}", e);
return Status::RetryLater;
};
}
}
}
if self.foreign_id & DC_EMPTY_INBOX > 0 {
if let Some(inbox_folder) = &context.get_config(Config::ConfiguredInboxFolder).await {
imap.empty_folder(context, &inbox_folder).await;
}
info!(context, "Done fetching existing messages.");
Status::Finished(Ok(()))
}
/// Synchronizes UIDs for sentbox, inbox and mvbox, in this order.
///
/// If a copy of the message is present in multiple folders, mvbox
/// is preferred to inbox, which is in turn preferred to
/// sentbox. This is because in the database it is impossible to
/// store multiple UIDs for one message, so we prefer to
/// automatically delete messages in the folders managed by Delta
/// Chat in contrast to the Sent folder, which is normally managed
/// by the user via webmail or another email client.
async fn resync_folders(&mut self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
if let Some(sentbox_folder) = &context.get_config(Config::ConfiguredSentboxFolder).await {
job_try!(
imap.resync_folder_uids(context, sentbox_folder.to_string())
.await
);
}
if let Some(inbox_folder) = &context.get_config(Config::ConfiguredInboxFolder).await {
job_try!(
imap.resync_folder_uids(context, inbox_folder.to_string())
.await
);
}
if let Some(mvbox_folder) = &context.get_config(Config::ConfiguredMvboxFolder).await {
job_try!(
imap.resync_folder_uids(context, mvbox_folder.to_string())
.await
);
}
Status::Finished(Ok(()))
}
@@ -740,6 +795,50 @@ async fn set_delivered(context: &Context, msg_id: MsgId) {
context.emit_event(EventType::MsgDelivered { chat_id, msg_id });
}
async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, folder: Config) {
let mailbox = if let Some(m) = context.get_config(folder).await {
m
} else {
return;
};
if let Err(e) = imap.select_with_uidvalidity(context, &mailbox).await {
warn!(context, "Could not select {}: {}", mailbox, e);
return;
}
match imap.get_all_recipients(context).await {
Ok(contacts) => {
let mut any_modified = false;
for contact in contacts {
let display_name_normalized = contact
.display_name
.as_ref()
.map(normalize_name)
.unwrap_or_default();
match Contact::add_or_lookup(
context,
display_name_normalized,
contact.addr,
Origin::OutgoingTo,
)
.await
{
Ok((_, modified)) => {
if modified != Modifier::None {
any_modified = true;
}
}
Err(e) => warn!(context, "Could not add recipient: {}", e),
}
}
if any_modified {
context.emit_event(EventType::ContactsChanged(None));
}
}
Err(e) => warn!(context, "Could not add recipients: {}", e),
};
}
/// Constructs a job for sending a message.
///
/// Returns `None` if no messages need to be sent out.
@@ -984,10 +1083,11 @@ async fn perform_job_action(
Action::MaybeSendLocationsEnded => {
location::job_maybe_send_locations_ended(context, job).await
}
Action::EmptyServer => job.empty_server(context, connection.inbox()).await,
Action::DeleteMsgOnImap => job.delete_msg_on_imap(context, connection.inbox()).await,
Action::ResyncFolders => job.resync_folders(context, connection.inbox()).await,
Action::MarkseenMsgOnImap => job.markseen_msg_on_imap(context, connection.inbox()).await,
Action::MoveMsg => job.move_msg(context, connection.inbox()).await,
Action::FetchExistingMsgs => job.fetch_existing_msgs(context, connection.inbox()).await,
Action::Housekeeping => {
sql::housekeeping(context).await;
Status::Finished(Ok(()))
@@ -1019,6 +1119,15 @@ async fn send_mdn(context: &Context, msg: &Message) -> Result<()> {
Ok(())
}
pub(crate) async fn schedule_resync(context: &Context) {
kill_action(context, Action::ResyncFolders).await;
add(
context,
Job::new(Action::ResyncFolders, 0, Params::new(), 0),
)
.await;
}
/// Creates a job.
pub fn create(action: Action, foreign_id: i32, param: Params, delay_seconds: i64) -> Result<Job> {
ensure!(
@@ -1041,9 +1150,10 @@ pub async fn add(context: &Context, job: Job) {
match action {
Action::Unknown => unreachable!(),
Action::Housekeeping
| Action::EmptyServer
| Action::DeleteMsgOnImap
| Action::ResyncFolders
| Action::MarkseenMsgOnImap
| Action::FetchExistingMsgs
| Action::MoveMsg => {
info!(context, "interrupt: imap");
context

View File

@@ -222,7 +222,7 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
let addr = context
.get_config(Config::ConfiguredAddr)
.await
.ok_or_else(|| Error::NoConfiguredAddr)?;
.ok_or(Error::NoConfiguredAddr)?;
let addr = EmailAddress::new(&addr)?;
let _guard = context.generating_key_mutex.lock().await;
@@ -355,7 +355,7 @@ pub async fn store_self_keypair(
}
/// A key fingerprint
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct Fingerprint(Vec<u8>);
impl Fingerprint {
@@ -375,6 +375,14 @@ impl Fingerprint {
}
}
impl fmt::Debug for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Fingerprint")
.field("hex", &self.hex())
.finish()
}
}
/// Make a human-readable fingerprint.
impl fmt::Display for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -423,11 +431,9 @@ mod tests {
use std::error::Error;
use async_std::sync::Arc;
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
lazy_static! {
static ref KEYPAIR: KeyPair = alice_keypair();
}
static KEYPAIR: Lazy<KeyPair> = Lazy::new(alice_keypair);
#[test]
fn test_from_armored_string() {
@@ -529,11 +535,11 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
#[test]
fn test_from_slice_bad_data() {
let mut bad_data: [u8; 4096] = [0; 4096];
for i in 0..4096 {
bad_data[i] = (i & 0xff) as u8;
for (i, v) in bad_data.iter_mut().enumerate() {
*v = (i & 0xff) as u8;
}
for j in 0..(4096 / 40) {
let slice = &bad_data[j..j + 4096 / 2 + j];
let slice = &bad_data.get(j..j + 4096 / 2 + j).unwrap();
assert!(SignedPublicKey::from_slice(slice).is_err());
assert!(SignedSecretKey::from_slice(slice).is_err());
}
@@ -593,7 +599,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
let ctx0 = ctx.clone();
let thr0 =
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx0)));
let ctx1 = ctx.clone();
let ctx1 = ctx;
let thr1 =
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx1)));
let res0 = thr0.join().unwrap();

View File

@@ -54,6 +54,7 @@ pub mod imex;
mod scheduler;
#[macro_use]
pub mod job;
mod format_flowed;
pub mod key;
mod keyring;
pub mod location;

View File

@@ -469,7 +469,7 @@ pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String
<Document>\n\
<Placemark>\
<Timestamp><when>{}</when></Timestamp>\
<Point><coordinates>{:.2},{:.2}</coordinates></Point>\
<Point><coordinates>{},{}</coordinates></Point>\
</Placemark>\n\
</Document>\n\
</kml>",
@@ -723,6 +723,8 @@ pub(crate) async fn job_maybe_send_locations_ended(
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::test_utils::TestContext;
@@ -757,4 +759,22 @@ mod tests {
assert!(locations_ref[1].accuracy < 2.6f64);
assert_eq!(locations_ref[1].timestamp, 1544739072);
}
#[async_std::test]
async fn test_get_message_kml() {
let context = TestContext::new().await;
let timestamp = 1598490000;
let xml = get_message_kml(timestamp, 51.423723f64, 8.552556f64);
let kml = Kml::parse(&context.ctx, xml.as_bytes()).expect("parsing failed");
let locations_ref = &kml.locations;
assert_eq!(locations_ref.len(), 1);
assert!(locations_ref[0].latitude >= 51.423723f64);
assert!(locations_ref[0].latitude < 51.423724f64);
assert!(locations_ref[0].longitude >= 8.552556f64);
assert!(locations_ref[0].longitude < 8.552557f64);
assert!(locations_ref[0].accuracy.abs() < f64::EPSILON);
assert_eq!(locations_ref[0].timestamp, timestamp);
}
}

View File

@@ -3,13 +3,16 @@
use std::borrow::Cow;
use std::fmt;
use crate::context::Context;
use crate::{context::Context, provider::Socket};
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
#[repr(i32)]
#[strum(serialize_all = "snake_case")]
pub enum CertificateChecks {
/// Same as AcceptInvalidCertificates unless overridden by
/// `strict_tls` setting in provider database.
Automatic = 0,
Strict = 1,
/// Same as AcceptInvalidCertificates
@@ -25,30 +28,29 @@ impl Default for CertificateChecks {
}
}
/// Login parameters for a single server, either IMAP or SMTP
#[derive(Default, Debug, Clone)]
pub struct ServerLoginParam {
pub server: String,
pub user: String,
pub password: String,
pub port: u16,
pub security: Socket,
/// TLS options: whether to allow invalid certificates and/or
/// invalid hostnames
pub certificate_checks: CertificateChecks,
}
#[derive(Default, Debug, Clone)]
pub struct LoginParam {
pub addr: String,
pub mail_server: String,
pub mail_user: String,
pub mail_pw: String,
pub mail_port: i32,
/// IMAP TLS options: whether to allow invalid certificates and/or invalid hostnames
pub imap_certificate_checks: CertificateChecks,
pub send_server: String,
pub send_user: String,
pub send_pw: String,
pub send_port: i32,
/// SMTP TLS options: whether to allow invalid certificates and/or invalid hostnames
pub smtp_certificate_checks: CertificateChecks,
pub imap: ServerLoginParam,
pub smtp: ServerLoginParam,
pub server_flags: i32,
}
impl LoginParam {
/// Create a new `LoginParam` with default values.
pub fn new() -> Self {
Default::default()
}
/// Read the login parameters from the database.
pub async fn from_database(context: &Context, prefix: impl AsRef<str>) -> Self {
let prefix = prefix.as_ref();
@@ -77,6 +79,13 @@ impl LoginParam {
let key = format!("{}mail_pw", prefix);
let mail_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
let key = format!("{}mail_security", prefix);
let mail_security = sql
.get_raw_config_int(context, key)
.await
.and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default();
let key = format!("{}imap_certificate_checks", prefix);
let imap_certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
@@ -100,6 +109,13 @@ impl LoginParam {
let key = format!("{}send_pw", prefix);
let send_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
let key = format!("{}send_security", prefix);
let send_security = sql
.get_raw_config_int(context, key)
.await
.and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default();
let key = format!("{}smtp_certificate_checks", prefix);
let smtp_certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
@@ -116,16 +132,22 @@ impl LoginParam {
LoginParam {
addr,
mail_server,
mail_user,
mail_pw,
mail_port,
imap_certificate_checks,
send_server,
send_user,
send_pw,
send_port,
smtp_certificate_checks,
imap: ServerLoginParam {
server: mail_server,
user: mail_user,
password: mail_pw,
port: mail_port as u16,
security: mail_security,
certificate_checks: imap_certificate_checks,
},
smtp: ServerLoginParam {
server: send_server,
user: send_user,
password: send_pw,
port: send_port as u16,
security: send_security,
certificate_checks: smtp_certificate_checks,
},
server_flags,
}
}
@@ -143,41 +165,51 @@ impl LoginParam {
sql.set_raw_config(context, key, Some(&self.addr)).await?;
let key = format!("{}mail_server", prefix);
sql.set_raw_config(context, key, Some(&self.mail_server))
sql.set_raw_config(context, key, Some(&self.imap.server))
.await?;
let key = format!("{}mail_port", prefix);
sql.set_raw_config_int(context, key, self.mail_port).await?;
sql.set_raw_config_int(context, key, self.imap.port as i32)
.await?;
let key = format!("{}mail_user", prefix);
sql.set_raw_config(context, key, Some(&self.mail_user))
sql.set_raw_config(context, key, Some(&self.imap.user))
.await?;
let key = format!("{}mail_pw", prefix);
sql.set_raw_config(context, key, Some(&self.mail_pw))
sql.set_raw_config(context, key, Some(&self.imap.password))
.await?;
let key = format!("{}mail_security", prefix);
sql.set_raw_config_int(context, key, self.imap.security as i32)
.await?;
let key = format!("{}imap_certificate_checks", prefix);
sql.set_raw_config_int(context, key, self.imap_certificate_checks as i32)
sql.set_raw_config_int(context, key, self.imap.certificate_checks as i32)
.await?;
let key = format!("{}send_server", prefix);
sql.set_raw_config(context, key, Some(&self.send_server))
sql.set_raw_config(context, key, Some(&self.smtp.server))
.await?;
let key = format!("{}send_port", prefix);
sql.set_raw_config_int(context, key, self.send_port).await?;
sql.set_raw_config_int(context, key, self.smtp.port as i32)
.await?;
let key = format!("{}send_user", prefix);
sql.set_raw_config(context, key, Some(&self.send_user))
sql.set_raw_config(context, key, Some(&self.smtp.user))
.await?;
let key = format!("{}send_pw", prefix);
sql.set_raw_config(context, key, Some(&self.send_pw))
sql.set_raw_config(context, key, Some(&self.smtp.password))
.await?;
let key = format!("{}send_security", prefix);
sql.set_raw_config_int(context, key, self.smtp.security as i32)
.await?;
let key = format!("{}smtp_certificate_checks", prefix);
sql.set_raw_config_int(context, key, self.smtp_certificate_checks as i32)
sql.set_raw_config_int(context, key, self.smtp.certificate_checks as i32)
.await?;
let key = format!("{}server_flags", prefix);
@@ -199,16 +231,24 @@ impl fmt::Display for LoginParam {
f,
"{} imap:{}:{}:{}:{}:cert_{} smtp:{}:{}:{}:{}:cert_{} {}",
unset_empty(&self.addr),
unset_empty(&self.mail_user),
if !self.mail_pw.is_empty() { pw } else { unset },
unset_empty(&self.mail_server),
self.mail_port,
self.imap_certificate_checks,
unset_empty(&self.send_user),
if !self.send_pw.is_empty() { pw } else { unset },
unset_empty(&self.send_server),
self.send_port,
self.smtp_certificate_checks,
unset_empty(&self.imap.user),
if !self.imap.password.is_empty() {
pw
} else {
unset
},
unset_empty(&self.imap.server),
self.imap.port,
self.imap.certificate_checks,
unset_empty(&self.smtp.user),
if !self.smtp.password.is_empty() {
pw
} else {
unset
},
unset_empty(&self.smtp.server),
self.smtp.port,
self.smtp.certificate_checks,
flags_readable,
)
}
@@ -237,30 +277,6 @@ fn get_readable_flags(flags: i32) -> String {
res += "AUTH_NORMAL ";
flag_added = true;
}
if 1 << bit == 0x100 {
res += "IMAP_STARTTLS ";
flag_added = true;
}
if 1 << bit == 0x200 {
res += "IMAP_SSL ";
flag_added = true;
}
if 1 << bit == 0x400 {
res += "IMAP_PLAIN ";
flag_added = true;
}
if 1 << bit == 0x10000 {
res += "SMTP_STARTTLS ";
flag_added = true;
}
if 1 << bit == 0x20000 {
res += "SMTP_SSL ";
flag_added = true;
}
if 1 << bit == 0x40000 {
res += "SMTP_PLAIN ";
flag_added = true;
}
if flag_added {
res += &format!("{:#0x}", 1 << bit);
}

View File

@@ -2,7 +2,7 @@
use async_std::path::{Path, PathBuf};
use deltachat_derive::{FromSql, ToSql};
use lazy_static::lazy_static;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use crate::chat::{self, Chat, ChatId};
@@ -19,10 +19,7 @@ use crate::mimeparser::{FailureReport, SystemMessage};
use crate::param::*;
use crate::pgp::*;
use crate::stock::StockMessage;
lazy_static! {
static ref UNWRAP_RE: regex::Regex = regex::Regex::new(r"\s+").unwrap();
}
use std::collections::BTreeMap;
// In practice, the user additionally cuts the string themselves
// pixel-accurate.
@@ -266,10 +263,9 @@ pub struct Message {
pub(crate) server_folder: Option<String>,
pub(crate) server_uid: u32,
pub(crate) is_dc_message: MessengerMessage,
pub(crate) starred: bool,
pub(crate) chat_blocked: Blocked,
pub(crate) location_id: u32,
pub(crate) error: String,
error: Option<String>,
pub(crate) param: Params,
}
@@ -310,7 +306,6 @@ impl Message {
" m.msgrmsg AS msgrmsg,",
" m.txt AS txt,",
" m.param AS param,",
" m.starred AS starred,",
" m.hidden AS hidden,",
" m.location_id AS location,",
" c.blocked AS blocked",
@@ -336,7 +331,8 @@ impl Message {
msg.ephemeral_timestamp = row.get("ephemeral_timestamp")?;
msg.viewtype = row.get("type")?;
msg.state = row.get("state")?;
msg.error = row.get("error")?;
let error: String = row.get("error")?;
msg.error = Some(error).filter(|error| !error.is_empty());
msg.is_dc_message = row.get("msgrmsg")?;
let text;
@@ -360,7 +356,6 @@ impl Message {
msg.text = Some(text);
msg.param = row.get::<_, String>("param")?.parse().unwrap_or_default();
msg.starred = row.get("starred")?;
msg.hidden = row.get("hidden")?;
msg.location_id = row.get("location")?;
msg.chat_blocked = row
@@ -494,7 +489,7 @@ impl Message {
pub fn get_text(&self) -> Option<String> {
self.text
.as_ref()
.map(|text| dc_truncate(text, 30000).to_string())
.map(|text| dc_truncate(text, DC_MAX_GET_TEXT_LEN).to_string())
}
pub fn get_filename(&self) -> Option<String> {
@@ -549,9 +544,7 @@ impl Message {
return ret;
};
let contact = if self.from_id != DC_CONTACT_ID_SELF as u32
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
{
let contact = if self.from_id != DC_CONTACT_ID_SELF as u32 && chat.typ == Chattype::Group {
Contact::get_by_id(context, self.from_id).await.ok()
} else {
None
@@ -585,10 +578,6 @@ impl Message {
self.state as i32 >= MessageState::OutDelivered as i32
}
pub fn is_starred(&self) -> bool {
self.starred
}
pub fn is_forwarded(&self) -> bool {
0 != self.param.get_int(Param::Forwarded).unwrap_or_default()
}
@@ -600,6 +589,10 @@ impl Message {
|| cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage
}
pub fn get_info_type(&self) -> SystemMessage {
self.param.get_cmd()
}
pub fn is_system_message(&self) -> bool {
let cmd = self.param.get_cmd();
cmd != SystemMessage::Unknown
@@ -750,6 +743,61 @@ impl Message {
self.update_param(context).await;
}
/// Sets message quote.
///
/// Message-Id is used to set Reply-To field, message text is used for quote.
///
/// Encryption is required if quoted message was encrypted.
///
/// The message itself is not required to exist in the database,
/// it may even be deleted from the database by the time the message is prepared.
pub async fn set_quote(&mut self, context: &Context, quote: &Message) -> Result<(), Error> {
ensure!(
!quote.rfc724_mid.is_empty(),
"Message without Message-Id cannot be quoted"
);
self.in_reply_to = Some(quote.rfc724_mid.clone());
if quote
.param
.get_bool(Param::GuaranteeE2ee)
.unwrap_or_default()
{
self.param.set(Param::GuaranteeE2ee, "1");
}
let text = quote.get_text().unwrap_or_default();
self.param.set(
Param::Quote,
if text.is_empty() {
// Use summary, similar to "Image" to avoid sending empty quote.
quote.get_summarytext(context, 500).await
} else {
text
},
);
Ok(())
}
pub fn quoted_text(&self) -> Option<String> {
self.param.get(Param::Quote).map(|s| s.to_string())
}
pub async fn quoted_message(&self, context: &Context) -> Result<Option<Message>, Error> {
if self.param.get(Param::Quote).is_some() {
if let Some(in_reply_to) = &self.in_reply_to {
let rfc724_mid = in_reply_to.trim_start_matches('<').trim_end_matches('>');
if !rfc724_mid.is_empty() {
if let Some((_, _, msg_id)) = rfc724_mid_exists(context, rfc724_mid).await? {
return Ok(Some(Message::load_from_db(context, msg_id).await?));
}
}
}
}
Ok(None)
}
pub async fn update_param(&mut self, context: &Context) -> bool {
context
.sql
@@ -760,6 +808,22 @@ impl Message {
.await
.is_ok()
}
/// Gets the error status of the message.
///
/// A message can have an associated error status if something went wrong when sending or
/// receiving message itself. The error status is free-form text and should not be further parsed,
/// rather it's presence is meant to indicate *something* went wrong with the message and the
/// text of the error is detailed information on what.
///
/// Some common reasons error can be associated with messages are:
/// * Lack of valid signature on an e2ee message, usually for received messages.
/// * Failure to decrypt an e2ee message, usually for received messages.
/// * When a message could not be delivered to one or more recipients the non-delivery
/// notification text can be stored in the error status.
pub fn error(&self) -> Option<String> {
self.error.clone()
}
}
#[derive(
@@ -866,13 +930,18 @@ impl From<MessageState> for LotState {
impl MessageState {
pub fn can_fail(self) -> bool {
match self {
MessageState::OutPreparing
| MessageState::OutPending
| MessageState::OutDelivered
| MessageState::OutMdnRcvd => true, // OutMdnRcvd can still fail because it could be a group message and only some recipients failed.
_ => false,
}
use MessageState::*;
matches!(
self,
OutPreparing | OutPending | OutDelivered | OutMdnRcvd // OutMdnRcvd can still fail because it could be a group message and only some recipients failed.
)
}
pub fn is_outgoing(self) -> bool {
use MessageState::*;
matches!(
self,
OutPreparing | OutDraft | OutPending | OutFailed | OutDelivered | OutMdnRcvd
)
}
}
@@ -909,7 +978,7 @@ impl Lot {
);
self.text1_meaning = Meaning::Text1Self;
}
} else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
} else if chat.typ == Chattype::Group {
if msg.is_info() || contact.is_none() {
self.text1 = None;
self.text1_meaning = Meaning::None;
@@ -929,16 +998,23 @@ impl Lot {
}
}
self.text2 = Some(
get_summarytext_by_raw(
msg.viewtype,
msg.text.as_ref(),
&msg.param,
SUMMARY_CHARACTERS,
context,
)
.await,
);
let mut text2 = get_summarytext_by_raw(
msg.viewtype,
msg.text.as_ref(),
&msg.param,
SUMMARY_CHARACTERS,
context,
)
.await;
if text2.is_empty() && msg.quoted_text().is_some() {
text2 = context
.stock_str(StockMessage::ReplyNoun)
.await
.into_owned()
}
self.text2 = Some(text2);
self.timestamp = msg.get_timestamp();
self.state = msg.state.into();
@@ -969,7 +1045,7 @@ pub async 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);
let rawtxt = dc_truncate(rawtxt.trim(), DC_MAX_GET_INFO_LEN);
let fts = dc_timestamp_to_str(msg.get_timestamp());
ret += &format!("Sent: {}", fts);
@@ -1054,8 +1130,8 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
ret += "\n";
if !msg.error.is_empty() {
ret += &format!("Error: {}", msg.error);
if let Some(error) = msg.error.as_ref() {
ret += &format!("Error: {}", error);
}
if let Some(path) = msg.get_file(context) {
@@ -1096,30 +1172,71 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
let extension: &str = &path.extension()?.to_str()?.to_lowercase();
let info = match extension {
// before using viewtype other than Viewtype::File,
// make sure, all target UIs support that type in the context of the used viewer/player.
// if in doubt, it is better to default to Viewtype::File that passes handing to an external app.
// (cmp. https://developer.android.com/guide/topics/media/media-formats )
"3gp" => (Viewtype::Video, "video/3gpp"),
"aac" => (Viewtype::Audio, "audio/aac"),
"avi" => (Viewtype::Video, "video/x-msvideo"),
"doc" => (Viewtype::File, "application/msword"),
"docx" => (
Viewtype::File,
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
),
"epub" => (Viewtype::File, "application/epub+zip"),
"flac" => (Viewtype::Audio, "audio/flac"),
"gif" => (Viewtype::Gif, "image/gif"),
"html" => (Viewtype::File, "text/html"),
"htm" => (Viewtype::File, "text/html"),
"ico" => (Viewtype::File, "image/vnd.microsoft.icon"),
"jar" => (Viewtype::File, "application/java-archive"),
"jpeg" => (Viewtype::Image, "image/jpeg"),
"jpe" => (Viewtype::Image, "image/jpeg"),
"jpg" => (Viewtype::Image, "image/jpeg"),
"json" => (Viewtype::File, "application/json"),
"mov" => (Viewtype::Video, "video/quicktime"),
"m4a" => (Viewtype::Audio, "audio/m4a"),
"mp3" => (Viewtype::Audio, "audio/mpeg"),
"mp4" => (Viewtype::Video, "video/mp4"),
"odp" => (
Viewtype::File,
"application/vnd.oasis.opendocument.presentation",
),
"ods" => (
Viewtype::File,
"application/vnd.oasis.opendocument.spreadsheet",
),
"odt" => (Viewtype::File, "application/vnd.oasis.opendocument.text"),
"oga" => (Viewtype::Audio, "audio/ogg"),
"ogg" => (Viewtype::Audio, "audio/ogg"),
"ogv" => (Viewtype::Video, "video/ogg"),
"opus" => (Viewtype::Audio, "audio/ogg"),
"ogv" => (Viewtype::File, "video/ogg"),
"opus" => (Viewtype::File, "audio/ogg"), // not supported eg. on Android 4
"otf" => (Viewtype::File, "font/otf"),
"pdf" => (Viewtype::File, "application/pdf"),
"png" => (Viewtype::Image, "image/png"),
"spx" => (Viewtype::Audio, "audio/ogg"), // Ogg Speex Profile
"svg" => (Viewtype::Image, "image/svg+xml"),
"rar" => (Viewtype::File, "application/vnd.rar"),
"rtf" => (Viewtype::File, "application/rtf"),
"spx" => (Viewtype::File, "audio/ogg"), // Ogg Speex Profile
"svg" => (Viewtype::File, "image/svg+xml"),
"tgs" => (Viewtype::Sticker, "application/x-tgsticker"),
"tiff" => (Viewtype::File, "image/tiff"),
"tif" => (Viewtype::File, "image/tiff"),
"ttf" => (Viewtype::File, "font/ttf"),
"vcard" => (Viewtype::File, "text/vcard"),
"vcf" => (Viewtype::File, "text/vcard"),
"wav" => (Viewtype::File, "audio/wav"),
"weba" => (Viewtype::File, "audio/webm"),
"webm" => (Viewtype::Video, "video/webm"),
"webp" => (Viewtype::Image, "image/webp"),
"webp" => (Viewtype::Image, "image/webp"), // iOS via SDWebImage, Android since 4.0
"wmv" => (Viewtype::Video, "video/x-ms-wmv"),
"xhtml" => (Viewtype::File, "application/xhtml+xml"),
"xlsx" => (
Viewtype::File,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
),
"xml" => (Viewtype::File, "application/vnd.ms-excel"),
"zip" => (Viewtype::File, "application/zip"),
_ => {
return None;
}
@@ -1190,6 +1307,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> bool {
.with_conn(move |conn| {
let mut stmt = conn.prepare_cached(concat!(
"SELECT",
" m.chat_id AS chat_id,",
" m.state AS state,",
" c.blocked AS blocked",
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
@@ -1200,6 +1318,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> bool {
for id in msg_ids.into_iter() {
let query_res = stmt.query_row(paramsv![id], |row| {
Ok((
row.get::<_, ChatId>("chat_id")?,
row.get::<_, MessageState>("state")?,
row.get::<_, Option<Blocked>>("blocked")?
.unwrap_or_default(),
@@ -1208,8 +1327,8 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> bool {
if let Err(rusqlite::Error::QueryReturnedNoRows) = query_res {
continue;
}
let (state, blocked) = query_res.map_err(Into::<anyhow::Error>::into)?;
msgs.push((id, state, blocked));
let (chat_id, state, blocked) = query_res.map_err(Into::<anyhow::Error>::into)?;
msgs.push((id, chat_id, state, blocked));
}
Ok(msgs)
@@ -1217,9 +1336,9 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> bool {
.await
.unwrap_or_default();
let mut send_event = false;
let mut updated_chat_ids = BTreeMap::new();
for (id, curr_state, curr_blocked) in msgs.into_iter() {
for (id, curr_chat_id, curr_state, curr_blocked) in msgs.into_iter() {
if let Err(err) = id.start_ephemeral_timer(context).await {
error!(
context,
@@ -1238,19 +1357,16 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> bool {
job::Job::new(Action::MarkseenMsgOnImap, id.to_u32(), Params::new(), 0),
)
.await;
send_event = true;
updated_chat_ids.insert(curr_chat_id, true);
}
} else if curr_state == MessageState::InFresh {
update_msg_state(context, id, MessageState::InNoticed).await;
send_event = true;
updated_chat_ids.insert(ChatId::new(DC_CHAT_ID_DEADDROP), true);
}
}
if send_event {
context.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
for updated_chat_id in updated_chat_ids.keys() {
context.emit_event(EventType::MsgsNoticed(*updated_chat_id));
}
true
@@ -1267,23 +1383,6 @@ pub async fn update_msg_state(context: &Context, msg_id: MsgId, state: MessageSt
.is_ok()
}
pub async fn star_msgs(context: &Context, msg_ids: Vec<MsgId>, star: bool) -> bool {
if msg_ids.is_empty() {
return false;
}
context
.sql
.with_conn(move |conn| {
let mut stmt = conn.prepare("UPDATE msgs SET starred=? WHERE id=?;")?;
for msg_id in msg_ids.into_iter() {
stmt.execute(paramsv![star as i32, msg_id])?;
}
Ok(())
})
.await
.is_ok()
}
/// Returns a summary text.
pub async fn get_summarytext_by_raw(
viewtype: Viewtype,
@@ -1362,7 +1461,7 @@ pub async fn get_summarytext_by_raw(
prefix
};
UNWRAP_RE.replace_all(&summary, " ").to_string()
summary.split_whitespace().join(" ")
}
// as we do not cut inside words, this results in about 32-42 characters.
@@ -1572,7 +1671,7 @@ pub(crate) async fn handle_ndn(
if let Ok((msg_id, chat_id, chat_type)) = res {
set_msg_failed(context, msg_id, error).await;
if chat_type == Chattype::Group || chat_type == Chattype::VerifiedGroup {
if chat_type == Chattype::Group {
if let Some(failed_recipient) = &failed.failed_recipient {
let contact_id =
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown).await;
@@ -1757,16 +1856,6 @@ pub async fn update_server_uid(
}
}
#[allow(dead_code)]
pub async fn dc_empty_server(context: &Context, flags: u32) {
job::kill_action(context, Action::EmptyServer).await;
job::add(
context,
job::Job::new(Action::EmptyServer, flags, Params::new(), 0),
)
.await;
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1807,12 +1896,29 @@ mod tests {
assert_eq!(_msg2.get_filemime(), None);
}
/// Tests that message cannot be prepared if account has no configured address.
#[async_std::test]
async fn test_prepare_not_configured() {
let d = test::TestContext::new().await;
let ctx = &d.ctx;
let contact = Contact::create(ctx, "", "dest@example.com")
.await
.expect("failed to create contact");
let chat = chat::create_by_contact_id(ctx, contact).await.unwrap();
let mut msg = Message::new(Viewtype::Text);
assert!(chat::prepare_msg(ctx, chat, &mut msg).await.is_err());
}
#[async_std::test]
async fn test_get_summarytext_by_raw() {
let d = test::TestContext::new().await;
let ctx = &d.ctx;
let some_text = Some("bla bla".to_string());
let some_text = Some(" bla \t\n\tbla\n\t".to_string());
let empty_text = Some("".to_string());
let no_text: Option<String> = None;
@@ -1853,8 +1959,7 @@ mod tests {
);
assert_eq!(
get_summarytext_by_raw(Viewtype::Voice, no_text.as_ref(), &mut some_file, 50, &ctx)
.await,
get_summarytext_by_raw(Viewtype::Voice, no_text.as_ref(), &some_file, 50, &ctx).await,
"Voice message" // file names are not added for voice messages
);
@@ -1864,8 +1969,7 @@ mod tests {
);
assert_eq!(
get_summarytext_by_raw(Viewtype::Audio, no_text.as_ref(), &mut some_file, 50, &ctx)
.await,
get_summarytext_by_raw(Viewtype::Audio, no_text.as_ref(), &some_file, 50, &ctx).await,
"Audio \u{2013} foo.bar" // file name is added for audio
);
@@ -1881,8 +1985,7 @@ mod tests {
);
assert_eq!(
get_summarytext_by_raw(Viewtype::File, some_text.as_ref(), &mut some_file, 50, &ctx)
.await,
get_summarytext_by_raw(Viewtype::File, some_text.as_ref(), &some_file, 50, &ctx).await,
"File \u{2013} foo.bar \u{2013} bla bla" // file name is added for files
);
@@ -1890,7 +1993,7 @@ mod tests {
asm_file.set(Param::File, "foo.bar");
asm_file.set_cmd(SystemMessage::AutocryptSetupMessage);
assert_eq!(
get_summarytext_by_raw(Viewtype::File, no_text.as_ref(), &mut asm_file, 50, &ctx).await,
get_summarytext_by_raw(Viewtype::File, no_text.as_ref(), &asm_file, 50, &ctx).await,
"Autocrypt Setup Message" // file name is not added for autocrypt setup messages
);
}
@@ -1967,7 +2070,7 @@ mod tests {
let chatitems = chat::get_chat_msgs(&t.ctx, device_chat_id, 0, None).await;
for chatitem in chatitems {
if let ChatItem::Message { msg_id } = chatitem {
if let Ok(msg) = Message::load_from_db(&t.ctx, msg_id.clone()).await {
if let Ok(msg) = Message::load_from_db(&t.ctx, msg_id).await {
if msg.get_viewtype() == Viewtype::Image {
has_image = true;
// just check that width/height are inside some reasonable ranges
@@ -1981,4 +2084,43 @@ mod tests {
}
assert!(has_image);
}
#[async_std::test]
async fn test_quote() {
use crate::config::Config;
let d = test::TestContext::new().await;
let ctx = &d.ctx;
let contact = Contact::create(ctx, "", "dest@example.com")
.await
.expect("failed to create contact");
let res = ctx
.set_config(Config::ConfiguredAddr, Some("self@example.com"))
.await;
assert!(res.is_ok());
let chat = chat::create_by_contact_id(ctx, contact).await.unwrap();
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("Quoted message".to_string()));
// Prepare message for sending, so it gets a Message-Id.
assert!(msg.rfc724_mid.is_empty());
let msg_id = chat::prepare_msg(ctx, chat, &mut msg).await.unwrap();
let msg = Message::load_from_db(ctx, msg_id).await.unwrap();
assert!(!msg.rfc724_mid.is_empty());
let mut msg2 = Message::new(Viewtype::Text);
msg2.set_quote(ctx, &msg).await.expect("can't set quote");
assert!(msg2.quoted_text() == msg.get_text());
let quoted_msg = msg2
.quoted_message(ctx)
.await
.expect("error while retrieving quoted message")
.expect("quoted message not found");
assert!(quoted_msg.get_text() == msg2.quoted_text());
}
}

View File

@@ -11,6 +11,7 @@ use crate::dc_tools::*;
use crate::e2ee::*;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::error::{bail, ensure, format_err, Error};
use crate::format_flowed::{format_flowed, format_flowed_quote};
use crate::location;
use crate::message::{self, Message};
use crate::mimeparser::SystemMessage;
@@ -144,7 +145,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
selfstatus: context
.get_config(Config::Selfstatus)
.await
.unwrap_or_else(|| default_str),
.unwrap_or(default_str),
recipients,
timestamp: msg.timestamp_sort,
loaded: Loaded::Message { chat },
@@ -182,7 +183,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let selfstatus = context
.get_config(Config::Selfstatus)
.await
.unwrap_or_else(|| default_str);
.unwrap_or(default_str);
let timestamp = dc_create_smeared_timestamp(context).await;
let res = MimeFactory::<'a, 'b> {
@@ -232,26 +233,20 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
fn is_e2ee_guaranteed(&self) -> bool {
match &self.loaded {
Loaded::Message { chat } => {
if chat.typ == Chattype::VerifiedGroup {
if chat.is_protected() {
return true;
}
let force_plaintext = self
!self
.msg
.param
.get_int(Param::ForcePlaintext)
.unwrap_or_default();
if force_plaintext == 0 {
return self
.get_bool(Param::ForcePlaintext)
.unwrap_or_default()
&& self
.msg
.param
.get_int(Param::GuaranteeE2ee)
.get_bool(Param::GuaranteeE2ee)
.unwrap_or_default()
!= 0;
}
false
}
Loaded::MDN { .. } => false,
}
@@ -260,7 +255,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
fn min_verified(&self) -> PeerstateVerifiedStatus {
match &self.loaded {
Loaded::Message { chat } => {
if chat.typ == Chattype::VerifiedGroup {
if chat.is_protected() {
PeerstateVerifiedStatus::BidirectVerified
} else {
PeerstateVerifiedStatus::Unverified
@@ -270,19 +265,30 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
}
fn should_force_plaintext(&self) -> i32 {
fn should_force_plaintext(&self) -> bool {
match &self.loaded {
Loaded::Message { chat } => {
if chat.typ == Chattype::VerifiedGroup {
0
if chat.is_protected() {
false
} else {
self.msg
.param
.get_int(Param::ForcePlaintext)
.get_bool(Param::ForcePlaintext)
.unwrap_or_default()
}
}
Loaded::MDN { .. } => ForcePlaintext::NoAutocryptHeader as i32,
Loaded::MDN { .. } => true,
}
}
fn should_skip_autocrypt(&self) -> bool {
match &self.loaded {
Loaded::Message { .. } => self
.msg
.param
.get_bool(Param::SkipAutocrypt)
.unwrap_or_default(),
Loaded::MDN { .. } => true,
}
}
@@ -339,7 +345,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
.stock_str(StockMessage::AcSetupMsgSubject)
.await
.into_owned()
} else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
} else if chat.typ == Chattype::Group {
let re = if self.in_reply_to.is_empty() {
""
} else {
@@ -475,6 +481,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let min_verified = self.min_verified();
let grpimage = self.grpimage();
let force_plaintext = self.should_force_plaintext();
let skip_autocrypt = self.should_skip_autocrypt();
let subject_str = self.subject_str().await;
let e2ee_guaranteed = self.is_e2ee_guaranteed();
let encrypt_helper = EncryptHelper::new(self.context).await?;
@@ -498,7 +505,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
Loaded::MDN { .. } => self.render_mdn().await?,
};
if force_plaintext != ForcePlaintext::NoAutocryptHeader as i32 {
if !skip_autocrypt {
// unless determined otherwise we add the Autocrypt header
let aheader = encrypt_helper.get_aheader().to_string();
unprotected_headers.push(Header::new("Autocrypt".into(), aheader));
@@ -509,7 +516,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let peerstates = self.peerstates_for_recipients().await?;
let should_encrypt =
encrypt_helper.should_encrypt(self.context, e2ee_guaranteed, &peerstates)?;
let is_encrypted = should_encrypt && force_plaintext == 0;
let is_encrypted = should_encrypt && !force_plaintext;
let rfc724_mid = match self.loaded {
Loaded::Message { .. } => self.msg.rfc724_mid.clone(),
@@ -701,11 +708,11 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let mut placeholdertext = None;
let mut meta_part = None;
if chat.typ == Chattype::VerifiedGroup {
if chat.is_protected() {
protected_headers.push(Header::new("Chat-Verified".to_string(), "1".to_string()));
}
if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
if chat.typ == Chattype::Group {
protected_headers.push(Header::new("Chat-Group-ID".into(), chat.grpid.clone()));
let encoded = encode_words(&chat.name);
@@ -839,6 +846,18 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
};
}
}
SystemMessage::ChatProtectionEnabled => {
protected_headers.push(Header::new(
"Chat-Content".to_string(),
"protection-enabled".to_string(),
));
}
SystemMessage::ChatProtectionDisabled => {
protected_headers.push(Header::new(
"Chat-Content".to_string(),
"protection-disabled".to_string(),
));
}
_ => {}
}
@@ -910,11 +929,18 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
};
let quoted_text = self
.msg
.quoted_text()
.map(|quote| format_flowed_quote(&quote) + "\r\n\r\n");
let flowed_text = format_flowed(final_text);
let footer = &self.selfstatus;
let message_text = format!(
"{}{}{}{}{}",
"{}{}{}{}{}{}",
fwdhint.unwrap_or_default(),
escape_message_footer_marks(final_text),
quoted_text.unwrap_or_default(),
escape_message_footer_marks(&flowed_text),
if !final_text.is_empty() && !footer.is_empty() {
"\r\n\r\n"
} else {
@@ -926,7 +952,10 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
// Message is sent as text/plain, with charset = utf-8
let main_part = PartBuilder::new()
.content_type(&mime::TEXT_PLAIN_UTF_8)
.header((
"Content-Type".to_string(),
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
))
.body(message_text);
let mut parts = Vec::new();

View File

@@ -3,9 +3,9 @@ use std::future::Future;
use std::pin::Pin;
use deltachat_derive::{FromSql, ToSql};
use lazy_static::lazy_static;
use lettre_email::mime::{self, Mime};
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
use once_cell::sync::Lazy;
use crate::aheader::Aheader;
use crate::blob::BlobObject;
@@ -17,6 +17,7 @@ use crate::dehtml::dehtml;
use crate::e2ee;
use crate::error::{bail, Result};
use crate::events::EventType;
use crate::format_flowed::unformat_flowed;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::Fingerprint;
use crate::location;
@@ -85,6 +86,10 @@ pub enum SystemMessage {
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged = 10,
// Chat protection state changed
ChatProtectionEnabled = 11,
ChatProtectionDisabled = 12,
}
impl Default for SystemMessage {
@@ -122,6 +127,7 @@ impl MimeMessage {
// remove headers that are allowed _only_ in the encrypted part
headers.remove("secure-join-fingerprint");
headers.remove("chat-verified");
// Memory location for a possible decrypted message.
let mail_raw;
@@ -217,12 +223,13 @@ impl MimeMessage {
failure_report: None,
};
parser.parse_mime_recursive(context, &mail).await?;
parser.maybe_remove_bad_parts().await;
parser.heuristically_parse_ndn(context).await;
parser.parse_headers(context)?;
if warn_empty_signature && parser.signatures.is_empty() {
for part in parser.parts.iter_mut() {
part.error = "No valid signature".to_string();
part.error = Some("No valid signature".to_string());
}
}
@@ -252,6 +259,10 @@ impl MimeMessage {
self.is_system_message = SystemMessage::LocationStreamingEnabled;
} else if value == "ephemeral-timer-changed" {
self.is_system_message = SystemMessage::EphemeralTimerChanged;
} else if value == "protection-enabled" {
self.is_system_message = SystemMessage::ChatProtectionEnabled;
} else if value == "protection-disabled" {
self.is_system_message = SystemMessage::ChatProtectionDisabled;
}
}
Ok(())
@@ -588,7 +599,7 @@ impl MimeMessage {
part.typ = Viewtype::Text;
part.msg_raw = Some(txt.clone());
part.msg = txt;
part.error = "Decryption failed".to_string();
part.error = Some("Decryption failed".to_string());
self.parts.push(part);
@@ -604,7 +615,7 @@ impl MimeMessage {
skip the rest. (see
https://k9mail.github.io/2016/11/24/OpenPGP-Considerations-Part-I.html
for background information why we use encrypted+signed) */
if let Some(first) = mail.subparts.iter().next() {
if let Some(first) = mail.subparts.get(0) {
any_part_added = self.parse_mime_recursive(context, first).await?;
}
}
@@ -641,7 +652,7 @@ impl MimeMessage {
}
}
Some(_) => {
if let Some(first) = mail.subparts.iter().next() {
if let Some(first) = mail.subparts.get(0) {
any_part_added = self.parse_mime_recursive(context, first).await?;
}
}
@@ -703,23 +714,55 @@ impl MimeMessage {
}
};
let (simplified_txt, is_forwarded) = if decoded_data.is_empty() {
("".into(), false)
let mut dehtml_failed = false;
let (simplified_txt, is_forwarded, top_quote) = if decoded_data.is_empty() {
("".to_string(), false, None)
} else {
let is_html = mime_type == mime::TEXT_HTML;
let out = if is_html {
dehtml(&decoded_data)
dehtml(&decoded_data).unwrap_or_else(|| {
dehtml_failed = true;
decoded_data.clone()
})
} else {
decoded_data.clone()
};
simplify(out, self.has_chat_version())
};
if !simplified_txt.is_empty() {
let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
{
format.as_str().to_ascii_lowercase() == "flowed"
} else {
false
};
let (simplified_txt, simplified_quote) = if mime_type.type_() == mime::TEXT
&& mime_type.subtype() == mime::PLAIN
&& is_format_flowed
{
let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().to_ascii_lowercase() == "yes"
} else {
false
};
let unflowed_text = unformat_flowed(&simplified_txt, delsp);
let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
(unflowed_text, unflowed_quote)
} else {
(simplified_txt, top_quote)
};
if !simplified_txt.is_empty() || simplified_quote.is_some() {
let mut part = Part::default();
part.dehtlm_failed = dehtml_failed;
part.typ = Viewtype::Text;
part.mimetype = Some(mime_type);
part.msg = simplified_txt;
if let Some(quote) = simplified_quote {
part.param.set(Param::Quote, quote);
}
part.msg_raw = Some(decoded_data);
self.do_add_single_part(part);
}
@@ -821,6 +864,7 @@ impl MimeMessage {
}
pub fn repl_msg_by_error(&mut self, error_msg: impl AsRef<str>) {
self.is_system_message = SystemMessage::Unknown;
if let Some(part) = self.parts.first_mut() {
part.typ = Viewtype::Text;
part.msg = format!("[{}]", error_msg.as_ref());
@@ -955,11 +999,21 @@ impl MimeMessage {
Ok(None)
}
async fn maybe_remove_bad_parts(&mut self) {
let good_parts = self.parts.iter().filter(|p| !p.dehtlm_failed).count();
if good_parts == 0 {
// We have no good part but show at least one bad part in order to show anything at all
self.parts.truncate(1);
} else if good_parts < self.parts.len() {
self.parts.retain(|p| !p.dehtlm_failed);
}
}
/// Some providers like GMX and Yahoo do not send standard NDNs (Non Delivery notifications).
/// If you improve heuristics here you might also have to change prefetch_should_download() in imap/mod.rs.
/// Also you should add a test in dc_receive_imf.rs (there already are lots of test_parse_ndn_* tests).
#[allow(clippy::indexing_slicing)]
async fn heuristically_parse_ndn(&mut self, context: &Context) -> Option<()> {
async fn heuristically_parse_ndn(&mut self, context: &Context) {
let maybe_ndn = if let Some(from) = self.get(HeaderDef::From_) {
let from = from.to_ascii_lowercase();
from.contains("mailer-daemon") || from.contains("mail-daemon")
@@ -967,9 +1021,8 @@ impl MimeMessage {
false
};
if maybe_ndn && self.failure_report.is_none() {
lazy_static! {
static ref RE: regex::Regex = regex::Regex::new(r"Message-ID:(.*)").unwrap();
}
static RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"Message-ID:(.*)").unwrap());
for captures in self
.parts
.iter()
@@ -989,7 +1042,6 @@ impl MimeMessage {
}
}
}
None // Always return None, we just return anything so that we can use the '?' operator.
}
/// Handle reports
@@ -1022,6 +1074,28 @@ impl MimeMessage {
message::handle_ndn(context, failure_report, error).await
}
}
/// Returns timestamp of the parent message.
///
/// If there is no parent message or it is not found in the
/// database, returns None.
pub async fn get_parent_timestamp(&self, context: &Context) -> Result<Option<i64>> {
let parent_timestamp = if let Some(field) = self
.get(HeaderDef::InReplyTo)
.and_then(|msgid| parse_message_id(msgid).ok())
{
context
.sql
.query_get_value_result(
"SELECT timestamp FROM msgs WHERE rfc724_mid=?",
paramsv![field],
)
.await?
} else {
None
};
Ok(parent_timestamp)
}
}
async fn update_gossip_peerstates(
@@ -1109,11 +1183,21 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
}
fn is_known(key: &str) -> bool {
match key {
"return-path" | "date" | "from" | "sender" | "reply-to" | "to" | "cc" | "bcc"
| "message-id" | "in-reply-to" | "references" | "subject" => true,
_ => false,
}
matches!(
key,
"return-path"
| "date"
| "from"
| "sender"
| "reply-to"
| "to"
| "cc"
| "bcc"
| "message-id"
| "in-reply-to"
| "references"
| "subject"
)
}
#[derive(Debug, Default, Clone)]
@@ -1125,7 +1209,8 @@ pub struct Part {
pub bytes: usize,
pub param: Params,
org_filename: Option<String>,
pub error: String,
pub error: Option<String>,
dehtlm_failed: bool,
}
/// return mimetype and viewtype for a parsed mail
@@ -1227,9 +1312,9 @@ fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<Option<String
}
/// Returned addresses are normalized and lowercased.
fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
get_all_addresses_from_header(headers, |header_key| {
header_key == "to" || header_key == "cc"
header_key == "to" || header_key == "cc" || header_key == "bcc"
})
}
@@ -1274,6 +1359,8 @@ where
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::test_utils::*;
@@ -1391,6 +1478,39 @@ mod tests {
assert!(mimeparser.chat_disposition_notification_to.is_none());
}
#[async_std::test]
async fn test_get_parent_timestamp() {
let context = TestContext::new().await;
let raw = b"From: foo@example.org\n\
Content-Type: text/plain\n\
Chat-Version: 1.0\n\
In-Reply-To: <Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org>\n\
\n\
Some reply\n\
";
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!(
mimeparser.get_parent_timestamp(&context.ctx).await.unwrap(),
None
);
let timestamp = 1570435529;
context
.ctx
.sql
.execute(
"INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)",
paramsv!["Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org", timestamp],
)
.await
.expect("Failed to write to the database");
assert_eq!(
mimeparser.get_parent_timestamp(&context.ctx).await.unwrap(),
Some(timestamp)
);
}
#[async_std::test]
async fn test_mimeparser_with_context() {
let context = TestContext::new().await;
@@ -1772,6 +1892,55 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
assert_eq!(message.parts[0].msg, "Hello!");
}
#[async_std::test]
async fn test_hide_html_without_content() {
let t = TestContext::new().await;
let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC)
From: sender@example.com
To: receiver@example.com
Subject: Mail with inline attachment
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="----=_Part_25_46172632.1581201680436"
------=_Part_25_46172632.1581201680436
Content-Type: text/html; charset=utf-8
<head>
<meta http-equiv="Content-Type" content="text/html; charset=Windows-1252">
<meta name="GENERATOR" content="MSHTML 11.00.10570.1001"></head>
<body><img align="baseline" alt="" src="cid:1712254131-1" border="0" hspace="0">
</body>
------=_Part_25_46172632.1581201680436
Content-Type: application/pdf; name="some_pdf.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: inline; filename="some_pdf.pdf"
JVBERi0xLjUKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl
Y29kZT4+CnN0cmVhbQp4nGVOuwoCMRDs8xVbC8aZvC4Hx4Hno7ATAhZi56MTtPH33YtXiLKQ3ZnM
MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
------=_Part_25_46172632.1581201680436--
"#;
let message = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap();
assert_eq!(message.parts.len(), 1);
assert_eq!(message.parts[0].typ, Viewtype::File);
assert_eq!(message.parts[0].msg, "");
// Make sure the file is there even though the html is wrong:
let param = &message.parts[0].param;
let blob: BlobObject = param
.get_blob(Param::File, &t.ctx, false)
.await
.unwrap()
.unwrap();
let f = async_std::fs::File::open(blob.to_abs_path()).await.unwrap();
let size = f.metadata().await.unwrap().len();
assert_eq!(size, 154);
}
#[async_std::test]
async fn parse_inline_image() {
let context = TestContext::new().await;
@@ -2029,4 +2198,72 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
let test = parse_message_ids(" < ").unwrap();
assert!(test.is_empty());
}
#[async_std::test]
async fn parse_format_flowed_quote() {
let context = TestContext::new().await;
let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Subject: Re: swipe-to-reply
MIME-Version: 1.0
In-Reply-To: <bar@example.org>
Date: Tue, 06 Oct 2020 00:00:00 +0000
Chat-Version: 1.0
Message-ID: <foo@example.org>
To: bob <bob@example.org>
From: alice <alice@example.org>
> Long
> quote.
Reply
"##;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!(
message.get_subject(),
Some("Re: swipe-to-reply".to_string())
);
assert_eq!(message.parts.len(), 1);
assert_eq!(message.parts[0].typ, Viewtype::Text);
assert_eq!(
message.parts[0].param.get(Param::Quote).unwrap(),
"Long quote."
);
assert_eq!(message.parts[0].msg, "Reply");
}
#[async_std::test]
async fn parse_quote_without_reply() {
let context = TestContext::new().await;
let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Subject: Re: swipe-to-reply
MIME-Version: 1.0
In-Reply-To: <bar@example.org>
Date: Tue, 06 Oct 2020 00:00:00 +0000
Message-ID: <foo@example.org>
To: bob <bob@example.org>
From: alice <alice@example.org>
> Just a quote.
"##;
let message = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
.unwrap();
assert_eq!(
message.get_subject(),
Some("Re: swipe-to-reply".to_string())
);
assert_eq!(message.parts.len(), 1);
assert_eq!(message.parts[0].typ, Viewtype::Text);
assert_eq!(
message.parts[0].param.get(Param::Quote).unwrap(),
"Just a quote."
);
assert_eq!(message.parts[0].msg, "");
}
}

View File

@@ -170,16 +170,14 @@ pub async fn dc_get_oauth2_access_token(
}
// ... and POST
let response = surf::post(post_url).body_form(&post_param);
if response.is_err() {
warn!(
context,
"Error calling OAuth2 at {}: {:?}", token_url, response
);
let mut req = surf::post(post_url).build();
if let Err(err) = req.body_form(&post_param) {
warn!(context, "Error calling OAuth2 at {}: {:?}", token_url, err);
return None;
}
let parsed: Result<Response, _> = response.unwrap().recv_json().await;
let client = surf::Client::new();
let parsed: Result<Response, _> = client.recv_json(req).await;
if parsed.is_err() {
warn!(
context,
@@ -305,7 +303,7 @@ impl Oauth2 {
let mut fqdn: String = String::from(domain.as_ref());
if !fqdn.ends_with('.') {
fqdn.push_str(".");
fqdn.push('.');
}
if let Ok(res) = resolver.mx_lookup(fqdn).await {
@@ -323,7 +321,7 @@ impl Oauth2 {
}
async fn get_addr(&self, context: &Context, access_token: impl AsRef<str>) -> Option<String> {
let userinfo_url = self.get_userinfo.unwrap_or_else(|| "");
let userinfo_url = self.get_userinfo.unwrap_or("");
let userinfo_url = replace_in_uri(&userinfo_url, "$ACCESS_TOKEN", access_token);
// should returns sth. as

View File

@@ -3,12 +3,13 @@ use std::fmt;
use std::str;
use async_std::path::PathBuf;
use itertools::Itertools;
use num_traits::FromPrimitive;
use serde::{Deserialize, Serialize};
use crate::blob::{BlobError, BlobObject};
use crate::context::Context;
use crate::error::{self, bail, ensure};
use crate::error::{self, bail};
use crate::message::MsgId;
use crate::mimeparser::SystemMessage;
@@ -40,16 +41,21 @@ pub enum Param {
/// 'c' nor 'e' are preset, the messages is only transport encrypted.
ErroneousE2ee = b'e',
/// For Messages: force unencrypted message, either `ForcePlaintext::AddAutocryptHeader` (1),
/// `ForcePlaintext::NoAutocryptHeader` (2) or 0.
/// For Messages: force unencrypted message, a value from `ForcePlaintext` enum.
ForcePlaintext = b'u',
/// For Messages: do not include Autocrypt header.
SkipAutocrypt = b'o',
/// For Messages
WantsMdn = b'r',
/// For Messages
Forwarded = b'a',
/// For Messages: quoted text.
Quote = b'q',
/// For Messages
Cmd = b'S',
@@ -95,6 +101,12 @@ pub enum Param {
Recipients = b'R',
/// For Groups
///
/// An unpromoted group has not had any messages sent to it and thus only exists on the
/// creator's device. Any changes made to an unpromoted group do not need to send
/// system messages to the group members to update them of the changes. Once a message
/// has been sent to a group it is promoted and group changes require sending system
/// messages to all members.
Unpromoted = b'U',
/// For Groups and Contacts
@@ -122,14 +134,6 @@ pub enum Param {
MsgId = b'I',
}
/// Possible values for `Param::ForcePlaintext`.
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive)]
#[repr(u8)]
pub enum ForcePlaintext {
AddAutocryptHeader = 1,
NoAutocryptHeader = 2,
}
/// An object for handling key=value parameter lists.
///
/// The structure is serialized by calling `to_string()` on it.
@@ -146,7 +150,12 @@ impl fmt::Display for Params {
if i > 0 {
writeln!(f)?;
}
write!(f, "{}={}", *key as u8 as char, value)?;
write!(
f,
"{}={}",
*key as u8 as char,
value.split('\n').join("\n\n")
)?;
}
Ok(())
}
@@ -157,27 +166,28 @@ impl str::FromStr for Params {
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut inner = BTreeMap::new();
for pair in s.trim().lines() {
let pair = pair.trim();
if pair.is_empty() {
continue;
}
// TODO: probably nicer using a regex
ensure!(pair.len() > 1, "Invalid key pair: '{}'", pair);
let mut split = pair.splitn(2, '=');
let key = split.next();
let value = split.next();
let mut lines = s.lines().peekable();
ensure!(key.is_some(), "Missing key");
ensure!(value.is_some(), "Missing value");
while let Some(line) = lines.next() {
if let [key, value] = line.splitn(2, '=').collect::<Vec<_>>()[..] {
let key = key.to_string();
let mut value = value.to_string();
while let Some(s) = lines.peek() {
if !s.is_empty() {
break;
}
lines.next();
value.push('\n');
value += lines.next().unwrap_or_default();
}
let key = key.unwrap_or_default().trim();
let value = value.unwrap_or_default().trim();
if let Some(key) = key.as_bytes().first().and_then(|key| Param::from_u8(*key)) {
inner.insert(key, value.to_string());
if let Some(key) = key.as_bytes().first().and_then(|key| Param::from_u8(*key)) {
inner.insert(key, value);
} else {
bail!("Unknown key: {}", key);
}
} else {
bail!("Unknown key: {}", key);
bail!("Not a key-value pair: {:?}", line);
}
}
@@ -373,7 +383,7 @@ mod tests {
#[test]
fn test_dc_param() {
let mut p1: Params = "\r\n\r\na=1\nf=2\n\nc = 3 ".parse().unwrap();
let mut p1: Params = "a=1\nf=2\nc=3".parse().unwrap();
assert_eq!(p1.get_int(Param::Forwarded), Some(1));
assert_eq!(p1.get_int(Param::File), Some(2));
@@ -407,6 +417,14 @@ mod tests {
assert_eq!(p1.len(), 0)
}
#[test]
fn test_roundtrip() {
let mut params = Params::new();
params.set(Param::Height, "foo\nbar=baz\nquux");
params.set(Param::Width, "\n\n\na=\n=");
assert_eq!(params.to_string().parse::<Params>().unwrap(), params);
}
#[test]
fn test_regression() {
let p1: Params = "a=cli%40deltachat.de\nn=\ni=TbnwJ6lSvD5\ns=0ejvbdFSQxB"
@@ -469,8 +487,8 @@ mod tests {
);
// Blob in blobdir, expect blob.
let bar = t.ctx.get_blobdir().join("bar");
p.set(Param::File, bar.to_str().unwrap());
let bar_path = t.ctx.get_blobdir().join("bar");
p.set(Param::File, bar_path.to_str().unwrap());
let blob = p
.get_blob(Param::File, &t.ctx, false)
.await

View File

@@ -94,47 +94,42 @@ pub enum ToSave {
}
impl<'a> Peerstate<'a> {
pub fn new(context: &'a Context, addr: String) -> Self {
pub fn from_header(context: &'a Context, header: &Aheader, message_time: i64) -> Self {
Peerstate {
context,
addr,
last_seen: 0,
last_seen_autocrypt: 0,
prefer_encrypt: Default::default(),
public_key: None,
public_key_fingerprint: None,
addr: header.addr.clone(),
last_seen: message_time,
last_seen_autocrypt: message_time,
prefer_encrypt: header.prefer_encrypt,
public_key: Some(header.public_key.clone()),
public_key_fingerprint: Some(header.public_key.fingerprint()),
gossip_key: None,
gossip_key_fingerprint: None,
gossip_timestamp: 0,
verified_key: None,
verified_key_fingerprint: None,
to_save: None,
to_save: Some(ToSave::All),
fingerprint_changed: false,
}
}
pub fn from_header(context: &'a Context, header: &Aheader, message_time: i64) -> Self {
let mut res = Self::new(context, header.addr.clone());
res.last_seen = message_time;
res.last_seen_autocrypt = message_time;
res.to_save = Some(ToSave::All);
res.prefer_encrypt = header.prefer_encrypt;
res.public_key = Some(header.public_key.clone());
res.recalc_fingerprint();
res
}
pub fn from_gossip(context: &'a Context, gossip_header: &Aheader, message_time: i64) -> Self {
let mut res = Self::new(context, gossip_header.addr.clone());
res.gossip_timestamp = message_time;
res.to_save = Some(ToSave::All);
res.gossip_key = Some(gossip_header.public_key.clone());
res.recalc_fingerprint();
res
Peerstate {
context,
addr: gossip_header.addr.clone(),
last_seen: 0,
last_seen_autocrypt: 0,
prefer_encrypt: Default::default(),
public_key: None,
public_key_fingerprint: None,
gossip_key: Some(gossip_header.public_key.clone()),
gossip_key_fingerprint: Some(gossip_header.public_key.fingerprint()),
gossip_timestamp: message_time,
verified_key: None,
verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false,
}
}
pub async fn from_addr(context: &'a Context, addr: &str) -> Result<Option<Peerstate<'a>>> {
@@ -175,40 +170,44 @@ impl<'a> Peerstate<'a> {
public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
gossip_key_fingerprint, verified_key, verified_key_fingerprint
*/
let mut res = Self::new(context, row.get(0)?);
res.last_seen = row.get(1)?;
res.last_seen_autocrypt = row.get(2)?;
res.prefer_encrypt = EncryptPreference::from_i32(row.get(3)?).unwrap_or_default();
res.gossip_timestamp = row.get(5)?;
res.public_key_fingerprint = row
.get::<_, Option<String>>(7)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default();
res.gossip_key_fingerprint = row
.get::<_, Option<String>>(8)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default();
res.verified_key_fingerprint = row
.get::<_, Option<String>>(10)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default();
res.public_key = row
.get(4)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
res.gossip_key = row
.get(6)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
res.verified_key = row
.get(9)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
let res = Peerstate {
context,
addr: row.get(0)?,
last_seen: row.get(1)?,
last_seen_autocrypt: row.get(2)?,
prefer_encrypt: EncryptPreference::from_i32(row.get(3)?).unwrap_or_default(),
public_key: row
.get(4)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
public_key_fingerprint: row
.get::<_, Option<String>>(7)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
gossip_key: row
.get(6)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
gossip_key_fingerprint: row
.get::<_, Option<String>>(8)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
gossip_timestamp: row.get(5)?,
verified_key: row
.get(9)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
verified_key_fingerprint: row
.get::<_, Option<String>>(10)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
to_save: None,
fingerprint_changed: false,
};
Ok(res)
})
@@ -291,7 +290,7 @@ impl<'a> Peerstate<'a> {
return;
}
if message_time > self.last_seen_autocrypt {
if message_time > self.last_seen {
self.last_seen = message_time;
self.last_seen_autocrypt = message_time;
self.to_save = Some(ToSave::Timestamps);
@@ -490,7 +489,6 @@ mod tests {
use super::*;
use crate::test_utils::*;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[async_std::test]
async fn test_peerstate_save_to_db() {
@@ -637,10 +635,44 @@ mod tests {
assert_eq!(peerstate.verified_key_fingerprint, None);
}
// TODO: don't copy this from stress.rs
#[allow(dead_code)]
struct TestContext {
ctx: Context,
dir: TempDir,
#[async_std::test]
async fn test_peerstate_degrade_reordering() {
let context = crate::test_utils::TestContext::new().await.ctx;
let addr = "example@example.org";
let pub_key = alice_keypair().public;
let header = Aheader::new(addr.to_string(), pub_key, EncryptPreference::Mutual);
let mut peerstate = Peerstate {
context: &context,
addr: addr.to_string(),
last_seen: 0,
last_seen_autocrypt: 0,
prefer_encrypt: EncryptPreference::NoPreference,
public_key: None,
public_key_fingerprint: None,
gossip_key: None,
gossip_timestamp: 0,
gossip_key_fingerprint: None,
verified_key: None,
verified_key_fingerprint: None,
to_save: None,
fingerprint_changed: false,
};
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::NoPreference);
peerstate.apply_header(&header, 100);
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
peerstate.degrade_encryption(300);
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset);
// This has message time 200, while encryption was degraded at timestamp 300.
// Because of reordering, header should not be applied.
peerstate.apply_header(&header, 200);
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset);
// Same header will be applied in the future.
peerstate.apply_header(&header, 400);
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
}
}

View File

@@ -381,7 +381,7 @@ pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
mod tests {
use super::*;
use crate::test_utils::*;
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
#[test]
fn test_split_armored_data_1() {
@@ -439,9 +439,9 @@ mod tests {
let bob = bob_keypair();
TestKeys {
alice_secret: alice.secret.clone(),
alice_public: alice.public.clone(),
alice_public: alice.public,
bob_secret: bob.secret.clone(),
bob_public: bob.public.clone(),
bob_public: bob.public,
}
}
}
@@ -449,26 +449,29 @@ mod tests {
/// The original text of [CTEXT_SIGNED]
static CLEARTEXT: &[u8] = b"This is a test";
lazy_static! {
/// Initialised [TestKeys] for tests.
static ref KEYS: TestKeys = TestKeys::new();
/// Initialised [TestKeys] for tests.
static KEYS: Lazy<TestKeys> = Lazy::new(TestKeys::new);
/// A cyphertext encrypted to Alice & Bob, signed by Alice.
static ref CTEXT_SIGNED: String = {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
smol::block_on(pk_encrypt(CLEARTEXT, keyring, Some(KEYS.alice_secret.clone()))).unwrap()
};
/// A cyphertext encrypted to Alice & Bob, signed by Alice.
static CTEXT_SIGNED: Lazy<String> = Lazy::new(|| {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
futures_lite::future::block_on(pk_encrypt(
CLEARTEXT,
keyring,
Some(KEYS.alice_secret.clone()),
))
.unwrap()
});
/// A cyphertext encrypted to Alice & Bob, not signed.
static ref CTEXT_UNSIGNED: String = {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
smol::block_on(pk_encrypt(CLEARTEXT, keyring, None)).unwrap()
};
}
/// A cyphertext encrypted to Alice & Bob, not signed.
static CTEXT_UNSIGNED: Lazy<String> = Lazy::new(|| {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
futures_lite::future::block_on(pk_encrypt(CLEARTEXT, keyring, None)).unwrap()
});
#[test]
fn test_encrypt_signed() {

File diff suppressed because it is too large Load Diff

View File

@@ -4,9 +4,10 @@ mod data;
use crate::config::Config;
use crate::dc_tools::EmailAddress;
use crate::provider::data::PROVIDER_DATA;
use crate::provider::data::{PROVIDER_DATA, PROVIDER_UPDATED};
use chrono::{NaiveDateTime, NaiveTime};
#[derive(Debug, Copy, Clone, PartialEq, ToPrimitive)]
#[derive(Debug, Display, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Status {
OK = 1,
@@ -14,21 +15,29 @@ pub enum Status {
BROKEN = 3,
}
#[derive(Debug, PartialEq)]
#[derive(Debug, Display, PartialEq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Protocol {
SMTP = 1,
IMAP = 2,
}
#[derive(Debug, PartialEq)]
#[derive(Debug, Display, PartialEq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Socket {
STARTTLS = 1,
SSL = 2,
Automatic = 0,
SSL = 1,
STARTTLS = 2,
Plain = 3,
}
#[derive(Debug, PartialEq)]
impl Default for Socket {
fn default() -> Self {
Socket::Automatic
}
}
#[derive(Debug, PartialEq, Clone)]
#[repr(u8)]
pub enum UsernamePattern {
EMAIL = 1,
@@ -42,7 +51,7 @@ pub enum Oauth2Authorizer {
Gmail = 2,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Server {
pub protocol: Protocol,
pub socket: Socket,
@@ -51,20 +60,6 @@ pub struct Server {
pub username_pattern: UsernamePattern,
}
impl Server {
pub fn apply_username_pattern(&self, addr: String) -> String {
match self.username_pattern {
UsernamePattern::EMAIL => addr,
UsernamePattern::EMAILLOCALPART => {
if let Some(at) = addr.find('@') {
return addr.split_at(at).0.to_string();
}
addr
}
}
}
}
#[derive(Debug)]
pub struct ConfigDefault {
pub key: Config,
@@ -83,25 +78,6 @@ pub struct Provider {
pub oauth2_authorizer: Option<Oauth2Authorizer>,
}
impl Provider {
pub fn get_server(&self, protocol: Protocol) -> Option<&Server> {
for record in self.server.iter() {
if record.protocol == protocol {
return Some(record);
}
}
None
}
pub fn get_imap_server(&self) -> Option<&Server> {
self.get_server(Protocol::IMAP)
}
pub fn get_smtp_server(&self) -> Option<&Server> {
self.get_server(Protocol::SMTP)
}
}
pub fn get_provider_info(addr: &str) -> Option<&Provider> {
let domain = match addr.parse::<EmailAddress>() {
Ok(addr) => addr.domain,
@@ -116,9 +92,18 @@ pub fn get_provider_info(addr: &str) -> Option<&Provider> {
None
}
// returns update timestamp in seconds, UTC, compatible for comparison with time() and database times
pub fn get_provider_update_timestamp() -> i64 {
NaiveDateTime::new(*PROVIDER_UPDATED, NaiveTime::from_hms(0, 0, 0)).timestamp_millis() / 1_000
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::dc_tools::time;
use chrono::NaiveDate;
#[test]
fn test_get_provider_info_unexistant() {
@@ -137,15 +122,16 @@ mod tests {
let provider = get_provider_info("nauta.cu"); // this is no email address
assert!(provider.is_none());
let provider = get_provider_info("user@nauta.cu").unwrap();
let addr = "user@nauta.cu";
let provider = get_provider_info(addr).unwrap();
assert!(provider.status == Status::OK);
let server = provider.get_imap_server().unwrap();
let server = &provider.server[0];
assert_eq!(server.protocol, Protocol::IMAP);
assert_eq!(server.socket, Socket::STARTTLS);
assert_eq!(server.hostname, "imap.nauta.cu");
assert_eq!(server.port, 143);
assert_eq!(server.username_pattern, UsernamePattern::EMAIL);
let server = provider.get_smtp_server().unwrap();
let server = &provider.server[1];
assert_eq!(server.protocol, Protocol::SMTP);
assert_eq!(server.socket, Socket::STARTTLS);
assert_eq!(server.hostname, "smtp.nauta.cu");
@@ -160,4 +146,16 @@ mod tests {
let provider = get_provider_info("user@googlemail.com").unwrap();
assert!(provider.status == Status::PREPARATION);
}
#[test]
fn test_get_provider_update_timestamp() {
let timestamp_past = NaiveDateTime::new(
NaiveDate::from_ymd(2020, 9, 9),
NaiveTime::from_hms(0, 0, 0),
)
.timestamp_millis()
/ 1_000;
assert!(get_provider_update_timestamp() <= time());
assert!(get_provider_update_timestamp() > timestamp_past);
}
}

View File

@@ -4,6 +4,7 @@
import sys
import os
import yaml
import datetime
out_all = ""
out_domains = ""
@@ -41,8 +42,8 @@ def process_config_defaults(data):
config_defaults = data.get("config_defaults", "")
for key in config_defaults:
value = str(config_defaults[key])
defaults += " ConfigDefault { key: Config::" + camel(key) + ", value: \"" + value + "\" },\n"
defaults += " ])"
defaults += " ConfigDefault { key: Config::" + camel(key) + ", value: \"" + value + "\" },\n"
defaults += " ])"
return defaults
@@ -65,7 +66,7 @@ def process_data(data, file):
raise TypeError("domain used twice: " + domain)
domains_dict[domain] = True
domains += " (\"" + domain + "\", &*" + file2varname(file) + "),\n"
domains += " (\"" + domain + "\", &*" + file2varname(file) + "),\n"
comment += domain + ", "
@@ -95,7 +96,7 @@ def process_data(data, file):
if username_pattern != "EMAIL" and username_pattern != "EMAILLOCALPART":
raise TypeError("bad username pattern")
server += (" Server { protocol: " + protocol + ", socket: " + socket + ", hostname: \""
server += (" Server { protocol: " + protocol + ", socket: " + socket + ", hostname: \""
+ hostname + "\", port: " + str(port) + ", username_pattern: " + username_pattern + " },\n")
config_defaults = process_config_defaults(data)
@@ -110,16 +111,16 @@ def process_data(data, file):
before_login_hint = cleanstr(data.get("before_login_hint", ""))
after_login_hint = cleanstr(data.get("after_login_hint", ""))
if (not has_imap and not has_smtp) or (has_imap and has_smtp):
provider += " static ref " + file2varname(file) + ": Provider = Provider {\n"
provider += " status: Status::" + status + ",\n"
provider += " before_login_hint: \"" + before_login_hint + "\",\n"
provider += " after_login_hint: \"" + after_login_hint + "\",\n"
provider += " overview_page: \"" + file2url(file) + "\",\n"
provider += " server: vec![\n" + server + " ],\n"
provider += " config_defaults: " + config_defaults + ",\n"
provider += " strict_tls: " + strict_tls + ",\n"
provider += " oauth2_authorizer: " + oauth2 + ",\n"
provider += " };\n\n"
provider += "static " + file2varname(file) + ": Lazy<Provider> = Lazy::new(|| Provider {\n"
provider += " status: Status::" + status + ",\n"
provider += " before_login_hint: \"" + before_login_hint + "\",\n"
provider += " after_login_hint: \"" + after_login_hint + "\",\n"
provider += " overview_page: \"" + file2url(file) + "\",\n"
provider += " server: vec![\n" + server + " ],\n"
provider += " config_defaults: " + config_defaults + ",\n"
provider += " strict_tls: " + strict_tls + ",\n"
provider += " oauth2_authorizer: " + oauth2 + ",\n"
provider += "});\n\n"
else:
raise TypeError("SMTP and IMAP must be specified together or left out both")
@@ -128,7 +129,7 @@ def process_data(data, file):
# finally, add the provider
global out_all, out_domains
out_all += " // " + file[file.rindex("/")+1:] + ": " + comment.strip(", ") + "\n"
out_all += "// " + file[file.rindex("/")+1:] + ": " + comment.strip(", ") + "\n"
# also add provider with no special things to do -
# eg. _not_ supporting oauth2 is also an information and we can skip the mx-lookup in this case
@@ -163,12 +164,16 @@ if __name__ == "__main__":
"use crate::provider::UsernamePattern::*;\n"
"use crate::provider::*;\n"
"use std::collections::HashMap;\n\n"
"lazy_static::lazy_static! {\n\n")
"use once_cell::sync::Lazy;\n\n")
process_dir(sys.argv[1])
out_all += " pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [\n"
out_all += "pub static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| [\n"
out_all += out_domains;
out_all += " ].iter().copied().collect();\n}"
out_all += "].iter().copied().collect());\n\n"
now = datetime.datetime.utcnow()
out_all += "pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> = "\
"Lazy::new(|| chrono::NaiveDate::from_ymd("+str(now.year)+", "+str(now.month)+", "+str(now.day)+"));\n"
print(out_all)

View File

@@ -1,6 +1,6 @@
//! # QR code module
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
use percent_encoding::percent_decode_str;
use serde::Deserialize;
@@ -358,12 +358,10 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Lot {
Lot::from_address(context, name, addr).await
}
lazy_static! {
static ref VCARD_NAME_RE: regex::Regex =
regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap();
static ref VCARD_EMAIL_RE: regex::Regex =
regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap();
}
static VCARD_NAME_RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap());
static VCARD_EMAIL_RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
/// Extract address for the matmsg scheme.
///

View File

@@ -3,6 +3,7 @@ use async_std::sync::{channel, Receiver, Sender};
use async_std::task;
use crate::context::Context;
use crate::dc_tools::maybe_add_time_based_warnings;
use crate::imap::Imap;
use crate::job::{self, Thread};
use crate::{config::Config, message::MsgId, smtp::Smtp};
@@ -81,6 +82,8 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
warn!(ctx, "failed to close folder: {:?}", err);
}
maybe_add_time_based_warnings(&ctx).await;
info = if ctx.get_config_bool(Config::InboxWatch).await {
fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await
} else {
@@ -128,7 +131,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
// connect and fake idle if unable to connect
if let Err(err) = connection.connect_configured(&ctx).await {
warn!(ctx, "imap connection failed: {}", err);
return connection.fake_idle(&ctx, None).await;
return connection.fake_idle(&ctx, Some(watch_folder)).await;
}
// fetch
@@ -412,10 +415,7 @@ impl Scheduler {
/// Check if the scheduler is running.
pub fn is_running(&self) -> bool {
match self {
Scheduler::Running { .. } => true,
_ => false,
}
matches!(self, Scheduler::Running { .. })
}
}

View File

@@ -1,7 +1,8 @@
//! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol)
use std::time::Duration;
use std::time::{Duration, Instant};
use anyhow::{bail, Error};
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use crate::aheader::EncryptPreference;
@@ -11,16 +12,16 @@ use crate::constants::*;
use crate::contact::*;
use crate::context::Context;
use crate::e2ee::*;
use crate::error::{bail, Error};
use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::lot::LotState;
use crate::lot::{Lot, LotState};
use crate::message::Message;
use crate::mimeparser::*;
use crate::param::*;
use crate::peerstate::*;
use crate::qr::check_qr;
use crate::sql;
use crate::stock::StockMessage;
use crate::token;
@@ -67,6 +68,41 @@ macro_rules! get_qr_attr {
};
}
/// State for setup-contact/secure-join protocol joiner's side.
///
/// The setup-contact protocol needs to carry state for both the inviter (Alice) and the
/// joiner/invitee (Bob). For Alice this state is minimal and in the `tokens` table in the
/// database. For Bob this state is only carried live on the [Context] in this struct.
#[derive(Debug, Default)]
pub(crate) struct Bob {
/// The next message expected by the protocol.
expects: SecureJoinStep,
/// The QR-scanned information of the currently running protocol.
pub qr_scan: Option<Lot>,
}
/// The next message expected by [Bob] in the setup-contact/secure-join protocol.
#[derive(Debug, PartialEq)]
enum SecureJoinStep {
/// No setup-contact protocol running.
NotActive,
/// Expecting the auth-required message.
///
/// This corresponds to the `vc-auth-required` or `vg-auth-required` message of step 3d.
AuthRequired,
/// Expecting the contact-confirm message.
///
/// This corresponds to the `vc-contact-confirm` or `vg-member-added` message of step
/// 6b.
ContactConfirm,
}
impl Default for SecureJoinStep {
fn default() -> Self {
Self::NotActive
}
}
pub async fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option<String> {
/*=======================================================
==== Alice - the inviter side ====
@@ -152,78 +188,79 @@ async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
}
}
async fn cleanup(
context: &Context,
contact_chat_id: ChatId,
ongoing_allocated: bool,
join_vg: bool,
) -> ChatId {
async fn cleanup(context: &Context, ongoing_allocated: bool) {
let mut bob = context.bob.write().await;
bob.expects = 0;
let ret_chat_id: ChatId = if bob.status == DC_BOB_SUCCESS {
if join_vg {
chat::get_chat_id_by_grpid(
context,
bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(),
)
.await
.unwrap_or((ChatId::new(0), false, Blocked::Not))
.0
} else {
contact_chat_id
}
} else {
ChatId::new(0)
};
bob.expects = SecureJoinStep::NotActive;
bob.qr_scan = None;
if ongoing_allocated {
context.free_ongoing().await;
}
ret_chat_id
}
/// Take a scanned QR-code and do the setup-contact/join-group handshake.
/// See the ffi-documentation for more details.
pub async fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
#[derive(Debug, thiserror::Error)]
pub enum JoinError {
#[error("Unknown QR-code")]
QrCode,
#[error("Aborted by user")]
Aborted,
#[error("Failed to send handshake message")]
SendMessage(#[from] SendMsgError),
// Note that this can currently only occur if there is a bug in the QR/Lot code as this
// is supposed to create a contact for us.
#[error("Unknown contact (this is a bug)")]
UnknownContact,
// Note that this can only occur if we failed to create the chat correctly.
#[error("No Chat found for group (this is a bug)")]
MissingChat(#[source] sql::Error),
}
/// Take a scanned QR-code and do the setup-contact/join-group/invite handshake.
///
/// This is the start of the process for the joiner. See the module and ffi documentation
/// for more details.
///
/// When joining a group this will start an "ongoing" process and will block until the
/// process is completed, the [ChatId] for the new group is not known any sooner. When
/// verifying a contact this returns immediately.
pub async fn dc_join_securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
if context.alloc_ongoing().await.is_err() {
return cleanup(&context, ChatId::new(0), false, false).await;
cleanup(&context, false).await;
return Err(JoinError::Aborted);
}
securejoin(context, qr).await
}
async fn securejoin(context: &Context, qr: &str) -> ChatId {
async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
/*========================================================
==== Bob - the joiner's side =====
==== Step 2 in "Setup verified contact" protocol =====
========================================================*/
let mut contact_chat_id = ChatId::new(0);
let mut join_vg: bool = false;
info!(context, "Requesting secure-join ...",);
ensure_secret_key_exists(context).await.ok();
let qr_scan = check_qr(context, &qr).await;
if qr_scan.state != LotState::QrAskVerifyContact && qr_scan.state != LotState::QrAskVerifyGroup
{
error!(context, "Unknown QR code.",);
return cleanup(&context, contact_chat_id, true, join_vg).await;
cleanup(&context, true).await;
return Err(JoinError::QrCode);
}
contact_chat_id = match chat::create_by_contact_id(context, qr_scan.id).await {
let contact_chat_id = match chat::create_by_contact_id(context, qr_scan.id).await {
Ok(chat_id) => chat_id,
Err(_) => {
error!(context, "Unknown contact.");
return cleanup(&context, contact_chat_id, true, join_vg).await;
cleanup(&context, true).await;
return Err(JoinError::UnknownContact);
}
};
if context.shall_stop_ongoing().await {
return cleanup(&context, contact_chat_id, true, join_vg).await;
cleanup(&context, true).await;
return Err(JoinError::Aborted);
}
join_vg = qr_scan.get_state() == LotState::QrAskVerifyGroup;
let join_vg = qr_scan.get_state() == LotState::QrAskVerifyGroup;
{
let mut bob = context.bob.write().await;
bob.status = 0;
bob.qr_scan = Some(qr_scan);
}
if fingerprint_equals_sender(
@@ -245,7 +282,7 @@ async fn securejoin(context: &Context, qr: &str) -> ChatId {
// the scanned fingerprint matches Alice's key,
// we can proceed to step 4b) directly and save two mails
info!(context, "Taking protocol shortcut.");
context.bob.write().await.expects = DC_VC_CONTACT_CONFIRM;
context.bob.write().await.expects = SecureJoinStep::ContactConfirm;
joiner_progress!(
context,
chat_id_2_contact_id(context, contact_chat_id).await,
@@ -273,10 +310,11 @@ async fn securejoin(context: &Context, qr: &str) -> ChatId {
.await
{
error!(context, "failed to send handshake message: {}", err);
return cleanup(&context, contact_chat_id, true, join_vg).await;
cleanup(&context, true).await;
return Err(JoinError::SendMessage(err));
}
} else {
context.bob.write().await.expects = DC_VC_AUTH_REQUIRED;
context.bob.write().await.expects = SecureJoinStep::AuthRequired;
// Bob -> Alice
if let Err(err) = send_handshake_msg(
@@ -290,7 +328,8 @@ async fn securejoin(context: &Context, qr: &str) -> ChatId {
.await
{
error!(context, "failed to send handshake message: {}", err);
return cleanup(&context, contact_chat_id, true, join_vg).await;
cleanup(&context, true).await;
return Err(JoinError::SendMessage(err));
}
}
@@ -299,15 +338,45 @@ async fn securejoin(context: &Context, qr: &str) -> ChatId {
while !context.shall_stop_ongoing().await {
async_std::task::sleep(Duration::from_millis(50)).await;
}
cleanup(&context, contact_chat_id, true, join_vg).await
// handle_securejoin_handshake() calls Context::stop_ongoing before the group chat
// is created (it is created after handle_securejoin_handshake() returns by
// dc_receive_imf()). As a hack we just wait a bit for it to appear.
let start = Instant::now();
let chatid = loop {
{
let bob = context.bob.read().await;
let grpid = bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap();
match chat::get_chat_id_by_grpid(context, grpid).await {
Ok((chatid, _is_protected, _blocked)) => break chatid,
Err(err) => {
if start.elapsed() > Duration::from_secs(7) {
return Err(JoinError::MissingChat(err));
}
}
}
}
async_std::task::sleep(Duration::from_millis(50)).await;
};
cleanup(&context, true).await;
Ok(chatid)
} else {
// for a one-to-one-chat, the chat is already known, return the chat-id,
// the verification runs in background
context.free_ongoing().await;
contact_chat_id
Ok(contact_chat_id)
}
}
/// Error for [send_handshake_msg].
///
/// Wrapping the [anyhow::Error] means we can "impl From" more easily on errors from this
/// function.
#[derive(Debug, thiserror::Error)]
#[error("Failed sending handshake message")]
pub struct SendMsgError(#[from] anyhow::Error);
async fn send_handshake_msg(
context: &Context,
contact_chat_id: ChatId,
@@ -315,7 +384,7 @@ async fn send_handshake_msg(
param2: impl AsRef<str>,
fingerprint: Option<Fingerprint>,
grpid: impl AsRef<str>,
) -> Result<(), HandshakeError> {
) -> Result<(), SendMsgError> {
let mut msg = Message::default();
msg.viewtype = Viewtype::Text;
msg.text = Some(format!("Secure-Join: {}", step));
@@ -336,18 +405,12 @@ async fn send_handshake_msg(
msg.param.set(Param::Arg4, grpid.as_ref());
}
if step == "vg-request" || step == "vc-request" {
msg.param.set_int(
Param::ForcePlaintext,
ForcePlaintext::AddAutocryptHeader as i32,
);
msg.param.set_int(Param::ForcePlaintext, 1);
} else {
msg.param.set_int(Param::GuaranteeE2ee, 1);
}
chat::send_msg(context, contact_chat_id, &mut msg)
.await
.map_err(HandshakeError::MsgSendFailed)?;
chat::send_msg(context, contact_chat_id, &mut msg).await?;
Ok(())
}
@@ -407,7 +470,7 @@ pub(crate) enum HandshakeError {
#[error("No configured self address found")]
NoSelfAddr,
#[error("Failed to send message")]
MsgSendFailed(#[source] Error),
MsgSendFailed(#[from] SendMsgError),
#[error("Failed to parse fingerprint")]
BadFingerprint(#[from] crate::key::FingerprintError),
}
@@ -521,7 +584,7 @@ pub(crate) async fn handle_securejoin_handshake(
let bob = context.bob.read().await;
let scan = bob.qr_scan.as_ref();
scan.is_none()
|| bob.expects != DC_VC_AUTH_REQUIRED
|| bob.expects != SecureJoinStep::AuthRequired
|| join_vg && scan.unwrap().state != LotState::QrAskVerifyGroup
};
@@ -545,7 +608,6 @@ pub(crate) async fn handle_securejoin_handshake(
},
)
.await;
context.bob.write().await.status = 0; // secure-join failed
context.stop_ongoing().await;
return Ok(HandshakeMessage::Ignore);
}
@@ -558,14 +620,13 @@ pub(crate) async fn handle_securejoin_handshake(
"Fingerprint mismatch on joiner-side.",
)
.await;
context.bob.write().await.status = 0; // secure-join failed
context.stop_ongoing().await;
return Ok(HandshakeMessage::Ignore);
}
info!(context, "Fingerprint verified.",);
let own_fingerprint = get_self_fingerprint(context).await.unwrap();
joiner_progress!(context, contact_id, 400);
context.bob.write().await.expects = DC_VC_CONTACT_CONFIRM;
context.bob.write().await.expects = SecureJoinStep::ContactConfirm;
// Bob -> Alice
send_handshake_msg(
@@ -709,7 +770,7 @@ pub(crate) async fn handle_securejoin_handshake(
HandshakeMessage::Ignore
};
if context.bob.read().await.expects != DC_VC_CONTACT_CONFIRM {
if context.bob.read().await.expects != SecureJoinStep::ContactConfirm {
info!(context, "Message belongs to a different handshake.",);
return Ok(abort_retval);
}
@@ -730,19 +791,19 @@ pub(crate) async fn handle_securejoin_handshake(
let vg_expect_encrypted = if join_vg {
let group_id = get_qr_attr!(context, text2).to_string();
// This is buggy, is_verified_group will always be
// This is buggy, is_protected_group will always be
// false since the group is created by receive_imf by
// the very handshake message we're handling now. But
// only after we have returned. It does not impact
// the security invariants of secure-join however.
let (_, is_verified_group, _) = chat::get_chat_id_by_grpid(context, &group_id)
let (_, is_protected_group, _) = chat::get_chat_id_by_grpid(context, &group_id)
.await
.unwrap_or((ChatId::new(0), false, Blocked::Not));
// when joining a non-verified group
// the vg-member-added message may be unencrypted
// when not all group members have keys or prefer encryption.
// So only expect encryption if this is a verified group
is_verified_group
is_protected_group
} else {
// setup contact is always encrypted
true
@@ -756,7 +817,6 @@ pub(crate) async fn handle_securejoin_handshake(
"Contact confirm message not encrypted.",
)
.await;
context.bob.write().await.status = 0;
return Ok(abort_retval);
}
@@ -788,7 +848,7 @@ pub(crate) async fn handle_securejoin_handshake(
return Ok(abort_retval);
}
secure_connection_established(context, contact_chat_id).await;
context.bob.write().await.expects = 0;
context.bob.write().await.expects = SecureJoinStep::NotActive;
// Bob -> Alice
send_handshake_msg(
@@ -805,7 +865,6 @@ pub(crate) async fn handle_securejoin_handshake(
)
.await?;
context.bob.write().await.status = 1;
context.stop_ongoing().await;
Ok(if join_vg {
HandshakeMessage::Propagate
@@ -825,6 +884,7 @@ pub(crate) async fn handle_securejoin_handshake(
return Ok(HandshakeMessage::Ignore);
}
if join_vg {
// Responsible for showing "$Bob securely joined $group" message
inviter_progress!(context, contact_id, 800);
inviter_progress!(context, contact_id, 1000);
let field_grpid = mime_message
@@ -1036,3 +1096,338 @@ fn encrypted_and_signed(
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat;
use crate::chat::ProtectionStatus;
use crate::peerstate::Peerstate;
use crate::test_utils::TestContext;
#[async_std::test]
async fn test_setup_contact() {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
// Generate QR-code, ChatId(0) indicates setup-contact
let qr = dc_get_securejoin_qr(&alice.ctx, ChatId::new(0))
.await
.unwrap();
// Bob scans QR-code, sends vc-request
let bob_chatid = dc_join_securejoin(&bob.ctx, &qr).await.unwrap();
let sent = bob.pop_sent_msg().await;
assert_eq!(sent.id(), bob_chatid);
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vc-request");
assert!(msg.get(HeaderDef::SecureJoinInvitenumber).is_some());
// Alice receives vc-request, sends vc-auth-required
alice.recv_msg(&sent).await;
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vc-auth-required");
// Bob receives vc-auth-required, sends vc-request-with-auth
bob.recv_msg(&sent).await;
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-request-with-auth"
);
assert!(msg.get(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
.await
.unwrap()
.fingerprint();
assert_eq!(
*msg.get(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
);
// Alice should not yet have Bob verified
let contact_bob_id =
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown).await;
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id)
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&alice.ctx).await,
VerifiedStatus::Unverified
);
// Alice receives vc-request-with-auth, sends vc-contact-confirm
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await,
VerifiedStatus::BidirectVerified
);
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm"
);
// Bob should not yet have Alice verified
let contact_alice_id =
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown).await;
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id)
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&bob.ctx).await,
VerifiedStatus::Unverified
);
// Bob receives vc-contact-confirm, sends vc-contact-confirm-received
bob.recv_msg(&sent).await;
assert_eq!(
contact_alice.is_verified(&bob.ctx).await,
VerifiedStatus::BidirectVerified
);
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm-received"
);
}
#[async_std::test]
async fn test_setup_contact_bad_qr() {
let bob = TestContext::new_bob().await;
let ret = dc_join_securejoin(&bob.ctx, "not a qr code").await;
assert!(matches!(ret, Err(JoinError::QrCode)));
}
#[async_std::test]
async fn test_setup_contact_bob_knows_alice() {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
// Ensure Bob knows Alice_FP
let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await.unwrap();
let peerstate = Peerstate {
context: &bob.ctx,
addr: "alice@example.com".into(),
last_seen: 10,
last_seen_autocrypt: 10,
prefer_encrypt: EncryptPreference::Mutual,
public_key: Some(alice_pubkey.clone()),
public_key_fingerprint: Some(alice_pubkey.fingerprint()),
gossip_key: Some(alice_pubkey.clone()),
gossip_timestamp: 10,
gossip_key_fingerprint: Some(alice_pubkey.fingerprint()),
verified_key: None,
verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
peerstate.save_to_db(&bob.ctx.sql, true).await.unwrap();
// Generate QR-code, ChatId(0) indicates setup-contact
let qr = dc_get_securejoin_qr(&alice.ctx, ChatId::new(0))
.await
.unwrap();
// Bob scans QR-code, sends vc-request-with-auth, skipping vc-request
dc_join_securejoin(&bob.ctx, &qr).await.unwrap();
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-request-with-auth"
);
assert!(msg.get(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
.await
.unwrap()
.fingerprint();
assert_eq!(
*msg.get(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
);
// Alice should not yet have Bob verified
let (contact_bob_id, _modified) = Contact::add_or_lookup(
&alice.ctx,
"Bob",
"bob@example.net",
Origin::ManuallyCreated,
)
.await
.unwrap();
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id)
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&alice.ctx).await,
VerifiedStatus::Unverified
);
// Alice receives vc-request-with-auth, sends vc-contact-confirm
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await,
VerifiedStatus::BidirectVerified
);
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm"
);
// Bob should not yet have Alice verified
let contact_alice_id =
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown).await;
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id)
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&bob.ctx).await,
VerifiedStatus::Unverified
);
// Bob receives vc-contact-confirm, sends vc-contact-confirm-received
bob.recv_msg(&sent).await;
assert_eq!(
contact_alice.is_verified(&bob.ctx).await,
VerifiedStatus::BidirectVerified
);
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm-received"
);
}
#[async_std::test]
async fn test_secure_join() {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chatid = chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat")
.await
.unwrap();
// Generate QR-code, secure-join implied by chatid
let qr = dc_get_securejoin_qr(&alice.ctx, chatid).await.unwrap();
// Bob scans QR-code, sends vg-request; blocks on ongoing process
let joiner = {
let qr = qr.clone();
let ctx = bob.ctx.clone();
async_std::task::spawn(async move { dc_join_securejoin(&ctx, &qr).await.unwrap() })
};
let sent = bob.pop_sent_msg().await;
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vg-request");
assert!(msg.get(HeaderDef::SecureJoinInvitenumber).is_some());
// Alice receives vg-request, sends vg-auth-required
alice.recv_msg(&sent).await;
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vg-auth-required");
// Bob receives vg-auth-required, sends vg-request-with-auth
bob.recv_msg(&sent).await;
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
"vg-request-with-auth"
);
assert!(msg.get(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
.await
.unwrap()
.fingerprint();
assert_eq!(
*msg.get(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
);
// Alice should not yet have Bob verified
let contact_bob_id =
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown).await;
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id)
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&alice.ctx).await,
VerifiedStatus::Unverified
);
// Alice receives vg-request-with-auth, sends vg-member-added
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await,
VerifiedStatus::BidirectVerified
);
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(msg.get(HeaderDef::SecureJoin).unwrap(), "vg-member-added");
// Bob should not yet have Alice verified
let contact_alice_id =
Contact::lookup_id_by_addr(&bob.ctx, "alice@example.com", Origin::Unknown).await;
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id)
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&bob.ctx).await,
VerifiedStatus::Unverified
);
// Bob receives vg-member-added, sends vg-member-added-received
bob.recv_msg(&sent).await;
assert_eq!(
contact_alice.is_verified(&bob.ctx).await,
VerifiedStatus::BidirectVerified
);
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get(HeaderDef::SecureJoin).unwrap(),
"vg-member-added-received"
);
let bob_chatid = joiner.await;
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await.unwrap();
assert!(bob_chat.is_protected());
}
}

View File

@@ -1,3 +1,5 @@
use itertools::Itertools;
// protect lines starting with `--` against being treated as a footer.
// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B);
// this should be invisible on most systems and there is no need to unescape it again
@@ -64,33 +66,28 @@ fn split_lines(buf: &str) -> Vec<&str> {
/// Simplify message text for chat display.
/// Remove quotes, signatures, trailing empty lines etc.
pub fn simplify(mut input: String, is_chat_message: bool) -> (String, bool) {
pub fn simplify(mut input: String, is_chat_message: bool) -> (String, bool, Option<String>) {
input.retain(|c| c != '\r');
let lines = split_lines(&input);
let (lines, is_forwarded) = skip_forward_header(&lines);
let (lines, top_quote) = remove_top_quote(lines);
let original_lines = &lines;
let lines = remove_message_footer(lines);
let text = if is_chat_message {
render_message(lines, false, false)
render_message(lines, false)
} else {
let (lines, has_nonstandard_footer) = remove_nonstandard_footer(lines);
let (lines, has_bottom_quote) = remove_bottom_quote(lines);
let (lines, has_top_quote) = remove_top_quote(lines);
if lines.iter().all(|it| it.trim().is_empty()) {
render_message(original_lines, false, false)
render_message(original_lines, false)
} else {
render_message(
lines,
has_top_quote,
has_nonstandard_footer || has_bottom_quote,
)
render_message(lines, has_nonstandard_footer || has_bottom_quote)
}
};
(text, is_forwarded)
(text, is_forwarded, top_quote)
}
/// Skips "forwarded message" header.
@@ -134,11 +131,15 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
}
#[allow(clippy::indexing_slicing)]
fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
let mut first_quoted_line = 0;
let mut last_quoted_line = None;
let mut has_quoted_headline = false;
for (l, line) in lines.iter().enumerate() {
if is_plain_quote(line) {
if last_quoted_line.is_none() {
first_quoted_line = l;
}
last_quoted_line = Some(l)
} else if !is_empty_line(line) {
if is_quoted_headline(line) && !has_quoted_headline && last_quoted_line.is_none() {
@@ -150,17 +151,25 @@ fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
}
}
if let Some(last_quoted_line) = last_quoted_line {
(&lines[last_quoted_line + 1..], true)
(
&lines[last_quoted_line + 1..],
Some(
lines[first_quoted_line..last_quoted_line + 1]
.iter()
.map(|s| {
s.strip_prefix(">")
.map_or(*s, |u| u.strip_prefix(" ").unwrap_or(u))
})
.join("\n"),
),
)
} else {
(lines, false)
(lines, None)
}
}
fn render_message(lines: &[&str], is_cut_at_begin: bool, is_cut_at_end: bool) -> String {
fn render_message(lines: &[&str], is_cut_at_end: bool) -> String {
let mut ret = String::new();
if is_cut_at_begin {
ret += "[...]";
}
/* we write empty lines only in case and non-empty line follows */
let mut pending_linebreaks = 0;
let mut empty_body = true;
@@ -183,7 +192,7 @@ fn render_message(lines: &[&str], is_cut_at_begin: bool, is_cut_at_end: bool) ->
pending_linebreaks = 1
}
}
if is_cut_at_end && (!is_cut_at_begin || !empty_body) {
if is_cut_at_end && !empty_body {
ret += " [...]";
}
// redo escaping done by escape_message_footer_marks()
@@ -231,7 +240,7 @@ mod tests {
#[test]
// proptest does not support [[:graphical:][:space:]] regex.
fn test_simplify_plain_text_fuzzy(input in "[!-~\t \n]+") {
let (output, _is_forwarded) = simplify(input, true);
let (output, _is_forwarded, _) = simplify(input, true);
assert!(output.split('\n').all(|s| s != "-- "));
}
}
@@ -239,7 +248,7 @@ mod tests {
#[test]
fn test_dont_remove_whole_message() {
let input = "\n------\nFailed\n------\n\nUh-oh, this workflow did not succeed!\n\nlots of other text".to_string();
let (plain, is_forwarded) = simplify(input, false);
let (plain, is_forwarded, _) = simplify(input, false);
assert_eq!(
plain,
"------\nFailed\n------\n\nUh-oh, this workflow did not succeed!\n\nlots of other text"
@@ -250,7 +259,7 @@ mod tests {
#[test]
fn test_chat_message() {
let input = "Hi! How are you?\n\n---\n\nI am good.\n-- \nSent with my Delta Chat Messenger: https://delta.chat".to_string();
let (plain, is_forwarded) = simplify(input, true);
let (plain, is_forwarded, _) = simplify(input, true);
assert_eq!(plain, "Hi! How are you?\n\n---\n\nI am good.");
assert!(!is_forwarded);
}
@@ -258,7 +267,7 @@ mod tests {
#[test]
fn test_simplify_trim() {
let input = "line1\n\r\r\rline2".to_string();
let (plain, is_forwarded) = simplify(input, false);
let (plain, is_forwarded, _) = simplify(input, false);
assert_eq!(plain, "line1\nline2");
assert!(!is_forwarded);
@@ -267,7 +276,7 @@ mod tests {
#[test]
fn test_simplify_forwarded_message() {
let input = "---------- Forwarded message ----------\r\nFrom: test@example.com\r\n\r\nForwarded message\r\n-- \r\nSignature goes here".to_string();
let (plain, is_forwarded) = simplify(input, false);
let (plain, is_forwarded, _) = simplify(input, false);
assert_eq!(plain, "Forwarded message");
assert!(is_forwarded);
@@ -287,17 +296,17 @@ mod tests {
#[test]
fn test_remove_top_quote() {
let (lines, has_top_quote) = remove_top_quote(&["> first", "> second"]);
let (lines, top_quote) = remove_top_quote(&["> first", "> second"]);
assert!(lines.is_empty());
assert!(has_top_quote);
assert_eq!(top_quote.unwrap(), "first\nsecond");
let (lines, has_top_quote) = remove_top_quote(&["> first", "> second", "not a quote"]);
let (lines, top_quote) = remove_top_quote(&["> first", "> second", "not a quote"]);
assert_eq!(lines, &["not a quote"]);
assert!(has_top_quote);
assert_eq!(top_quote.unwrap(), "first\nsecond");
let (lines, has_top_quote) = remove_top_quote(&["not a quote", "> first", "> second"]);
let (lines, top_quote) = remove_top_quote(&["not a quote", "> first", "> second"]);
assert_eq!(lines, &["not a quote", "> first", "> second"]);
assert!(!has_top_quote);
assert!(top_quote.is_none());
}
#[test]
@@ -312,41 +321,41 @@ mod tests {
#[test]
fn test_remove_message_footer() {
let input = "text\n--\nno footer".to_string();
let (plain, _) = simplify(input, true);
let (plain, _, _) = simplify(input, true);
assert_eq!(plain, "text\n--\nno footer");
let input = "text\n\n--\n\nno footer".to_string();
let (plain, _) = simplify(input, true);
let (plain, _, _) = simplify(input, true);
assert_eq!(plain, "text\n\n--\n\nno footer");
let input = "text\n\n-- no footer\n\n".to_string();
let (plain, _) = simplify(input, true);
let (plain, _, _) = simplify(input, true);
assert_eq!(plain, "text\n\n-- no footer");
let input = "text\n\n--\nno footer\n-- \nfooter".to_string();
let (plain, _) = simplify(input, true);
let (plain, _, _) = simplify(input, true);
assert_eq!(plain, "text\n\n--\nno footer");
let input = "text\n\n--\ntreated as footer when unescaped".to_string();
let (plain, _) = simplify(input.clone(), true);
let (plain, _, _) = simplify(input.clone(), true);
assert_eq!(plain, "text"); // see remove_message_footer() for some explanations
let escaped = escape_message_footer_marks(&input);
let (plain, _) = simplify(escaped, true);
let (plain, _, _) = simplify(escaped, true);
assert_eq!(plain, "text\n\n--\ntreated as footer when unescaped");
// Nonstandard footer sent by https://siju.es/
let input = "Message text here\n---Desde mi teléfono con SIJÚ\n\nQuote here".to_string();
let (plain, _) = simplify(input.clone(), false);
let (plain, _, _) = simplify(input.clone(), false);
assert_eq!(plain, "Message text here [...]");
let (plain, _) = simplify(input.clone(), true);
let (plain, _, _) = simplify(input.clone(), true);
assert_eq!(plain, input);
let input = "--\ntreated as footer when unescaped".to_string();
let (plain, _) = simplify(input.clone(), true);
let (plain, _, _) = simplify(input.clone(), true);
assert_eq!(plain, ""); // see remove_message_footer() for some explanations
let escaped = escape_message_footer_marks(&input);
let (plain, _) = simplify(escaped, true);
let (plain, _, _) = simplify(escaped, true);
assert_eq!(plain, "--\ntreated as footer when unescaped");
}
}

View File

@@ -10,9 +10,9 @@ use async_smtp::*;
use crate::constants::*;
use crate::context::Context;
use crate::events::EventType;
use crate::login_param::{dc_build_tls, CertificateChecks, LoginParam};
use crate::login_param::{dc_build_tls, CertificateChecks, LoginParam, ServerLoginParam};
use crate::oauth2::*;
use crate::provider::get_provider_info;
use crate::provider::{get_provider_info, Socket};
use crate::stock::StockMessage;
/// SMTP write and read timeout in seconds.
@@ -30,7 +30,7 @@ pub enum Error {
error: error::Error,
},
#[error("SMTP: failed to connect: {0:?}")]
#[error("SMTP: failed to connect: {0}")]
ConnectionFailure(#[source] smtp::error::Error),
#[error("SMTP: failed to setup connection {0:?}")]
@@ -94,31 +94,65 @@ impl Smtp {
.unwrap_or_default()
}
/// Connect using configured parameters.
pub async fn connect_configured(&mut self, context: &Context) -> Result<()> {
if self.is_connected().await {
return Ok(());
}
let lp = LoginParam::from_database(context, "configured_").await;
let res = self
.connect(
context,
&lp.smtp,
&lp.addr,
lp.server_flags & DC_LP_AUTH_OAUTH2 != 0,
)
.await;
if let Err(ref err) = res {
let message = context
.stock_string_repl_str2(
StockMessage::ServerResponse,
format!("SMTP {}:{}", lp.smtp.server, lp.smtp.port),
err.to_string(),
)
.await;
context.emit_event(EventType::ErrorNetwork(message));
};
res
}
/// Connect using the provided login params.
pub async fn connect(&mut self, context: &Context, lp: &LoginParam) -> Result<()> {
pub async fn connect(
&mut self,
context: &Context,
lp: &ServerLoginParam,
addr: &str,
oauth2: bool,
) -> Result<()> {
if self.is_connected().await {
warn!(context, "SMTP already connected.");
return Ok(());
}
if lp.send_server.is_empty() || lp.send_port == 0 {
context.emit_event(EventType::ErrorNetwork("SMTP bad parameters.".into()));
if lp.server.is_empty() || lp.port == 0 {
return Err(Error::BadParameters);
}
let from =
EmailAddress::new(lp.addr.clone()).map_err(|err| Error::InvalidLoginAddress {
address: lp.addr.clone(),
EmailAddress::new(addr.to_string()).map_err(|err| Error::InvalidLoginAddress {
address: addr.to_string(),
error: err,
})?;
self.from = Some(from);
let domain = &lp.send_server;
let port = lp.send_port as u16;
let domain = &lp.server;
let port = lp.port;
let provider = get_provider_info(&lp.addr);
let strict_tls = match lp.smtp_certificate_checks {
let provider = get_provider_info(addr);
let strict_tls = match lp.certificate_checks {
CertificateChecks::Automatic => provider.map_or(false, |provider| provider.strict_tls),
CertificateChecks::Strict => true,
CertificateChecks::AcceptInvalidCertificates
@@ -127,17 +161,16 @@ impl Smtp {
let tls_config = dc_build_tls(strict_tls);
let tls_parameters = ClientTlsParameters::new(domain.to_string(), tls_config);
let (creds, mechanism) = if 0 != lp.server_flags & (DC_LP_AUTH_OAUTH2 as i32) {
let (creds, mechanism) = if oauth2 {
// oauth2
let addr = &lp.addr;
let send_pw = &lp.send_pw;
let send_pw = &lp.password;
let access_token = dc_get_oauth2_access_token(context, addr, send_pw, false).await;
if access_token.is_none() {
return Err(Error::Oauth2Error {
address: addr.to_string(),
});
}
let user = &lp.send_user;
let user = &lp.user;
(
smtp::authentication::Credentials::new(
user.to_string(),
@@ -147,8 +180,8 @@ impl Smtp {
)
} else {
// plain
let user = lp.send_user.clone();
let pw = lp.send_pw.clone();
let user = lp.user.clone();
let pw = lp.password.clone();
(
smtp::authentication::Credentials::new(user, pw),
vec![
@@ -158,12 +191,9 @@ impl Smtp {
)
};
let security = if 0
!= lp.server_flags & (DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_PLAIN) as i32
{
smtp::ClientSecurity::Opportunistic(tls_parameters)
} else {
smtp::ClientSecurity::Wrapper(tls_parameters)
let security = match lp.security {
Socket::STARTTLS | Socket::Plain => smtp::ClientSecurity::Opportunistic(tls_parameters),
_ => smtp::ClientSecurity::Wrapper(tls_parameters),
};
let client = smtp::SmtpClient::with_security((domain.as_str(), port), security)
@@ -179,15 +209,6 @@ impl Smtp {
let mut trans = client.into_transport();
if let Err(err) = trans.connect().await {
let message = context
.stock_string_repl_str2(
StockMessage::ServerResponse,
format!("SMTP {}:{}", domain, port),
err.to_string(),
)
.await;
emit_event!(context, EventType::ErrorNetwork(message));
return Err(Error::ConnectionFailure(err));
}
@@ -196,7 +217,7 @@ impl Smtp {
context.emit_event(EventType::SmtpConnected(format!(
"SMTP-LOGIN as {} ok",
lp.send_user,
lp.user,
)));
Ok(())

View File

@@ -5,6 +5,7 @@ use async_smtp::*;
use crate::context::Context;
use crate::events::EventType;
use itertools::Itertools;
use std::time::Duration;
pub type Result<T> = std::result::Result<T, Error>;
@@ -33,11 +34,7 @@ impl Smtp {
) -> Result<()> {
let message_len_bytes = message.len();
let recipients_display = recipients
.iter()
.map(|x| format!("{}", x))
.collect::<Vec<String>>()
.join(",");
let recipients_display = recipients.iter().map(|x| x.to_string()).join(",");
let envelope =
Envelope::new(self.from.clone(), recipients).map_err(Error::EnvelopeError)?;

View File

@@ -14,6 +14,7 @@ use crate::constants::{ShowEmails, DC_CHAT_ID_TRASH};
use crate::context::Context;
use crate::dc_tools::*;
use crate::ephemeral::start_ephemeral_timers;
use crate::error::format_err;
use crate::param::*;
use crate::peerstate::*;
@@ -77,18 +78,29 @@ impl Sql {
// drop closes the connection
}
// return true on success, false on failure
pub async fn open<T: AsRef<Path>>(&self, context: &Context, dbfile: T, readonly: bool) -> bool {
match open(context, self, dbfile, readonly).await {
Ok(_) => true,
Err(err) => match err.downcast_ref::<Error>() {
Some(Error::SqlAlreadyOpen) => false,
pub async fn open<T: AsRef<Path>>(
&self,
context: &Context,
dbfile: T,
readonly: bool,
) -> crate::error::Result<()> {
let res = open(context, self, &dbfile, readonly).await;
if let Err(err) = &res {
match err.downcast_ref::<Error>() {
Some(Error::SqlAlreadyOpen) => {}
_ => {
self.close().await;
false
}
},
}
}
res.map_err(|e| {
format_err!(
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
"Could not open db file {}: {:#}",
dbfile.as_ref().to_string_lossy(),
e
)
})
}
pub async fn execute<S: AsRef<str>>(
@@ -130,7 +142,7 @@ impl Sql {
&self,
) -> Result<r2d2::PooledConnection<r2d2_sqlite::SqliteConnectionManager>> {
let lock = self.pool.read().await;
let pool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
let conn = pool.get()?;
Ok(conn)
@@ -144,7 +156,7 @@ impl Sql {
+ FnOnce(r2d2::PooledConnection<r2d2_sqlite::SqliteConnectionManager>) -> Result<H>,
{
let lock = self.pool.read().await;
let pool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
let conn = pool.get()?;
g(conn)
@@ -156,7 +168,7 @@ impl Sql {
Fut: Future<Output = Result<H>> + Send,
{
let lock = self.pool.read().await;
let pool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?;
let conn = pool.get()?;
g(conn).await
@@ -666,7 +678,10 @@ async fn open(
.with_flags(open_flags)
.with_init(|c| {
c.execute_batch(&format!(
"PRAGMA secure_delete=on; PRAGMA busy_timeout = {};",
"PRAGMA secure_delete=on;
PRAGMA busy_timeout = {};
PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android
",
Duration::from_secs(10).as_millis()
))?;
Ok(())
@@ -693,156 +708,208 @@ async fn open(
.ok();
let mut exists_before_update = false;
let mut dbversion_before_update: i32 = 0;
/* Init tables to dbversion=0 */
// Init tables to dbversion=68
let mut dbversion_before_update: i32 = 68;
if !sql.table_exists("config").await? {
info!(
context,
"First time init: creating tables in {:?}.",
dbfile.as_ref(),
);
sql.execute(
"CREATE TABLE config (id INTEGER PRIMARY KEY, keyname TEXT, value TEXT);",
paramsv![],
)
sql.with_conn(move |mut conn| {
let tx = conn.transaction()?;
tx.execute_batch(
r#"
CREATE TABLE config (id INTEGER PRIMARY KEY, keyname TEXT, value TEXT);
CREATE INDEX config_index1 ON config (keyname);
CREATE TABLE contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT DEFAULT '',
addr TEXT DEFAULT '' COLLATE NOCASE,
origin INTEGER DEFAULT 0,
blocked INTEGER DEFAULT 0,
last_seen INTEGER DEFAULT 0,
param TEXT DEFAULT '',
authname TEXT DEFAULT '',
selfavatar_sent INTEGER DEFAULT 0
);
CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE);
CREATE INDEX contacts_index2 ON contacts (addr COLLATE NOCASE);
INSERT INTO contacts (id,name,origin) VALUES
(1,'self',262144), (2,'info',262144), (3,'rsvd',262144),
(4,'rsvd',262144), (5,'device',262144), (6,'rsvd',262144),
(7,'rsvd',262144), (8,'rsvd',262144), (9,'rsvd',262144);
CREATE TABLE chats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type INTEGER DEFAULT 0,
name TEXT DEFAULT '',
draft_timestamp INTEGER DEFAULT 0,
draft_txt TEXT DEFAULT '',
blocked INTEGER DEFAULT 0,
grpid TEXT DEFAULT '',
param TEXT DEFAULT '',
archived INTEGER DEFAULT 0,
gossiped_timestamp INTEGER DEFAULT 0,
locations_send_begin INTEGER DEFAULT 0,
locations_send_until INTEGER DEFAULT 0,
locations_last_sent INTEGER DEFAULT 0,
created_timestamp INTEGER DEFAULT 0,
muted_until INTEGER DEFAULT 0,
ephemeral_timer INTEGER
);
CREATE INDEX chats_index1 ON chats (grpid);
CREATE INDEX chats_index2 ON chats (archived);
CREATE INDEX chats_index3 ON chats (locations_send_until);
INSERT INTO chats (id,type,name) VALUES
(1,120,'deaddrop'), (2,120,'rsvd'), (3,120,'trash'),
(4,120,'msgs_in_creation'), (5,120,'starred'), (6,120,'archivedlink'),
(7,100,'rsvd'), (8,100,'rsvd'), (9,100,'rsvd');
CREATE TABLE chats_contacts (chat_id INTEGER, contact_id INTEGER);
CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);
CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);
CREATE TABLE msgs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rfc724_mid TEXT DEFAULT '',
server_folder TEXT DEFAULT '',
server_uid INTEGER DEFAULT 0,
chat_id INTEGER DEFAULT 0,
from_id INTEGER DEFAULT 0,
to_id INTEGER DEFAULT 0,
timestamp INTEGER DEFAULT 0,
type INTEGER DEFAULT 0,
state INTEGER DEFAULT 0,
msgrmsg INTEGER DEFAULT 1,
bytes INTEGER DEFAULT 0,
txt TEXT DEFAULT '',
txt_raw TEXT DEFAULT '',
param TEXT DEFAULT '',
starred INTEGER DEFAULT 0,
timestamp_sent INTEGER DEFAULT 0,
timestamp_rcvd INTEGER DEFAULT 0,
hidden INTEGER DEFAULT 0,
mime_headers TEXT,
mime_in_reply_to TEXT,
mime_references TEXT,
move_state INTEGER DEFAULT 1,
location_id INTEGER DEFAULT 0,
error TEXT DEFAULT '',
-- Timer value in seconds. For incoming messages this
-- timer starts when message is read, so we want to have
-- the value stored here until the timer starts.
ephemeral_timer INTEGER DEFAULT 0,
-- Timestamp indicating when the message should be
-- deleted. It is convenient to store it here because UI
-- needs this value to display how much time is left until
-- the message is deleted.
ephemeral_timestamp INTEGER DEFAULT 0
);
CREATE INDEX msgs_index1 ON msgs (rfc724_mid);
CREATE INDEX msgs_index2 ON msgs (chat_id);
CREATE INDEX msgs_index3 ON msgs (timestamp);
CREATE INDEX msgs_index4 ON msgs (state);
CREATE INDEX msgs_index5 ON msgs (starred);
CREATE INDEX msgs_index6 ON msgs (location_id);
CREATE INDEX msgs_index7 ON msgs (state, hidden, chat_id);
INSERT INTO msgs (id,msgrmsg,txt) VALUES
(1,0,'marker1'), (2,0,'rsvd'), (3,0,'rsvd'),
(4,0,'rsvd'), (5,0,'rsvd'), (6,0,'rsvd'), (7,0,'rsvd'),
(8,0,'rsvd'), (9,0,'daymarker');
CREATE TABLE jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
added_timestamp INTEGER,
desired_timestamp INTEGER DEFAULT 0,
action INTEGER,
foreign_id INTEGER,
param TEXT DEFAULT '',
thread INTEGER DEFAULT 0,
tries INTEGER DEFAULT 0
);
CREATE INDEX jobs_index1 ON jobs (desired_timestamp);
CREATE TABLE leftgrps (
id INTEGER PRIMARY KEY,
grpid TEXT DEFAULT ''
);
CREATE INDEX leftgrps_index1 ON leftgrps (grpid);
CREATE TABLE keypairs (
id INTEGER PRIMARY KEY,
addr TEXT DEFAULT '' COLLATE NOCASE,
is_default INTEGER DEFAULT 0,
private_key,
public_key,
created INTEGER DEFAULT 0
);
CREATE TABLE acpeerstates (
id INTEGER PRIMARY KEY,
addr TEXT DEFAULT '' COLLATE NOCASE,
last_seen INTEGER DEFAULT 0,
last_seen_autocrypt INTEGER DEFAULT 0,
public_key,
prefer_encrypted INTEGER DEFAULT 0,
gossip_timestamp INTEGER DEFAULT 0,
gossip_key,
public_key_fingerprint TEXT DEFAULT '',
gossip_key_fingerprint TEXT DEFAULT '',
verified_key,
verified_key_fingerprint TEXT DEFAULT ''
);
CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);
CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);
CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);
CREATE TABLE msgs_mdns (
msg_id INTEGER,
contact_id INTEGER,
timestamp_sent INTEGER DEFAULT 0
);
CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);
CREATE TABLE tokens (
id INTEGER PRIMARY KEY,
namespc INTEGER DEFAULT 0,
foreign_id INTEGER DEFAULT 0,
token TEXT DEFAULT '',
timestamp INTEGER DEFAULT 0
);
CREATE TABLE locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
latitude REAL DEFAULT 0.0,
longitude REAL DEFAULT 0.0,
accuracy REAL DEFAULT 0.0,
timestamp INTEGER DEFAULT 0,
chat_id INTEGER DEFAULT 0,
from_id INTEGER DEFAULT 0,
independent INTEGER DEFAULT 0
);
CREATE INDEX locations_index1 ON locations (from_id);
CREATE INDEX locations_index2 ON locations (timestamp);
CREATE TABLE devmsglabels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT,
msg_id INTEGER DEFAULT 0
);
CREATE INDEX devmsglabels_index1 ON devmsglabels (label);
"#,
)?;
tx.commit()?;
Ok(())
})
.await?;
sql.execute(
"CREATE INDEX config_index1 ON config (keyname);",
paramsv![],
)
.await?;
sql.execute(
"CREATE TABLE contacts (\
id INTEGER PRIMARY KEY AUTOINCREMENT, \
name TEXT DEFAULT '', \
addr TEXT DEFAULT '' COLLATE NOCASE, \
origin INTEGER DEFAULT 0, \
blocked INTEGER DEFAULT 0, \
last_seen INTEGER DEFAULT 0, \
param TEXT DEFAULT '');",
paramsv![],
)
.await?;
sql.execute(
"CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE);",
paramsv![],
)
.await?;
sql.execute(
"CREATE INDEX contacts_index2 ON contacts (addr COLLATE NOCASE);",
paramsv![],
)
.await?;
sql.execute(
"INSERT INTO contacts (id,name,origin) VALUES \
(1,'self',262144), (2,'info',262144), (3,'rsvd',262144), \
(4,'rsvd',262144), (5,'device',262144), (6,'rsvd',262144), \
(7,'rsvd',262144), (8,'rsvd',262144), (9,'rsvd',262144);",
paramsv![],
)
.await?;
sql.execute(
"CREATE TABLE chats (\
id INTEGER PRIMARY KEY AUTOINCREMENT, \
type INTEGER DEFAULT 0, \
name TEXT DEFAULT '', \
draft_timestamp INTEGER DEFAULT 0, \
draft_txt TEXT DEFAULT '', \
blocked INTEGER DEFAULT 0, \
grpid TEXT DEFAULT '', \
param TEXT DEFAULT '');",
paramsv![],
)
.await?;
sql.execute("CREATE INDEX chats_index1 ON chats (grpid);", paramsv![])
sql.set_raw_config_int(context, "dbversion", dbversion_before_update)
.await?;
sql.execute(
"CREATE TABLE chats_contacts (chat_id INTEGER, contact_id INTEGER);",
paramsv![],
)
.await?;
sql.execute(
"CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);",
paramsv![],
)
.await?;
sql.execute(
"INSERT INTO chats (id,type,name) VALUES \
(1,120,'deaddrop'), (2,120,'rsvd'), (3,120,'trash'), \
(4,120,'msgs_in_creation'), (5,120,'starred'), (6,120,'archivedlink'), \
(7,100,'rsvd'), (8,100,'rsvd'), (9,100,'rsvd');",
paramsv![],
)
.await?;
sql.execute(
"CREATE TABLE msgs (\
id INTEGER PRIMARY KEY AUTOINCREMENT, \
rfc724_mid TEXT DEFAULT '', \
server_folder TEXT DEFAULT '', \
server_uid INTEGER DEFAULT 0, \
chat_id INTEGER DEFAULT 0, \
from_id INTEGER DEFAULT 0, \
to_id INTEGER DEFAULT 0, \
timestamp INTEGER DEFAULT 0, \
type INTEGER DEFAULT 0, \
state INTEGER DEFAULT 0, \
msgrmsg INTEGER DEFAULT 1, \
bytes INTEGER DEFAULT 0, \
txt TEXT DEFAULT '', \
txt_raw TEXT DEFAULT '', \
param TEXT DEFAULT '');",
paramsv![],
)
.await?;
sql.execute("CREATE INDEX msgs_index1 ON msgs (rfc724_mid);", paramsv![])
.await?;
sql.execute("CREATE INDEX msgs_index2 ON msgs (chat_id);", paramsv![])
.await?;
sql.execute("CREATE INDEX msgs_index3 ON msgs (timestamp);", paramsv![])
.await?;
sql.execute("CREATE INDEX msgs_index4 ON msgs (state);", paramsv![])
.await?;
sql.execute(
"INSERT INTO msgs (id,msgrmsg,txt) VALUES \
(1,0,'marker1'), (2,0,'rsvd'), (3,0,'rsvd'), \
(4,0,'rsvd'), (5,0,'rsvd'), (6,0,'rsvd'), (7,0,'rsvd'), \
(8,0,'rsvd'), (9,0,'daymarker');",
paramsv![],
)
.await?;
sql.execute(
"CREATE TABLE jobs (\
id INTEGER PRIMARY KEY AUTOINCREMENT, \
added_timestamp INTEGER, \
desired_timestamp INTEGER DEFAULT 0, \
action INTEGER, \
foreign_id INTEGER, \
param TEXT DEFAULT '');",
paramsv![],
)
.await?;
sql.execute(
"CREATE INDEX jobs_index1 ON jobs (desired_timestamp);",
paramsv![],
)
.await?;
if !sql.table_exists("config").await?
|| !sql.table_exists("contacts").await?
|| !sql.table_exists("chats").await?
|| !sql.table_exists("chats_contacts").await?
|| !sql.table_exists("msgs").await?
|| !sql.table_exists("jobs").await?
{
error!(
context,
"Cannot create tables in new database \"{:?}\".",
dbfile.as_ref(),
);
// cannot create the tables - maybe we cannot write?
return Err(Error::SqlFailedToOpen.into());
} else {
sql.set_raw_config_int(context, "dbversion", 0).await?;
}
} else {
exists_before_update = true;
dbversion_before_update = sql
@@ -858,7 +925,7 @@ async fn open(
let mut dbversion = dbversion_before_update;
let mut recalc_fingerprints = false;
let mut update_icons = false;
let mut update_icons = !exists_before_update;
if dbversion < 1 {
info!(context, "[migration] v1");
@@ -872,7 +939,6 @@ async fn open(
paramsv![],
)
.await?;
dbversion = 1;
sql.set_raw_config_int(context, "dbversion", 1).await?;
}
if dbversion < 2 {
@@ -882,7 +948,6 @@ async fn open(
paramsv![],
)
.await?;
dbversion = 2;
sql.set_raw_config_int(context, "dbversion", 2).await?;
}
if dbversion < 7 {
@@ -898,7 +963,6 @@ async fn open(
paramsv![],
)
.await?;
dbversion = 7;
sql.set_raw_config_int(context, "dbversion", 7).await?;
}
if dbversion < 10 {
@@ -919,7 +983,6 @@ async fn open(
paramsv![],
)
.await?;
dbversion = 10;
sql.set_raw_config_int(context, "dbversion", 10).await?;
}
if dbversion < 12 {
@@ -934,7 +997,6 @@ async fn open(
paramsv![],
)
.await?;
dbversion = 12;
sql.set_raw_config_int(context, "dbversion", 12).await?;
}
if dbversion < 17 {
@@ -946,6 +1008,8 @@ async fn open(
.await?;
sql.execute("CREATE INDEX chats_index2 ON chats (archived);", paramsv![])
.await?;
// 'starred' column is not used currently
// (dropping is not easily doable and stop adding it will make reusing it complicated)
sql.execute(
"ALTER TABLE msgs ADD COLUMN starred INTEGER DEFAULT 0;",
paramsv![],
@@ -953,7 +1017,6 @@ async fn open(
.await?;
sql.execute("CREATE INDEX msgs_index5 ON msgs (starred);", paramsv![])
.await?;
dbversion = 17;
sql.set_raw_config_int(context, "dbversion", 17).await?;
}
if dbversion < 18 {
@@ -968,7 +1031,6 @@ async fn open(
paramsv![],
)
.await?;
dbversion = 18;
sql.set_raw_config_int(context, "dbversion", 18).await?;
}
if dbversion < 27 {
@@ -992,7 +1054,6 @@ async fn open(
paramsv![],
)
.await?;
dbversion = 27;
sql.set_raw_config_int(context, "dbversion", 27).await?;
}
if dbversion < 34 {
@@ -1028,7 +1089,6 @@ async fn open(
)
.await?;
recalc_fingerprints = true;
dbversion = 34;
sql.set_raw_config_int(context, "dbversion", 34).await?;
}
if dbversion < 39 {
@@ -1052,7 +1112,6 @@ async fn open(
paramsv![],
)
.await?;
dbversion = 39;
sql.set_raw_config_int(context, "dbversion", 39).await?;
}
if dbversion < 40 {
@@ -1062,14 +1121,12 @@ async fn open(
paramsv![],
)
.await?;
dbversion = 40;
sql.set_raw_config_int(context, "dbversion", 40).await?;
}
if dbversion < 44 {
info!(context, "[migration] v44");
sql.execute("ALTER TABLE msgs ADD COLUMN mime_headers TEXT;", paramsv![])
.await?;
dbversion = 44;
sql.set_raw_config_int(context, "dbversion", 44).await?;
}
if dbversion < 46 {
@@ -1094,7 +1151,6 @@ async fn open(
paramsv![],
)
.await?;
dbversion = 47;
sql.set_raw_config_int(context, "dbversion", 47).await?;
}
if dbversion < 48 {
@@ -1105,8 +1161,6 @@ async fn open(
paramsv![],
)
.await?;
dbversion = 48;
sql.set_raw_config_int(context, "dbversion", 48).await?;
}
if dbversion < 49 {
@@ -1116,7 +1170,6 @@ async fn open(
paramsv![],
)
.await?;
dbversion = 49;
sql.set_raw_config_int(context, "dbversion", 49).await?;
}
if dbversion < 50 {
@@ -1128,7 +1181,6 @@ async fn open(
sql.set_raw_config_int(context, "show_emails", ShowEmails::All as i32)
.await?;
}
dbversion = 50;
sql.set_raw_config_int(context, "dbversion", 50).await?;
}
if dbversion < 53 {
@@ -1169,7 +1221,6 @@ async fn open(
paramsv![],
)
.await?;
dbversion = 53;
sql.set_raw_config_int(context, "dbversion", 53).await?;
}
if dbversion < 54 {
@@ -1184,7 +1235,6 @@ async fn open(
paramsv![],
)
.await?;
dbversion = 54;
sql.set_raw_config_int(context, "dbversion", 54).await?;
}
if dbversion < 55 {
@@ -1264,18 +1314,11 @@ async fn open(
paramsv![],
)
.await?;
// Timer value in seconds. For incoming messages this
// timer starts when message is read, so we want to have
// the value stored here until the timer starts.
sql.execute(
"ALTER TABLE msgs ADD COLUMN ephemeral_timer INTEGER DEFAULT 0",
paramsv![],
)
.await?;
// Timestamp indicating when the message should be
// deleted. It is convenient to store it here because UI
// needs this value to display how much time is left until
// the message is deleted.
sql.execute(
"ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0",
paramsv![],
@@ -1288,6 +1331,57 @@ async fn open(
update_icons = true;
sql.set_raw_config_int(context, "dbversion", 66).await?;
}
if dbversion < 67 {
info!(context, "[migration] v67");
for prefix in &["", "configured_"] {
if let Some(server_flags) = sql
.get_raw_config_int(context, format!("{}server_flags", prefix))
.await
{
let imap_socket_flags = server_flags & 0x700;
let key = format!("{}mail_security", prefix);
match imap_socket_flags {
0x100 => sql.set_raw_config_int(context, key, 2).await?, // STARTTLS
0x200 => sql.set_raw_config_int(context, key, 1).await?, // SSL/TLS
0x400 => sql.set_raw_config_int(context, key, 3).await?, // Plain
_ => sql.set_raw_config_int(context, key, 0).await?,
}
let smtp_socket_flags = server_flags & 0x70000;
let key = format!("{}send_security", prefix);
match smtp_socket_flags {
0x10000 => sql.set_raw_config_int(context, key, 2).await?, // STARTTLS
0x20000 => sql.set_raw_config_int(context, key, 1).await?, // SSL/TLS
0x40000 => sql.set_raw_config_int(context, key, 3).await?, // Plain
_ => sql.set_raw_config_int(context, key, 0).await?,
}
}
}
sql.set_raw_config_int(context, "dbversion", 67).await?;
}
if dbversion < 68 {
info!(context, "[migration] v68");
// the index is used to speed up get_fresh_msg_cnt() (see comment there for more details) and marknoticed_chat()
sql.execute(
"CREATE INDEX IF NOT EXISTS msgs_index7 ON msgs (state, hidden, chat_id);",
paramsv![],
)
.await?;
sql.set_raw_config_int(context, "dbversion", 68).await?;
}
if dbversion < 69 {
info!(context, "[migration] v69");
sql.execute(
"ALTER TABLE chats ADD COLUMN protected INTEGER DEFAULT 0;",
paramsv![],
)
.await?;
sql.execute(
"UPDATE chats SET protected=1, type=120 WHERE type=130;", // 120=group, 130=old verified group
paramsv![],
)
.await?;
sql.set_raw_config_int(context, "dbversion", 69).await?;
}
// (2) updates that require high-level objects
// (the structure is complete now and all objects are usable)

View File

@@ -5,8 +5,8 @@ use std::borrow::Cow;
use strum::EnumProperty;
use strum_macros::EnumProperty;
use crate::blob::BlobObject;
use crate::chat;
use crate::chat::ProtectionStatus;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::contact::*;
use crate::context::Context;
@@ -14,6 +14,7 @@ use crate::error::{bail, Error};
use crate::message::Message;
use crate::param::Param;
use crate::stock::StockMessage::{DeviceMessagesHint, WelcomeMessage};
use crate::{blob::BlobObject, config::Config};
/// Stock strings
///
@@ -119,9 +120,6 @@ pub enum StockMessage {
#[strum(props(fallback = "Archived chats"))]
ArchivedChats = 40,
#[strum(props(fallback = "Starred messages"))]
StarredMsgs = 41,
#[strum(props(fallback = "Autocrypt Setup Message"))]
AcSetupMsgSubject = 42,
@@ -216,6 +214,36 @@ pub enum StockMessage {
#[strum(props(fallback = "You are invited to a video chat, click %1$s to join."))]
VideochatInviteMsgBody = 83,
#[strum(props(fallback = "Error:\n\n“%1$s”"))]
ConfigurationFailed = 84,
#[strum(props(
fallback = "⚠️ Date or time of your device seem to be inaccurate (%1$s).\n\n\
Adjust your clock ⏰🔧 to ensure your messages are received correctly."
))]
BadTimeMsgBody = 85,
#[strum(props(fallback = "⚠️ Your Delta Chat version might be outdated.\n\n\
This may cause problems because your chat partners use newer versions - \
and you are missing the latest features 😳\n\
Please check https://get.delta.chat or your app store for updates."))]
UpdateReminderMsgBody = 86,
#[strum(props(
fallback = "Could not find your mail server.\n\nPlease check your internet connection."
))]
ErrorNoNetwork = 87,
#[strum(props(fallback = "Chat protection enabled."))]
ProtectionEnabled = 88,
#[strum(props(fallback = "Chat protection disabled."))]
ProtectionDisabled = 89,
// used in summaries, a noun, not a verb (not: "to reply")
#[strum(props(fallback = "Reply"))]
ReplyNoun = 90,
}
/*
@@ -381,16 +409,27 @@ impl Context {
}
}
pub async fn update_device_chats(&self) -> Result<(), Error> {
// check for the LAST added device message - if it is present, we can skip message creation.
// this is worthwhile as this function is typically called
// by the ui on every probram start or even on every opening of the chatlist.
if chat::was_device_msg_ever_added(&self, "core-welcome").await? {
/// Returns a stock message saying that protection status has changed.
pub async fn stock_protection_msg(&self, protect: ProtectionStatus, from_id: u32) -> String {
self.stock_system_msg(
match protect {
ProtectionStatus::Protected => StockMessage::ProtectionEnabled,
ProtectionStatus::Unprotected => StockMessage::ProtectionDisabled,
},
"",
"",
from_id,
)
.await
}
pub(crate) async fn update_device_chats(&self) -> Result<(), Error> {
if self.get_config_bool(Config::Bot).await {
return Ok(());
}
// create saved-messages chat;
// we do this only once, if the user has deleted the chat, he can recreate it manually.
// create saved-messages chat; we do this only once, if the user has deleted the chat,
// he can recreate it manually (make sure we do not re-add it when configure() was called a second time)
if !self.sql.get_raw_config_bool(&self, "self-chat-added").await {
self.sql
.set_raw_config_bool(&self, "self-chat-added", true)

View File

@@ -2,20 +2,35 @@
//!
//! This module is only compiled for test runs.
use std::str::FromStr;
use std::time::{Duration, Instant};
use async_std::path::PathBuf;
use async_std::sync::RwLock;
use tempfile::{tempdir, TempDir};
use crate::chat;
use crate::chat::{ChatId, ChatItem};
use crate::config::Config;
use crate::context::Context;
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_tools::EmailAddress;
use crate::job::Action;
use crate::key::{self, DcKey};
use crate::message::Message;
use crate::mimeparser::MimeMessage;
use crate::param::{Param, Params};
/// A Context and temporary directory.
///
/// The temporary directory can be used to store the SQLite database,
/// see e.g. [test_context] which does this.
#[derive(Debug)]
pub(crate) struct TestContext {
pub ctx: Context,
pub dir: TempDir,
/// Counter for fake IMAP UIDs in [recv_msg], for private use in that function only.
recv_idx: RwLock<u32>,
}
impl TestContext {
@@ -35,7 +50,11 @@ impl TestContext {
let ctx = Context::new("FakeOS".into(), dbfile.into(), id)
.await
.unwrap();
Self { ctx, dir }
Self {
ctx,
dir,
recv_idx: RwLock::new(0),
}
}
/// Create a new configured [TestContext].
@@ -48,6 +67,19 @@ impl TestContext {
t
}
/// Create a new configured [TestContext].
///
/// This is a shortcut which configures bob@example.net with a fixed key.
pub async fn new_bob() -> Self {
let t = Self::new().await;
let keypair = bob_keypair();
t.configure_addr(&keypair.addr.to_string()).await;
key::store_self_keypair(&t.ctx, &keypair, key::KeyPairUse::Default)
.await
.expect("Failed to save Bob's key");
t
}
/// Configure with alice@example.com.
///
/// The context will be fake-configured as the alice user, with a pre-generated secret
@@ -76,6 +108,135 @@ impl TestContext {
.await
.unwrap();
}
/// Retrieve a sent message from the jobs table.
///
/// This retrieves and removes a message which has been scheduled to send from the jobs
/// table. Messages are returned in the order they have been sent.
///
/// Panics if there is no message or on any error.
pub async fn pop_sent_msg(&self) -> SentMessage {
let start = Instant::now();
let (rowid, foreign_id, raw_params) = loop {
let row = self
.ctx
.sql
.query_row(
r#"
SELECT id, foreign_id, param
FROM jobs
WHERE action=?
ORDER BY desired_timestamp;
"#,
paramsv![Action::SendMsgToSmtp],
|row| {
let id: i64 = row.get(0)?;
let foreign_id: i64 = row.get(1)?;
let param: String = row.get(2)?;
Ok((id, foreign_id, param))
},
)
.await;
if let Ok(row) = row {
break row;
}
if start.elapsed() < Duration::from_secs(3) {
async_std::task::sleep(Duration::from_millis(100)).await;
} else {
panic!("no sent message found in jobs table");
}
};
let id = ChatId::new(foreign_id as u32);
let params = Params::from_str(&raw_params).unwrap();
let blob_path = params
.get_blob(Param::File, &self.ctx, false)
.await
.expect("failed to parse blob from param")
.expect("no Param::File found in Params")
.to_abs_path();
self.ctx
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![rowid])
.await
.expect("failed to remove job");
SentMessage {
id,
params,
blob_path,
}
}
/// Parse a message.
///
/// Parsing a message does not run the entire receive pipeline, but is not without
/// side-effects either. E.g. if the message includes autocrypt headers the relevant
/// peerstates will be updated. Later receiving the message using [recv_msg] is
/// unlikely to be affected as the peerstate would be processed again in exactly the
/// same way.
pub async fn parse_msg(&self, msg: &SentMessage) -> MimeMessage {
MimeMessage::from_bytes(&self.ctx, msg.payload().as_bytes())
.await
.unwrap()
}
/// Receive a message.
///
/// Receives a message using the `dc_receive_imf()` pipeline.
pub async fn recv_msg(&self, msg: &SentMessage) {
let mut idx = self.recv_idx.write().await;
*idx += 1;
dc_receive_imf(&self.ctx, msg.payload().as_bytes(), "INBOX", *idx, false)
.await
.unwrap();
}
/// Get the most recent message of a chat.
///
/// Panics on errors or if the most recent message is a marker.
pub async fn get_last_msg(&self, chat_id: ChatId) -> Message {
let msgs = chat::get_chat_msgs(&self.ctx, chat_id, 0, None).await;
let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
msg_id
} else {
panic!("Wrong item type");
};
Message::load_from_db(&self.ctx, *msg_id).await.unwrap()
}
}
/// A raw message as it was scheduled to be sent.
///
/// This is a raw message, probably in the shape DC was planning to send it but not having
/// passed through a SMTP-IMAP pipeline.
#[derive(Debug, Clone)]
pub struct SentMessage {
id: ChatId,
params: Params,
blob_path: PathBuf,
}
impl SentMessage {
/// The ChatId the message belonged to.
pub fn id(&self) -> ChatId {
self.id
}
/// A recipient the message was destined for.
///
/// If there are multiple recipients this is just a random one, so is not very useful.
pub fn recipient(&self) -> EmailAddress {
let raw = self
.params
.get(Param::Recipients)
.expect("no recipients in params");
let rcpt = raw.split(' ').next().expect("no recipient found");
rcpt.parse().expect("failed to parse email address")
}
/// The raw message payload.
pub fn payload(&self) -> String {
std::fs::read_to_string(&self.blob_path).unwrap()
}
}
/// Load a pre-generated keypair for alice@example.com from disk.

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<clientConfig version="1.1">
<emailProvider id="lakenet.ch">
<domain>%EMAILDOMAIN%</domain>
<displayName>%EMAILDOMAIN% Mail</displayName>
<displayShortName>%EMAILDOMAIN%</displayShortName>
<incomingServer type="imap">
<hostname>mail.lakenet.ch</hostname>
<port>993</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<incomingServer type="imap">
<hostname>mail.lakenet.ch</hostname>
<port>143</port>
<socketType>STARTTLS</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<incomingServer type="pop3">
<hostname>mail.lakenet.ch</hostname>
<port>995</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<incomingServer type="pop3">
<hostname>mail.lakenet.ch</hostname>
<port>110</port>
<socketType>STARTTLS</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<outgoingServer type="smtp">
<hostname>mail.lakenet.ch</hostname>
<port>587</port>
<socketType>STARTTLS</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</outgoingServer>
<documentation url="https://www.lakenet.ch/">
<descr lang="it">Impostazioni per le e-mail LakeNet</descr>
<descr lang="fr">Reglages pour le courriel e-mail LakeNet</descr>
<descr lang="en">Settings for LakeNet's e-mail accounts</descr>
</documentation>
</emailProvider>
<webMail>
<loginPage url="https://lakenet.ch/webmail/" />
<loginPageInfo url="https://lakenet.ch/webmail/">
<username>%EMAILADDRESS%</username>
<usernameField id="rcmloginuser" name="_user"/>
<passwordField id="rcmloginpwd" name="_pass"/>
<loginButton id="rcmloginsubmit"/>
</loginPageInfo>
</webMail>
<clientConfigUpdate url="https://lakenet.ch/.well-known/autoconfig/mail/config-v1.1.xml" />
</clientConfig>

View File

@@ -0,0 +1,71 @@
<clientConfig version="1.1">
<!-- Retrieved from https://autoconfig.thunderbird.net/v1.1/outlook.com on 2019-10-11 -->
<emailProvider id="outlook.com">
<domain>hotmail.com</domain>
<domain>hotmail.co.uk</domain>
<domain>hotmail.co.jp</domain>
<domain>hotmail.com.br</domain>
<domain>hotmail.de</domain>
<domain>hotmail.fr</domain>
<domain>hotmail.it</domain>
<domain>hotmail.es</domain>
<domain>live.com</domain>
<domain>live.co.uk</domain>
<domain>live.co.jp</domain>
<domain>live.de</domain>
<domain>live.fr</domain>
<domain>live.it</domain>
<domain>live.jp</domain>
<domain>msn.com</domain>
<domain>outlook.com</domain>
<displayName>Outlook.com (Microsoft)</displayName>
<displayShortName>Outlook</displayShortName>
<incomingServer type="exchange">
<hostname>outlook.office365.com</hostname>
<port>443</port>
<username>%EMAILADDRESS%</username>
<socketType>SSL</socketType>
<authentication>OAuth2</authentication>
<owaURL>https://outlook.office365.com/owa/</owaURL>
<ewsURL>https://outlook.office365.com/ews/exchange.asmx</ewsURL>
<useGlobalPreferredServer>true</useGlobalPreferredServer>
</incomingServer>
<incomingServer type="imap">
<hostname>outlook.office365.com</hostname>
<port>993</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<incomingServer type="pop3">
<hostname>outlook.office365.com</hostname>
<port>995</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
<pop3>
<leaveMessagesOnServer>true</leaveMessagesOnServer>
<!-- Outlook.com docs specifically mention that POP3 deletes have effect on the main inbox on webmail and IMAP -->
</pop3>
</incomingServer>
<outgoingServer type="smtp">
<hostname>smtp.office365.com</hostname>
<port>587</port>
<socketType>STARTTLS</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</outgoingServer>
<documentation url="http://windows.microsoft.com/en-US/windows/outlook/send-receive-from-app">
<descr lang="en">Set up an email app with Outlook.com</descr>
</documentation>
</emailProvider>
<webMail>
<loginPage url="https://www.outlook.com/"/>
<loginPageInfo url="https://www.outlook.com/">
<username>%EMAILADDRESS%</username>
<usernameField id="i0116" name="login"/>
<passwordField id="i0118" name="passwd"/>
<loginButton id="idSIButton9" name="SI"/>
</loginPageInfo>
</webMail>
</clientConfig>";

View File

@@ -0,0 +1,114 @@
Return-Path: <camping@otherdomain.eu>
X-Original-To: alice@example.com
Delivered-To: m045a7e8@dd37930.kasserver.com
Received: from mout.kundenserver.de (mout.kundenserver.de [212.227.126.131])
by dd37930.kasserver.com (Postfix) with ESMTPS id 271F34B4258C
for <holger@somedomain.de>; Thu, 6 Aug 2020 18:40:32 +0200 (CEST)
Received: from oxbsltgw18.schlund.de ([172.19.249.35]) by
mrelayeu.kundenserver.de (mreue009 [213.165.67.103]) with ESMTPSA (Nemesis)
id 1MpDRv-1kW93Y0lGZ-00qjvh for <holger@somedomain.de>; Thu, 06 Aug 2020
18:40:31 +0200
Date: Thu, 6 Aug 2020 18:40:30 +0200 (CEST)
From: Camping <camping@otherdomain.eu>
Reply-To: Camping <camping@otherdomain.eu>
To: Alice <alice@example.com>
Message-ID: <512278196.1287440.1596732031020@email.ionos.fr>
Subject: Re: subj?
MIME-Version: 1.0
Content-Type: multipart/related;
boundary="----=_Part_1287438_2124736777.1596732031007"
X-Mailer: Open-Xchange Mailer v7.10.1-Rev32
X-Originating-Client: open-xchange-appsuite
------=_Part_1287438_2124736777.1596732031007
MIME-Version: 1.0
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
<!doctype html>
<html>
<head>=20
<meta charset=3D"UTF-8">=20
</head>
<body>
<div class=3D"default-style">
Guten Abend,
<br>
</div>
<div class=3D"default-style">
<br>
</div>
<div class=3D"default-style">
Lots of text
<br>
</div>
<div class=3D"default-style">
text with Umlaut =C3=A4...
<br>
</div>
<div class=3D"default-style">
MfG
<br>
</div>
<div class=3D"default-style">
<br>
</div>
<div class=3D"io-ox-signature">
<p>--------------------------------------<br></p>
<p style=3D"text-align: left;"><a href=3D"https://example.com=
/">Camping </a><br></p>
<p>someaddress<br></p>
<p>sometown</p>
<p><img alt=3D"" class=3D"aspect-ratio" style=3D"width: 505px; height: 1=
68px; max-width: 100%;" id=3D"d5cd260f-1b1f-4bfa-81d7-c0a74923b979" src=3D"=
cid:d5cd260f-1b1f-4bfa-81d7-c0a74923b979" width=3D"505" height=3D"168"><br>=
</p>
</div>
<blockquote type=3D"cite">
<div>
Le 5 ao=C3=BBt 2020 =C3=A0 10:46, holger &lt;
<a href=3D"mailto:holger@somedomain.de">holger@somedomain.de</a>&gt; a =
=C3=A9crit&nbsp;:
</div>
<div>
<br>
</div>
<div>
<br>
</div>
<div>
Bonjour,
</div>
<div>
<br>
</div>
<div>
<br>
</div>
<div>
<br>
</div>
<div>
--
</div>
<div>
Sent with my Delta Chat Messenger:=20
<a href=3D"https://delta.chat" rel=3D"noopener" target=3D"_blank">https=
://delta.chat</a>
<br>
</div>
</blockquote>=20
</body>
</html>
------=_Part_1287438_2124736777.1596732031007
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-ID: <d5cd260f-1b1f-4bfa-81d7-c0a74923b979>
Content-Disposition: inline
iVBORw0KGgoAAAANSUhEUgAAAlgAAADICAYAAAA0n5+2AAAgAElEQVR4nCzaZ3sjhIGo7fyMPSch
MN29S7YsW71avfcuWZJlS3K33HuvY08fpjEVhoGhDh0CSSAESEhCgDRSKNns5mx2k8AM5Dznw/v+
[scrubbed]
/kNZ/B9Uub32fzngSwAAAABJRU5ErkJggg==
------=_Part_1287438_2124736777.1596732031007--

View File

@@ -1,117 +0,0 @@
//! Stress some functions for testing; if used as a lib, this file is obsolete.
use deltachat::config;
use deltachat::context::*;
use tempfile::{tempdir, TempDir};
/* some data used for testing
******************************************************************************/
async fn stress_functions(context: &Context) {
let res = context
.get_config(config::Config::SysConfigKeys)
.await
.unwrap();
assert!(!res.contains(" probably_never_a_key "));
assert!(res.contains(" addr "));
assert!(res.contains(" mail_server "));
assert!(res.contains(" mail_user "));
assert!(res.contains(" mail_pw "));
assert!(res.contains(" mail_port "));
assert!(res.contains(" send_server "));
assert!(res.contains(" send_user "));
assert!(res.contains(" send_pw "));
assert!(res.contains(" send_port "));
assert!(res.contains(" server_flags "));
assert!(res.contains(" imap_folder "));
assert!(res.contains(" displayname "));
assert!(res.contains(" selfstatus "));
assert!(res.contains(" selfavatar "));
assert!(res.contains(" e2ee_enabled "));
assert!(res.contains(" mdns_enabled "));
assert!(res.contains(" save_mime_headers "));
assert!(res.contains(" configured_addr "));
assert!(res.contains(" configured_mail_server "));
assert!(res.contains(" configured_mail_user "));
assert!(res.contains(" configured_mail_pw "));
assert!(res.contains(" configured_mail_port "));
assert!(res.contains(" configured_send_server "));
assert!(res.contains(" configured_send_user "));
assert!(res.contains(" configured_send_pw "));
assert!(res.contains(" configured_send_port "));
assert!(res.contains(" configured_server_flags "));
// Cant check, no configured context
// assert!(dc_is_configured(context) != 0, "Missing configured context");
// let setupcode = dc_create_setup_code(context);
// let setupcode_c = CString::new(setupcode.clone()).unwrap();
// let setupfile = dc_render_setup_file(context, &setupcode).unwrap();
// let setupfile_c = CString::new(setupfile).unwrap();
// let mut headerline_2: *const libc::c_char = ptr::null();
// let payload = dc_decrypt_setup_file(context, setupcode_c.as_ptr(), setupfile_c.as_ptr());
// assert!(payload.is_null());
// assert!(!dc_split_armored_data(
// payload,
// &mut headerline_2,
// ptr::null_mut(),
// ptr::null_mut(),
// ptr::null_mut(),
// ));
// assert!(!headerline_2.is_null());
// assert_eq!(
// strcmp(
// headerline_2,
// b"-----BEGIN PGP PRIVATE KEY BLOCK-----\x00" as *const u8 as *const libc::c_char,
// ),
// 0
// );
// free(payload as *mut libc::c_void);
// Cant check, no configured context
// assert!(dc_is_configured(context) != 0, "missing configured context");
// let qr = dc_get_securejoin_qr(context, 0);
// assert!(!qr.is_null(), "Invalid qr code generated");
// let qr_r = as_str(qr);
// assert!(qr_r.len() > 55);
// assert!(qr_r.starts_with("OPENPGP4FPR:"));
// let res = dc_check_qr(context, qr);
// let s = res.get_state();
// assert!(
// s == QrState::AskVerifyContact
// || s == QrState::FprMissmatch
// || s == QrState::FprWithoutAddr
// );
// free(qr.cast());
}
#[allow(dead_code)]
struct TestContext {
ctx: Context,
dir: TempDir,
}
async fn create_test_context() -> TestContext {
use rand::Rng;
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let id = rand::thread_rng().gen();
let ctx = Context::new("FakeOs".into(), dbfile.into(), id)
.await
.unwrap();
TestContext { ctx, dir }
}
#[async_std::test]
async fn test_stress_tests() {
let context = create_test_context().await;
stress_functions(&context.ctx).await;
}