Compare commits

..

240 Commits
1.34.0 ... sqlx

Author SHA1 Message Date
dignifiedquire
df546b9d2e more type fixes 2020-06-24 12:57:39 +02:00
dignifiedquire
5c13d2322a improve some typehints 2020-06-24 11:54:43 +02:00
dignifiedquire
2d5caf9d3e fix compilation 2020-06-24 11:06:26 +02:00
dignifiedquire
876e3ed58e update deps 2020-06-24 10:55:31 +02:00
dignifiedquire
cdb5f0d536 refactor(sql): switch execute to sqlx 2020-06-24 10:54:51 +02:00
dignifiedquire
0d791bb6b3 feat: start preparations for sqlx, split out migrations 2020-06-24 10:30:34 +02:00
bjoern
9152f93a46 Merge pull request #1668 from deltachat/add-sanitise-tests
add more tests for BlobObject::sanitise_name()
2020-06-23 19:04:51 +02:00
bjoern
6a4b6fddac Merge pull request #1659 from deltachat/prep-1.39
prepare 1.39
2020-06-23 19:01:36 +02:00
B. Petersen
e3c90aff22 add more tests for BlobObject::sanitise_name() 2020-06-23 18:51:40 +02:00
B. Petersen
b7464f7a5c bump version to 1.39 2020-06-23 18:41:45 +02:00
B. Petersen
53128cc64b update changelog for 1.39 and fix the one for 1.36 2020-06-23 18:41:45 +02:00
bjoern
ccf8eeacd6 Merge pull request #1667 from deltachat/revert-1664-sanitize-filename-reader-friendly
Revert "Switch to sanitize-filename-reader-friendly"
2020-06-23 18:40:40 +02:00
bjoern
aeb8a2e260 Revert "Switch to sanitize-filename-reader-friendly"
This reverts commit 93797bc82f.
2020-06-23 18:11:18 +02:00
Alexander Krotov
93797bc82f Switch to sanitize-filename-reader-friendly
One advantage is that it does not depend on any crates, so there is a
higher chance of dropping regex crate eventually.
2020-06-23 14:37:35 +03:00
holger krekel
07236efc45 fix bcc_self to remain "0" for testrun/fivechat test accounts 2020-06-23 08:06:32 +02:00
B. Petersen
0fbddc939b update provider-db 2020-06-23 08:06:32 +02:00
Alexander Krotov
a031151587 Fix two +nightly clippy suggestions 2020-06-23 03:17:07 +03:00
B. Petersen
545ff4f7ba apply config_defaults only for unset values
instead of applying all config_defaults unconditionally
after the first configure, a config_defaults for a given key
is now applied when this key has never been set before.

this way, you can set some keys before calling configure()
and also, later versions can add new defaults for new keys
(that would require another call to configure() then, however)
2020-06-23 00:48:40 +02:00
B. Petersen
73e695537a add missing doc for bcc_self 2020-06-22 22:49:26 +02:00
B. Petersen
16e3c113b7 update ffi docs; avatar is sent with messages since end 2019 2020-06-22 22:49:26 +02:00
bjoern
88d7bf49ff Merge pull request #1658 from deltachat/remove-config-changes-interrupt
remove config changes interrupt
2020-06-22 14:18:48 +02:00
B. Petersen
74ea884aa4 remove now superfluous interrupting on watch-settings-changes 2020-06-22 13:48:33 +02:00
B. Petersen
16c53637d9 update docs to new watch-settings behavior 2020-06-22 13:47:47 +02:00
Friedel Ziegelmayer
f63f0550b0 Merge pull request #1654 from deltachat/fix-move-loops
fix(scheduler): only start watch loops if the appropriate config is set
2020-06-22 12:51:43 +02:00
Friedel Ziegelmayer
530503932b Merge pull request #1655 from deltachat/feat-update-deps
feat: update deps
2020-06-22 12:23:26 +02:00
Friedel Ziegelmayer
d2dc4edd82 Merge pull request #1657 from deltachat/fix-move-loops-inbox
perform jobs also if inbox_watch disabled
2020-06-22 12:23:10 +02:00
Alexander Krotov
8de1bc6cbd sql: fix potential panic in maybe_add_file
When maybe_add_file was called with "$BLOBDIR" it tried to remove 9
bytes from the string, while it only contains 8.
2020-06-22 13:22:37 +03:00
B. Petersen
76e39bfa7c this pr creates the inbox_loop inpendendingly of inbox_watch config-setting as the loop is also required to perform jobs. the config-setting is checked inside the loop then 2020-06-22 12:00:25 +02:00
Alexander Krotov
cf09942737 deltachat-ffi: use as_deref() as suggested by clippy 2020-06-22 12:37:29 +03:00
dignifiedquire
6fe1f01c5f feat: update deps
includes async-std@1.6.2 and smol@0.1.18 which fix various  hangs and possible deadlocks
2020-06-22 10:04:10 +02:00
dignifiedquire
f880d6188b fix(scheduler): only start watch loops if the appropriate config is set 2020-06-22 10:00:50 +02:00
bjoern
22c62ea6af Merge pull request #1649 from deltachat/freepascal
README: add link to Free Pascal bindings
2020-06-21 21:52:51 +02:00
Alexander Krotov
e3af3a24a8 README: add link to Free Pascal bindings 2020-06-21 20:14:30 +03:00
Alexander Krotov
7bfadb14ea Fix a typo (prover -> provider) 2020-06-21 20:13:06 +03:00
bjoern
75d20b899a Merge pull request #1646 from deltachat/prep-1.38
prepare 1.38
2020-06-21 12:12:02 +02:00
B. Petersen
31a5811241 bump version to 1.38 2020-06-20 19:16:07 +02:00
B. Petersen
cd1f5bf229 update changelog for 1.38 2020-06-20 19:09:33 +02:00
bjoern
632fc19f41 Merge pull request #1645 from deltachat/correct-seen
give correct "fresh" flag to calc_sort_timestamp()
2020-06-20 19:05:34 +02:00
B. Petersen
7ad95ea165 give correct "fresh" flag to calc_sort_timestamp()
for "fresh" messages, calc_sort_timestamp() makes sure,
the sorting timestamp is not less than the last-non-fresh message.

this commit fixes the "fresh" state we give to calc_sort_timestamp(),
it just uses the message-state we've already calculated.

the old assumption, that unseen messages are always fresh
is wrong as this "seen" flag just comes from an imap-flag
that is not set for self-sent messages.

therefore, in a multi-device-setup, things are totally messed up.
(the bug was probably in there since a long time,
however did not came to light until the async-move)
2020-06-20 18:36:09 +02:00
Floris Bruynooghe
9d7b756ddb Unify some testing interfaces
This tidies up our testing tools a little bit.  We had several
functions which through various changes ended up doing the same and
some more which did very similar stuff, so I merged them to have
things simpler.  Also moved towards methods on the TestContext struct
while cleaning this up anyway, seems like this structure is going to
stay around for a bit anyway.

The intersting change is in `test_utils.rs`, everything else is just
updating callers.  A few tests used example.org which I moved to
example.com to be able to re-use more configuration of the test
context.
2020-06-20 14:37:41 +02:00
bjoern
73412db267 Merge pull request #1644 from deltachat/stop-time-in-repl
print time needed to build chatlist in repl tool
2020-06-20 13:07:50 +02:00
B. Petersen
059a7bcd7f print time needed to build chatlist in repl tool
the chatlist is the most complicated list to get from sql
and is also the most used list,
so it makes sense to keep an eye on the timing of that.
2020-06-20 00:56:43 +02:00
bjoern
3e47564b2f Merge pull request #1643 from deltachat/prep-1.37
prepare 1.37
2020-06-19 21:54:04 +02:00
B. Petersen
d8be0cdf35 bump version to 1.37 2020-06-19 21:32:47 +02:00
B. Petersen
26a44b6d32 update changelog for 1.37 2020-06-19 21:31:34 +02:00
bjoern
12eacaae36 Merge pull request #1641 from deltachat/oauth2-provider-db
get Oauth2-information from provider-db
2020-06-19 21:13:36 +02:00
B. Petersen
2d8148a1a3 make use of new oauth2-authorizer information in the provider-db 2020-06-19 17:06:31 +02:00
B. Petersen
916007ed2d run update.py 2020-06-19 16:38:03 +02:00
B. Petersen
b91b88e11b let update.py add information of oauth2-authorizer 2020-06-19 16:37:43 +02:00
bjoern
b6c0f44608 Merge pull request #1635 from deltachat/fix-chatlist-hidden
fix getting last message for chatlist, avoid empty summaries
2020-06-19 13:36:08 +02:00
Alexander Krotov
2a623541d7 configure/mod.rs: forbid indexing and slicing 2020-06-19 14:24:53 +03:00
Alexander Krotov
0007e93e80 scheduler: forbid indexing and slicing 2020-06-19 14:24:53 +03:00
Alexander Krotov
c655fd8a64 contact: forbid indexing and slicing 2020-06-19 14:24:53 +03:00
Alexander Krotov
ad531876fd contact: simplify name normalization
This removes one indexing operation and reduces surprises when comma
means something other than first name and last name separator.
2020-06-19 14:24:53 +03:00
Alexander Krotov
53bee68acb smtp/mod.rs: forbid indexing and slicing 2020-06-19 00:56:01 +03:00
Alexander Krotov
b5400cf551 Refactor imap/mod.rs to avoid indexing
Also replace assert! with debug_assert!
2020-06-19 00:56:01 +03:00
B. Petersen
491af1b583 fix getting last message for chatlist
the last message shown in a chatlist
is the one with the largets timestamp that is not hidden.

in the past, we calcualted the last timestamp using a subquery
and uses that timestamp to finally get the message.
this may fail when there are two messages with the same max. timestamp.

with this fix, we return the id from the subquery and use that
(the subquery already filters by hidden etc.)

in practise, by time-smearing,
usually delta-chat avoids messages from the same device
having the same timestamp - however, this may not be true for multi-device
and/or read-receipts.

i have not seen this error all the years, however, it happens with
the async move several times - maybe because things are just sent faster
and things become more probabe.
2020-06-18 14:46:04 +02:00
bjoern
5b1d06cb28 Merge pull request #1634 from deltachat/typo-faild
fix typo
2020-06-18 03:02:23 +02:00
B. Petersen
7df5195d77 fix typo 2020-06-18 00:31:40 +02:00
dignifiedquire
baff13ecab fix warnings and bugs, noticed on nightly 2020-06-17 19:27:27 +02:00
bjoern
a7bf05bebb Merge pull request #1629 from deltachat/prep-1.36
prepare 1.36
2020-06-17 16:06:21 +02:00
B. Petersen
aa9b5da1c0 bump version to 1.36 2020-06-17 15:45:38 +02:00
B. Petersen
dfd705f9c6 update changelog for 1.36.0 2020-06-17 15:45:37 +02:00
bjoern
472c0bcea5 Merge pull request #1631 from deltachat/fix-securejoin
s/fingerprint/fingerprint.hex()/
2020-06-17 15:45:11 +02:00
Hocuri
8c2af132c8 Sync heuristically_parse_ndn() and maybe_ndn in prefetch_should_download() 2020-06-17 12:42:40 +02:00
Hocuri
79145576ab s/fingerprint/fingerpring.hex()/ 2020-06-17 11:08:38 +02:00
Hocuri
8ca55b0f60 clippy 2020-06-17 10:58:27 +02:00
Hocuri
74cb4ca1cd Check for mime_parser.has_chat_version() instead of is_dc_message != MessengerMessage::No and avoid passing is_dc_message around, this will save us output &mut argument and simplify the logic. 2020-06-17 10:58:27 +02:00
Hocuri
351e5dc6f3 Add Python test 2020-06-17 10:58:27 +02:00
Hocuri
4eee4a08e7 Mark read receipts as read 2020-06-17 10:58:27 +02:00
Maykel Moya
b5fa0f8924 Add support for G Suite domains
Do a lookup based on domain's MX servers. G Suite domains are expected
to have at least 'aspmx.l.google.com' listed in MXs.

See https://support.google.com/a/answer/140034

fixes #1425
2020-06-17 11:50:46 +03:00
Alexander Krotov
baba91c054 pgp: refactor and document pk_decrypt()
Avoid unnecessary indexing, decompress only once and check if the message
is Signed before trying to verify it.
2020-06-17 11:48:29 +03:00
Hocuri
40c9c2752b Parse ndns from Tiscali 2020-06-17 10:39:05 +02:00
bjoern
f4a1a526f5 Merge pull request #1628 from deltachat/lto
Re-enable lto=true for release builds
2020-06-16 22:58:54 +02:00
Alexander Krotov
7d80179ed1 Re-enable lto=true for release builds 2020-06-16 23:12:07 +03:00
bjoern
71080ed6d5 Merge pull request #1620 from deltachat/update-docs
update docs
2020-06-16 18:41:00 +02:00
bjoern
44037dd711 Update deltachat-ffi/deltachat.h
Co-authored-by: Hocuri <hocuri@gmx.de>
2020-06-15 23:36:22 +02:00
B. Petersen
bc275d8670 update docs 2020-06-15 23:36:22 +02:00
Hocuri
eb29f9c4c1 Parse testrun NDNs 2020-06-15 16:20:23 +02:00
bjoern
6340b278d9 Merge pull request #1619 from deltachat/rotate-images
respect image orientation from exif on recoding
2020-06-15 11:37:31 +02:00
B. Petersen
519e1c1cd0 warn about unused orientation values, add a comment about the orientation values 2020-06-15 02:18:48 +02:00
B. Petersen
d2320394ca convert exif orientation to desired pixel rotation 2020-06-15 01:13:37 +02:00
B. Petersen
9307f2d49f rotate image pixels, prototype a function to get exif data 2020-06-15 01:13:37 +02:00
B. Petersen
7362941245 add kamadak-exif crate 2020-06-15 00:32:13 +02:00
Alexander Krotov
f7c7f414ed refactor: remove .unwrap() from Peerstate.has_verified_key() 2020-06-15 00:47:25 +03:00
Hocuri
23d6012c1f Start parsing ndns (#1552)
Fix  #1478

I changed my original plans a little because I had so many extra ideas and then sorted that I should rather look at actual NDNs and look what is necessary to parse them:

- Recognize NDNs by ~the sender address, which is in a regex the providers database. The problem with heuristics would be that someone could send fake-NDNs and mark messages as failed.~ the standard ("report/delivery-status") and heuristics ("subject contains 'fail' and sender contains 'daemon'"). If there is a valid Message-ID, then rely on that this is an NDN (of course, generally someone might try to find out a Message-ID and send a fake NDN).
- ~Look for `In-Reply-To`~ (only Gmail did this and Gmail uses rfc822 anyway.)
- ~Look for a mimepart `message/delivery-status`, which might contain a `X-Original-Message-ID`~ (only Gmail did this and Gmail uses rfc822, too, anyway.).
- Search through the body and look for a line `Message-ID: *` (remember to remove `<`, `>`), in the hope that that's the original header
- Look for a mime-part containing the string `rfc822`, which will contain the original header. Parse them with Mailparse and look for `Message-ID`.
2020-06-13 17:44:29 +02:00
Hocuri
15b30ceed1 check for sender mailer-daemon as link2xt proposed 2020-06-13 17:29:38 +02:00
Hocuri
45b871f76d Look at From instead of Subject and ContentType in prefetch_should_download. 2020-06-13 14:18:16 +02:00
Hocuri
9f1112833f let prefetch_should_download() check if it might be an ndn 2020-06-13 12:06:30 +02:00
B. Petersen
fc88bff32f make clippy happy 2020-06-13 10:18:18 +02:00
Hocuri
bbf049e95b string 2020-06-13 10:18:18 +02:00
Hocuri
52dfa9b536 Renaming, comment 2020-06-13 10:18:18 +02:00
Hocuri
1fe85dfb3c more functional 2020-06-13 10:18:17 +02:00
Hocuri
27ff1c4a75 check in heuristically_parse_ndn() that rfc724_mid_exists() so that we do not ignore emails because we erreneously thought that it was an ndn 2020-06-13 10:18:17 +02:00
Hocuri
adf4035775 rename reports 2020-06-13 10:18:17 +02:00
Hocuri
990c80cedf lots of small fixes from the reviews 2020-06-13 10:18:17 +02:00
Hocuri
8ebce0c861 warn instead of error 2020-06-13 10:18:16 +02:00
Hocuri
ffb6a84b1f Warn instead of error 2020-06-13 10:18:16 +02:00
Hocuri
c60ec00aac Oops #2, adapt DC_STR_COUNT in deltachat.h 2020-06-13 10:18:16 +02:00
Hocuri
dd3f81a556 Oops, add FAILED_SENDING_TO to deltachat.h 2020-06-13 10:18:16 +02:00
Hocuri
8938cb2573 clippy 2020-06-13 10:18:16 +02:00
Hocuri
995660020b rm unused aol_ndn.eml (aol's ndns are very similar to these of Yahoo) 2020-06-13 10:18:15 +02:00
Hocuri
7997e7dde4 remove println 2020-06-13 10:18:15 +02:00
Hocuri
20ad98d168 typo 2020-06-13 10:18:15 +02:00
Hocuri
c827c9d209 Add yahoo test 2020-06-13 10:18:15 +02:00
Hocuri
bde97b20e9 Repair getting x-failed-recipients header, all tests passing now 2020-06-13 10:18:15 +02:00
Hocuri
777df24c75 Make the gmx test pass 2020-06-13 10:18:14 +02:00
Hocuri
e1711855cc Make the posteo test pass 2020-06-13 10:18:14 +02:00
Hocuri
3899d70b3c I hate SQL 2020-06-13 10:18:14 +02:00
Hocuri
e7aee5b4f4 add gmx and posteo tests 2020-06-13 10:18:14 +02:00
Hocuri
bd2a7a3d40 Correct failed recipient 2020-06-13 10:18:13 +02:00
Hocuri
2e59d5674e fix sql 2020-06-13 10:18:13 +02:00
Hocuri
98b5f768b6 Improve test, fixed compile errors from rebasing 2020-06-13 10:18:13 +02:00
Hocuri
b7d0f29002 Add test-data/message/gmx_ndn.eml 2020-06-13 10:18:13 +02:00
Hocuri
df9cb5e3b8 Fix error in message info 2020-06-13 10:18:12 +02:00
Hocuri
a30486112f Add test 2020-06-13 10:18:12 +02:00
Hocuri
016b96e30e Fix migration 2020-06-13 10:18:12 +02:00
Hocuri
6b763bf417 Return true for MessageState::OutMdnRcvd.can_fail() because it could be a group message and only some recipients failed 2020-06-13 10:18:12 +02:00
Hocuri
6ded0d3bc1 Do not show error messages in chat 2020-06-13 10:18:12 +02:00
Hocuri
f0837cfa73 Repair errors saved for messages 2020-06-13 10:18:11 +02:00
Hocuri
8350729cbb Improve errors 2020-06-13 10:18:11 +02:00
Hocuri
3757e5dca1 Try to add decent error msg (doesnt work yet) 2020-06-13 10:18:11 +02:00
Hocuri
f02c17cae4 Parse standard ndns (e.g. Gmail) 2020-06-13 10:18:11 +02:00
dignifiedquire
e08e817988 fix: update deps to fix nightly builds 2020-06-13 08:45:01 +02:00
Alexander Krotov
dad6381519 run_bot_process: remove account from _accounts before starting the bot
Otherwise wait_configure_and_start_io() will start account, and it will
operate on the same database as the bot.
2020-06-13 06:36:07 +02:00
bjoern
d35cf7d6a2 Merge pull request #1606 from deltachat/fix1589
attempt to fix #1598 -- less chatty on errors
2020-06-12 12:40:53 +02:00
holger krekel
1d34e1f27a attempt to fix #1589 -- if we trigger a reconnect we don't need to "error!" which shows a toast to the user.
the next reconnect will report if it can't connect.
2020-06-12 11:57:38 +02:00
Alexander Krotov
e03246d105 refactor: replace calc_timestamps with calc_sort_timestamp 2020-06-12 09:13:56 +02:00
dignifiedquire
944f1ec005 feat: update dependencies for new rustcrypto releases 2020-06-12 09:12:38 +02:00
Friedel Ziegelmayer
d208905473 fix(receive): improve message sorting 2020-06-11 17:30:57 +02:00
Hocuri
6d2d31928d Warn about the correct folder 2020-06-11 14:36:08 +02:00
Alexander Krotov
f5156f3df6 IMAP: logout from the server with a LOGOUT command
CLOSE, which was used previously, only expunges messages and deselects
folder, and it should only be called if some folder is selected. For that,
Imap.close_folder() method is used.
2020-06-11 13:54:14 +02:00
holger krekel
554160db15 also catch DC_KEY_GEN_RSA2048 as const 2020-06-11 09:22:31 +02:00
Floris Bruynooghe
d8bd9b0515 Import constants from cffi
This replaces the constants list with those compiled by CFFI.  There
is perhaps not much point in having this module anymore but this is
easy to do.
2020-06-11 09:22:31 +02:00
Floris Bruynooghe
27b75103ca Refactor cffi build script to extract defines from header file
This adds functionality to the cffi build script to also extract
defines so that we can ask the compiler to figure out what the correct
values are.  To do this we need to be able to locate the header file
used in the first place, for which we add a small utility in the
header file itself guarded to only be compiled for this specific case.
2020-06-11 09:22:31 +02:00
Hocuri
69e01862b7 More verbose SMTP connect error to see what is going on at #1556 2020-06-11 08:55:47 +02:00
B. Petersen
91f46b1291 bump version to 1.35.0 2020-06-10 19:24:23 +02:00
B. Petersen
9de3774715 update changelog 2020-06-10 19:24:23 +02:00
Hocuri
4dbe836dfa rebuild cargo.lock 2020-06-10 13:22:03 +02:00
Hocuri
322cc5a013 Cargo update 2020-06-10 13:22:03 +02:00
Hocuri
7cc5243130 Also revert cargo.lock 2020-06-10 13:22:03 +02:00
Hocuri
ba549bd559 Revert "Use cloned repos until https://github.com/deltachat/rust-email/pull/4 is merged"
This reverts commit df66f16c84f1a827619e67b3b989a6070f526f31.
2020-06-10 13:22:03 +02:00
Hocuri
84be82c670 Add test 2020-06-10 13:22:03 +02:00
Hocuri
acb42982b7 Use cloned repos until https://github.com/deltachat/rust-email/pull/4 is merged 2020-06-10 13:22:03 +02:00
Hocuri
3370c51b35 Add test 2020-06-10 13:22:03 +02:00
Hocuri
dcfed03702 MISSING_MIME_VERSION, MIME_HEADER_CTYPE_ONLY 2020-06-10 13:22:03 +02:00
holger krekel
e7dd74e4b1 simplify configure() and don't keep state on the account in non-test mode 2020-06-10 12:47:49 +02:00
Alexander Krotov
19b53c76da Add strict_tls support 2020-06-10 10:52:53 +03:00
holger krekel
95b40ad1d8 avoid hello.com and use example.org 2020-06-09 14:39:00 +02:00
holger krekel
0efb2215e4 renamings and parallel sending 2020-06-09 14:39:00 +02:00
holger krekel
0c8f951d8f address @hocuri comment -- remove as_contact() as it's synonym with create_contact
and the latter is already an established API and conveys better that a contact
object will be created if it doesn't exist.
2020-06-09 14:39:00 +02:00
holger krekel
0bb4ef0bd9 introduce chat.num_contacts() as a more efficient shortcut 2020-06-09 14:39:00 +02:00
holger krekel
f93a863f5f fix and steamline tests and test setup 2020-06-09 14:39:00 +02:00
holger krekel
f263843c5f route all flexible contact add/remove through account.as_contact(obj) 2020-06-09 14:39:00 +02:00
holger krekel
503202376a remove logid from Account creation, one can now just use the "displayname" for log purposes 2020-06-09 14:39:00 +02:00
holger krekel
ca70c6a205 remove account.create_chat_by_message in favor of message.create_chat(), simplifing the API 2020-06-09 14:39:00 +02:00
holger krekel
7d5fba8416 refine contact API and introduce account.create_chat() helper
strike create_chat_by_contact in favor of contact.create_chat()
2020-06-09 14:39:00 +02:00
holger krekel
3a85b671a1 remove acfactory.get_chat() in favour of account.create_chat(account2) directly working. 2020-06-09 14:39:00 +02:00
holger krekel
1083cab972 as discussed with @dignifiedquire only do package-building and upload on master, not on branches. 2020-06-09 14:23:07 +02:00
Friedel Ziegelmayer
7677650b39 Merge pull request #1580 from deltachat/fix/imap-connection 2020-06-09 13:44:57 +02:00
dignifiedquire
1f2087190e ci(github): dont build all branches on push 2020-06-09 13:42:40 +02:00
dignifiedquire
59fadee9e0 ci(circle): remove outdated reference 2020-06-09 13:24:19 +02:00
dignifiedquire
4a3825c302 fix: improve imap connection establishment
- fixes blocking on start_io
- attempts to connect to the imap on all tasks when needed
2020-06-09 13:20:16 +02:00
dignifiedquire
52e74c241f update pipeline name 2020-06-09 13:14:55 +02:00
dignifiedquire
3fa69c1852 fixup: ci 2020-06-09 13:10:25 +02:00
dignifiedquire
b3074f854e remove beta from matrix 2020-06-09 13:07:56 +02:00
dignifiedquire
95c5128d9f fixup: ci 2020-06-09 13:07:18 +02:00
dignifiedquire
dc17006b16 fixup: ci 2020-06-09 13:05:21 +02:00
dignifiedquire
e4a4c230fe fixup: ci 2020-06-09 13:02:17 +02:00
dignifiedquire
f56a4450f3 switch rust tests to github ci 2020-06-09 12:59:53 +02:00
dignifiedquire
913db3b958 ci(github): update toolchain 2020-06-09 12:55:25 +02:00
Alexander Krotov
7de23f86b1 Do not reply to messages that can't be decrypted
This commit fixes the test broken in previous commit.
2020-06-08 23:16:35 +02:00
Alexander Krotov
35566f5ea5 Extend undecipherable group test
Reply to group messages assigned to 1:1 chat on ac4 and see where they
end up on ac1 and ac2. Currently they contain group ID in Reply-To field
and go to group chat, so the test is failing.
2020-06-08 23:16:35 +02:00
Alexander Krotov
34579974c3 Don't make ad-hoc groups when message cannot be decrypted
This fixes the test added in the parent commit.
2020-06-08 23:16:35 +02:00
Alexander Krotov
c6f19ea0a4 Add failing test for undecipherable group messages 2020-06-08 23:16:35 +02:00
Alexander Krotov
64ab955ad7 create_or_lookup_group: streamline group ID parsing 2020-06-08 19:25:36 +03:00
holger krekel
4fdf496cac refine one more test to "newstyle" 2020-06-08 16:31:21 +02:00
holger krekel
6497e6397d merge direct imap tests into their already existing counterparts 2020-06-08 16:31:21 +02:00
holger krekel
d8bbe2fcce refine test / chat API 2020-06-08 16:31:21 +02:00
holger krekel
b6cc44a956 integrate direct imap test in existing BCC test 2020-06-08 16:31:21 +02:00
holger krekel
0105c831f1 make direct_imap a permanent feature of online accounts 2020-06-08 16:31:21 +02:00
holger krekel
d40f96ac65 fixing imap interactions 2020-06-08 16:31:21 +02:00
holger krekel
69135709ac more refines and test fixes 2020-06-08 16:31:21 +02:00
holger krekel
612a9d012c snap using imapclient 2020-06-08 16:31:21 +02:00
bjoern
2ad014faf4 Merge pull request #1563 from deltachat/recode-images
recode images
2020-06-08 11:02:35 +02:00
B. Petersen
f3a59e19d8 simplify condition for jpeg-check 2020-06-08 10:37:13 +02:00
bjoern
17283c86a3 Update src/chat.rs
Co-authored-by: Hocuri <hocuri@gmx.de>
2020-06-08 10:37:13 +02:00
bjoern
945943a849 Update src/constants.rs
Co-authored-by: Hocuri <hocuri@gmx.de>
2020-06-08 10:37:13 +02:00
bjoern
34c69785d0 Update src/blob.rs
Co-authored-by: Hocuri <hocuri@gmx.de>
2020-06-08 10:37:12 +02:00
B. Petersen
d5ea4f9b1a make clippy happy 2020-06-08 10:37:12 +02:00
B. Petersen
191009372b basically recode images 2020-06-08 10:37:12 +02:00
Alexander Krotov
39faddc74d create_or_lookup_adhoc_group: move comment to the correct place
The comment is related to member list processing, not mailing list check.
2020-06-08 08:30:26 +03:00
bjoern
5d1623b98f Merge pull request #1574 from deltachat/better-subject-addon
add new string to deltachat.h
2020-06-07 12:41:21 +02:00
B. Petersen
af0dc42df3 add new string to deltachat.h 2020-06-07 12:29:29 +02:00
Hocuri
c18705fae3 Improve test 2020-06-07 12:11:52 +02:00
Hocuri
22973899b8 Assume that thare always is Config::Addr set 2020-06-07 12:11:52 +02:00
Hocuri
f172e92098 Repair test 2020-06-07 12:11:52 +02:00
Hocuri
e1ff657c78 Dont Hardcode 'Delta Chat' 2020-06-07 12:11:52 +02:00
Hocuri
3e6cd3ff34 Adapt to async, set first subject to 'Message from <sender name>' 2020-06-07 12:11:52 +02:00
Hocuri
f8680724f8 Set subject to Re: <last subject> for better compability with normal MUAs
The code in dc_receive_imf.rs looks a bit funny, an alternative would be a function:

fn upcate_chat_last_subject(context: &Context, chat_id: &ChatId, mime_parser: &mut MimeMessage) -> Result<()> {
    let mut chat = Chat::load_from_db(context, *chat_id)?;
    chat.param.set(Param::LastSubject, mime_parser.get_subject().ok_or_else(||Error::Message("No subject in email".to_string()))?);
    chat.update_param(context)?;
    Ok(())
}
2020-06-07 12:11:52 +02:00
holger krekel
30c76976fc update links and add a little boilerplate / status info 2020-06-06 19:20:37 +02:00
Alexander Krotov
f0f020d9d2 chat: get rid of ChatId.is_error()
get_rowid should not return 0, as we have inserted a row right above.

And using is_error() instead of comparing row_id to 0 is a strange way
to check this condition.

As all functions that actually returned 0 chat ID to indicate error have
been removed, the function is gone too.
2020-06-06 19:49:57 +03:00
Hocuri
17a13f0f83 Adapt spec.md to new subject (#1395) 2020-06-06 18:34:50 +02:00
bjoern
ec441b16f1 Revert "Enable strict TLS certificate checks by default"
This reverts commit 6d9ff3d248.
2020-06-06 18:42:54 +03:00
Alexander Krotov
5239f2edad dc_receive_imf: replace chat_id.is_error() with chat_id.is_unset()
Both methods do the same: compare chat_id to 0. However, in these cases
0 refers to the state when chat_id is not determined yet, because no
corresponding chat has been found.

All functions that returned 0 to indicate error have already been
resultified.
2020-06-06 18:29:35 +03:00
Alexander Krotov
cd751a64cb python tests: fix typos 2020-06-06 16:11:59 +03:00
Alexander Krotov
6d9ff3d248 Enable strict TLS certificate checks by default 2020-06-06 00:08:29 +02:00
holger krekel
d97d9980dd cleanup 2020-06-05 23:28:27 +02:00
holger krekel
4ad4d6d10d Update python/src/deltachat/direct_imap.py
Co-authored-by: Hocuri <hocuri@gmx.de>
2020-06-05 23:28:27 +02:00
holger krekel
82731ee86c fix test 2020-06-05 23:28:27 +02:00
holger krekel
04bdfa17f7 improve shutdown order 2020-06-05 23:28:27 +02:00
holger krekel
7a5759de4b some streamlining and test fixing 2020-06-05 23:28:27 +02:00
holger krekel
e29dcbf8eb remove wrong inbox folders 2020-06-05 23:28:27 +02:00
holger krekel
882f90b5ff more cleanups, don't run the test 30 times anymore -- it's too wasteful
and doesn't gurantee test failure.
2020-06-05 23:28:27 +02:00
holger krekel
469451d5dd rework imap structure logging 2020-06-05 23:28:27 +02:00
holger krekel
af33c2dea7 test pass again 2020-06-05 23:28:27 +02:00
holger krekel
d076ab4d6d fix grouping / helper function 2020-06-05 21:59:33 +02:00
holger krekel
e66ed8eadb shift tests away a little, mark "ignored" for regular pytest runs 2020-06-05 21:59:33 +02:00
Hocuri
05e1c00cd1 fix: update message ids correctly
Fixes #1495
2020-06-05 16:27:22 +02:00
Floris Bruynooghe
ca95f25639 Use the Fingerprint type to handle fingerprints
This uses the Fingerprint type more consistenly when handling
fingerprits rather then have various string representations passed
around and sometimes converted back and forth with slight differences
in strictness.

It fixes an important bug in the existing, but until now unused,
parsing behaviour of Fingerprint.  It also adds a default length check
on the fingerprint as that was checked in some existing places.

Fially generating keys is no longer expensive, so let's not ignore
these tests.
2020-06-04 22:46:59 +02:00
Friedel Ziegelmayer
95cde55a7f Merge pull request #1549 from deltachat/fix-reconnect-logic 2020-06-03 16:17:15 +02:00
dignifiedquire
8756c0cbe1 test(python): avoid race condition in unicode test 2020-06-03 16:10:44 +02:00
dignifiedquire
86c6b09814 fix: trigger reconnects when errors occur during idle and fetch 2020-06-03 15:40:54 +02:00
bjoern
6ce27a7f87 Merge pull request #1548 from deltachat/fix-folders
fix(imap): deterministically detect folder meaning
2020-06-02 16:18:49 +02:00
dignifiedquire
7addb15be5 fix(imap): deterministically detect folder meaning 2020-06-02 15:18:24 +02:00
Hocuri
7b3a962498 Sanitize address book 2020-05-31 17:04:25 +02:00
Hocuri
41bba7e780 Fix #880 Don't vary advanced login settings if a user set a particular setting 2020-05-29 20:31:34 +02:00
Friedel Ziegelmayer
419b7d1d5c Merge pull request #1539 from deltachat/sane-configure
refactor(configure): simplify logic and code
2020-05-29 19:32:49 +02:00
dignifiedquire
6d8b4a7ec0 simplify further and apply CR 2020-05-29 19:09:45 +02:00
B. Petersen
84963e198e do autoconfig only when no advanced options are entered
the advanced options are not used anyway later,
but prevent imap/smtp connections from being altered.

nb: we want to stop altering when some advanced options
are entered, however, we want to do this probaby
not depending on autoconfig.
2020-05-29 19:09:45 +02:00
dignifiedquire
408e9946af refactor(configure): cleanup logic 2020-05-29 19:09:45 +02:00
dignifiedquire
43f49f8917 refactor(configure): remove step-counter 2020-05-29 19:09:45 +02:00
Hocuri
b6161c431b Fix #1474 "Sending message to contact with < or > in Recipient gets treated as "Sent" but is not received" (#1476)
Fix #1474 "Sending message to contact with < or > in Recipient gets treated as "Sent" but is not received".

As I was at it, I also extracted the correct name and address from addresses like Mueller, Dave <dave@domain.com>.
2020-05-29 18:14:21 +02:00
Floris Bruynooghe
a236a619ad Finish Key->DcKey refactoring
Migrates .verify() and .split_key() to DcKey.  Removes all remaining
uses of Key.
2020-05-29 11:25:52 +02:00
Floris Bruynooghe
cdbd3d7d84 Move ascii-armored stuff from Key to DcKey
This means all key conversions/serialisation/deserialisation can be
done with DcKey rather than Key.  Also migrate all key conversion
tests to DcKey rather than Key.
2020-05-29 11:25:52 +02:00
Floris Bruynooghe
8efc880b77 Move Keyring and fingerprint to DcKey trait
This moves both the Keyring and the fingerprints to the DcKey trait,
unfortunately I was not able to disentangle these two changes.  The
Keyring now ensures only the right kind of key is added to it.

The keyring now uses the DcKey::load_self method rather than
re-implement the SQL to load keys from the database.  This vastly
simpliefies the use and fixes an error where a failed key load or
unconfigured would result in the message being treated as plain text
and benefits from the in-line key generation path.

For the fingerprint a new type representing it is introduced.  The aim
is to replace more fingerpring uses with this type as now there are
various string representations being passed around and converted
between.  The Display trait is used for the space-separated and
multiline format, which is perhaps not the most obvious but seems
right together with FromStr etc.
2020-05-29 11:25:52 +02:00
jikstra
4bade7e13a Update cargo lock 2020-05-28 20:38:11 +02:00
Jikstra
53099bbfd1 Merge pull request #1537 from deltachat/fix_dc_str_constants
Add missing DC_STR_* constants to deltachat.h
2020-05-28 18:24:11 +02:00
jikstra
7d1d02bf3b Add missing DC_STR_* constants to deltachat.h 2020-05-28 13:41:56 +02:00
bjoern
4f477ec6d2 Merge pull request #1536 from deltachat/prep-1.34
Prep 1.34
2020-05-27 20:33:53 +02:00
88 changed files with 9361 additions and 6832 deletions

View File

@@ -138,12 +138,6 @@ jobs:
- py-docs
- wheelhouse
remote_tests_rust:
machine: true
steps:
- checkout
- run: ci_scripts/remote_tests_rust.sh
remote_tests_python:
machine: true
steps:
@@ -178,11 +172,6 @@ workflows:
jobs:
# - cargo_fetch
- remote_tests_rust:
filters:
tags:
only: /.*/
- remote_tests_python:
filters:
tags:
@@ -191,8 +180,9 @@ workflows:
- remote_python_packaging:
requires:
- remote_tests_python
- remote_tests_rust
filters:
branches:
only: master
tags:
only: /.*/
@@ -201,6 +191,8 @@ workflows:
- remote_python_packaging
- build_doxygen
filters:
branches:
only: master
tags:
only: /.*/
# - rustfmt:
@@ -212,6 +204,8 @@ workflows:
- build_doxygen:
filters:
branches:
only: master
tags:
only: /.*/

89
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,89 @@
name: Rust CI
on:
pull_request:
push:
branches:
- master
- staging
- trying
jobs:
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.43.1
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
run_clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.43.1
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
build_and_test:
name: Build and test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
rust: [nightly, 1.43.1]
steps:
- uses: actions/checkout@master
- name: Install ${{ matrix.rust }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
- name: Cache cargo registry
uses: actions/cache@v2
with:
path: ~/.cargo/registry
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-registry-${{ hashFiles('**/Cargo.toml') }}
- name: Cache cargo index
uses: actions/cache@v2
with:
path: ~/.cargo/git
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-index-${{ hashFiles('**/Cargo.toml') }}
- name: Cache cargo build
uses: actions/cache@v2
with:
path: target
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-build-target-${{ hashFiles('**/Cargo.toml') }}
- name: check
uses: actions-rs/cargo@v1
with:
command: check
args: --workspace --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace

View File

@@ -1,48 +0,0 @@
on: push
name: Code Quality
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2020-03-19
override: true
- uses: actions-rs/cargo@v1
with:
command: check
args: --workspace --examples --tests --all-features
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2020-03-19
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
run_clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly-2020-03-19
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features

View File

@@ -1,5 +1,75 @@
# Changelog
## 1.39.0
- fix handling of `mvbox_watch`, `sentbox_watch`, `inbox_watch` #1654 #1658
- fix potential panics, update dependencies #1650 #1655
## 1.38.0
- fix sorting, esp. for multi-device
## 1.37.0
- improve ndn heuristics #1630
- get oauth2 authorizer from provider-db #1641
- removed linebreaks and spaces from generated qr-code #1631
- more fixes #1633 #1635 #1636 #1637
## 1.36.0
- parse ndn (network delivery notification) reports
and report failed messages as such #1552 #1622 #1630
- add oauth2 support for gsuite domains #1626
- read image orientation from exif before recoding #1619
- improve logging #1593 #1598
- improve python and bot bindings #1583 #1609
- improve imap logout #1595
- fix sorting #1600 #1604
- fix qr code generation #1631
- update rustcrypto releases #1603
- refactorings #1617
## 1.35.0
- enable strict-tls from a new provider-db setting #1587
- new subject 'Message from USER' for one-to-one chats #1395
- recode images #1563
- improve reconnect handling #1549 #1580
- improve importing addresses #1544
- improve configure and folder detection #1539 #1548
- improve test suite #1559 #1564 #1580 #1581 #1582 #1584 #1588:
- fix ad-hoc groups #1566
- preventions against being marked as spam #1575
- refactorings #1542 #1569
## 1.34.0
- new api for io, thread and event handling #1356,

1671
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,23 @@
[package]
name = "deltachat"
version = "1.34.0"
version = "1.39.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
[profile.release]
# lto = true
lto = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
libc = "0.2.51"
pgp = { version = "0.5.1", default-features = false }
pgp = { version = "0.6.0", default-features = false }
hex = "0.4.0"
sha2 = "0.8.0"
sha2 = "0.9.0"
rand = "0.7.0"
smallvec = "1.0.0"
surf = { version = "2.0.0-alpha.2", default-features = false, features = ["h1-client"] }
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 = { version = "0.3" }
@@ -25,54 +25,54 @@ email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
async-imap = "0.3.1"
async-native-tls = { version = "0.3.3" }
async-std = { version = "1.6.0", features = ["unstable"] }
base64 = "0.11"
async-std = { version = "1.6.1", features = ["unstable"] }
base64 = "0.12"
charset = "0.1"
percent-encoding = "2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4.6"
indexmap = "1.3.0"
kamadak-exif = "0.5"
lazy_static = "1.4.0"
regex = "1.1.6"
rusqlite = { version = "0.22", features = ["bundled"] }
r2d2_sqlite = "0.15.0"
r2d2 = "0.8.5"
strum = "0.16.0"
strum_macros = "0.16.0"
strum = "0.18.0"
strum_macros = "0.18.0"
backtrace = "0.3.33"
byteorder = "1.3.1"
itertools = "0.8.0"
image-meta = "0.1.0"
quick-xml = "0.17.1"
quick-xml = "0.18.1"
escaper = "0.1.0"
bitflags = "1.1.0"
debug_stub_derive = "0.3.0"
sanitize-filename = "0.2.1"
stop-token = { version = "0.1.1", features = ["unstable"] }
mailparse = "0.12.0"
mailparse = "0.12.1"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
native-tls = "0.2.3"
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
futures = "0.3.4"
thiserror = "1.0.14"
anyhow = "1.0.28"
async-trait = "0.1.31"
url = "2.1.1"
async-std-resolver = "0.19.5"
sqlx = { git = "https://github.com/launchbadge/sqlx", branch = "master", features = ["runtime-async-std", "sqlite", "macros"] }
libsqlite3-sys = { version = "0.18", features = ["bundled", "min_sqlite_version_3_7_16"] }
pretty_env_logger = { version = "0.3.1", optional = true }
log = {version = "0.4.8", optional = true }
pretty_env_logger = { version = "0.4.0", optional = true }
log = { version = "0.4.8", optional = true }
rustyline = { version = "4.1.0", optional = true }
ansi_term = { version = "0.12.1", optional = true }
[dev-dependencies]
tempfile = "3.0"
pretty_assertions = "0.6.1"
pretty_env_logger = "0.3.0"
proptest = "0.9.4"
pretty_env_logger = "0.4.0"
proptest = "0.10"
async-std = { version = "1.6.0", features = ["unstable", "attributes"] }
smol = "0.1.10"
smol = "0.1.11"
log = "0.4.8"
[workspace]
members = [
@@ -97,6 +97,3 @@ internals = []
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term"]
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored"]
nightly = ["pgp/nightly"]
[patch.crates-io]
smol = { git = "https://github.com/dignifiedquire/smol-1", branch = "isolate-nix" }

View File

@@ -123,6 +123,7 @@ Language bindings are available for:
- [Node.js](https://www.npmjs.com/package/deltachat-node)
- [Python](https://py.delta.chat)
- [Go](https://github.com/hugot/go-deltachat/)
- [Free Pascal](https://github.com/deltachat/deltachat-fp/)
- **Java** and **Swift** (contained in the Android/iOS repos)
The following "frontend" projects make use of the Rust-library

View File

@@ -1,19 +0,0 @@
environment:
matrix:
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
install:
- appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
- rustup-init -yv --default-toolchain nightly-2020-03-19
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin
- rustc -vV
- cargo -vV
build: false
test_script:
- cargo test --release --all
cache:
- target
- C:\Users\appveyor\.cargo\registry

View File

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

View File

@@ -262,17 +262,25 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `selfavatar` = File containing avatar. Will immediately be copied to the
* `blobdir`; the original image will not be needed anymore.
* NULL to remove the avatar.
* It is planned for future versions
* to send this image together with the next messages.
* As for `displayname` and `selfstatus`, also the avatar is sent to the recipients.
* To save traffic, however, the avatar is attached only as needed
* and also recoded to a reasonable size.
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
* - `mdns_enabled` = 0=do not send or request read receipts,
* 1=send and request read receipts (default)
* - `bcc_self` = 0=do not send a copy of outgoing messages to self (default),
* 1=send a copy of outgoing messages to self.
* Sending messages to self is needed for a proper multi-account setup,
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
* - `inbox_watch` = 1=watch `INBOX`-folder for changes (default),
* 0=do not watch the `INBOX`-folder
* 0=do not watch the `INBOX`-folder,
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* - `sentbox_watch`= 1=watch `Sent`-folder for changes (default),
* 0=do not watch the `Sent`-folder
* 0=do not watch the `Sent`-folder,
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* - `mvbox_watch` = 1=watch `DeltaChat`-folder for changes (default),
* 0=do not watch the `DeltaChat`-folder
* 0=do not watch the `DeltaChat`-folder,
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* - `mvbox_move` = 1=heuristically detect chat-messages
* and move them to the `DeltaChat`-folder,
* 0=do not move chat-messages
@@ -307,9 +315,9 @@ char* dc_get_blobdir (const dc_context_t* context);
* DC_MEDIA_QUALITY_WORSE (1)
* allow worse images/videos/voice quality to gain smaller sizes,
* suitable for providers or areas known to have a bad connection.
* In contrast to other options, the implementation of this option is currently up to the UIs;
* this may change in future, however,
* having the option in the core allows provider-specific-defaults already today.
* The library uses the `media_quality` setting to use different defaults
* for recoding images sent with type DC_MSG_IMAGE.
* If needed, recoding other file types is up to the UI.
*
* If you want to retrieve a value, use dc_get_config().
*
@@ -528,9 +536,20 @@ int dc_is_io_running(const dc_context_t* context);
void dc_stop_io(dc_context_t* context);
/**
* This function can be called whenever there is a hint
* that the network is available again.
* The library will try to send pending messages out.
* This function should be called when there is a hint
* that the network is available again,
* eg. as a response to system event reporting network availability.
* The library will try to send pending messages out immediately.
*
* Moreover, to have a reliable state
* when the app comes to foreground with network available,
* it may be reasonable to call the function also at that moment.
*
* It is okay to call the function unconditionally when there is
* network available, however, calling the function
* _without_ having network may interfere with the backoff algorithm
* and will led to let the jobs fail faster, with fewer retries
* and may avoid messages being sent out.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
@@ -754,6 +773,15 @@ uint32_t dc_prepare_msg (dc_context_t* context, uint32_t ch
* dc_msg_unref(msg);
* ~~~
*
* If you send images with the DC_MSG_IMAGE type,
* they will be recoded to a reasonable size before sending, if possible
* (cmp the dc_set_config()-option `media_quality`).
* If that fails, is not possible, or the image is already small enough, the image is sent as original.
* If you want images to be always sent as the original file, use the DC_MSG_FILE type.
*
* Videos and other file types are currently not recoded by the library,
* with dc_prepare_msg(), however, you can do that from the UI.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id Chat ID to send the message to.
@@ -2782,6 +2810,9 @@ int dc_msg_get_viewtype (const dc_msg_t* msg);
* If a sent message changes to this state, you'll receive the event #DC_EVENT_MSG_DELIVERED.
* - DC_STATE_OUT_MDN_RCVD (28) - Outgoing message read by the recipient (two checkmarks; this requires goodwill on the receiver's side)
* If a sent message changes to this state, you'll receive the event #DC_EVENT_MSG_READ.
* Also messages already read by some recipients
* may get into the state DC_STATE_OUT_FAILED at a later point,
* eg. when in a group, delivery fails for some recipients.
*
* If you just want to check if a message is sent or not, please use dc_msg_is_sent() which regards all states accordingly.
*
@@ -4117,8 +4148,9 @@ void dc_event_unref(dc_event_t* event);
/**
* A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
* DC_STATE_OUT_FAILED, see dc_msg_get_state().
* A single message could not be sent.
* State changed from DC_STATE_OUT_PENDING, DC_STATE_OUT_DELIVERED or DC_STATE_OUT_MDN_RCVD
* to DC_STATE_OUT_FAILED, see dc_msg_get_state().
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
@@ -4283,7 +4315,7 @@ void dc_event_unref(dc_event_t* event);
*/
/**
* Prover works out-of-the-box.
* Provider works out-of-the-box.
* This provider status is returned for provider where the login
* works by just entering the name or the email-address.
*
@@ -4423,12 +4455,26 @@ void dc_event_unref(dc_event_t* event);
#define DC_STR_LOCATION 66
#define DC_STR_STICKER 67
#define DC_STR_DEVICE_MESSAGES 68
#define DC_STR_COUNT 68
#define DC_STR_SAVED_MESSAGES 69
#define DC_STR_DEVICE_MESSAGES_HINT 70
#define DC_STR_WELCOME_MESSAGE 71
#define DC_STR_UNKNOWN_SENDER_FOR_CHAT 72
#define DC_STR_SUBJECT_FOR_NEW_CONTACT 73
#define DC_STR_FAILED_SENDING_TO 74
#define DC_STR_COUNT 74
/*
* @}
*/
#ifdef PY_CFFI_INC
/* Helper utility to locate the header file when building python bindings. */
char* _dc_header_file_location(void) {
return __FILE__;
}
#endif
#ifdef __cplusplus
}

View File

@@ -126,7 +126,7 @@ pub unsafe extern "C" fn dc_set_config(
// When ctx.set_config() fails it already logged the error.
// TODO: Context::set_config() should not log this
Ok(key) => block_on(async move {
ctx.set_config(key, to_opt_string_lossy(value).as_ref().map(|x| x.as_str()))
ctx.set_config(key, to_opt_string_lossy(value).as_deref())
.await
.is_ok() as libc::c_int
}),
@@ -285,7 +285,7 @@ pub unsafe extern "C" fn dc_start_io(context: *mut dc_context_t) {
}
let ctx = &*context;
block_on({ ctx.start_io() })
block_on(ctx.start_io())
}
#[no_mangle]
@@ -295,7 +295,7 @@ pub unsafe extern "C" fn dc_is_io_running(context: *mut dc_context_t) -> libc::c
}
let ctx = &*context;
block_on({ ctx.is_io_running() }) as libc::c_int
block_on(ctx.is_io_running()) as libc::c_int
}
#[no_mangle]
@@ -349,7 +349,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| Event::MsgDelivered { chat_id, .. }
| Event::MsgFailed { chat_id, .. }
| Event::MsgRead { chat_id, .. }
| Event::ChatModified(chat_id) => chat_id.to_u32() as libc::c_int,
| Event::ChatModified(chat_id) => chat_id.to_u32().unwrap_or_default() as libc::c_int,
Event::ContactsChanged(id) | Event::LocationChanged(id) => {
let id = id.unwrap_or_default();
id as libc::c_int
@@ -396,7 +396,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| Event::IncomingMsg { msg_id, .. }
| Event::MsgDelivered { msg_id, .. }
| Event::MsgFailed { msg_id, .. }
| Event::MsgRead { msg_id, .. } => msg_id.to_u32() as libc::c_int,
| Event::MsgRead { msg_id, .. } => msg_id.to_u32().unwrap_or_default() as libc::c_int,
Event::SecurejoinInviterProgress { progress, .. }
| Event::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
}
@@ -554,14 +554,7 @@ pub unsafe extern "C" fn dc_get_chatlist(
let qi = if query_id == 0 { None } else { Some(query_id) };
block_on(async move {
match chatlist::Chatlist::try_load(
&ctx,
flags as usize,
qs.as_ref().map(|x| x.as_str()),
qi,
)
.await
{
match chatlist::Chatlist::try_load(&ctx, flags as usize, qs.as_deref(), qi).await {
Ok(list) => {
let ffi_list = ChatlistWrapper { context, list };
Box::into_raw(Box::new(ffi_list))
@@ -752,13 +745,9 @@ pub unsafe extern "C" fn dc_add_device_msg(
};
block_on(async move {
chat::add_device_msg(
&ctx,
to_opt_string_lossy(label).as_ref().map(|x| x.as_str()),
msg,
)
.await
.unwrap_or_log_default(&ctx, "Failed to add device message")
chat::add_device_msg(&ctx, to_opt_string_lossy(label).as_deref(), msg)
.await
.unwrap_or_log_default(&ctx, "Failed to add device message")
})
.to_u32()
}
@@ -844,7 +833,8 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
let arr = dc_array_t::from(
chat::get_chat_msgs(&ctx, ChatId::new(chat_id), flags, marker_flag)
.await
.iter()
.unwrap_or_log_default(ctx, "failed get_chat_msgs")
.into_iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(),
);
@@ -909,7 +899,7 @@ pub unsafe extern "C" fn dc_get_fresh_msgs(
let arr = dc_array_t::from(
ctx.get_fresh_msgs()
.await
.iter()
.into_iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(),
);
@@ -986,7 +976,8 @@ pub unsafe extern "C" fn dc_get_chat_media(
or_msg_type3,
)
.await
.iter()
.unwrap_or_log_default(ctx, "failed get_chat_media")
.into_iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(),
);
@@ -1030,8 +1021,9 @@ pub unsafe extern "C" fn dc_get_next_media(
or_msg_type3,
)
.await
.unwrap_or_log_default(ctx, "failed get_next_media")
.map(|msg_id| msg_id.to_u32())
.unwrap_or(0)
.unwrap_or_else(|| 0)
})
}
@@ -1097,7 +1089,10 @@ pub unsafe extern "C" fn dc_get_chat_contacts(
let ctx = &*context;
block_on(async move {
let arr = dc_array_t::from(chat::get_chat_contacts(&ctx, ChatId::new(chat_id)).await);
let list = chat::get_chat_contacts(&ctx, ChatId::new(chat_id))
.await
.unwrap_or_log_default(ctx, "failed get_chat_contacts");
let arr = dc_array_t::from(list);
Box::into_raw(Box::new(arr))
})
}
@@ -1118,7 +1113,7 @@ pub unsafe extern "C" fn dc_search_msgs(
let arr = dc_array_t::from(
ctx.search_msgs(ChatId::new(chat_id), to_string_lossy(query))
.await
.iter()
.into_iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(),
);
@@ -1307,7 +1302,12 @@ pub unsafe extern "C" fn dc_get_msg_info(
}
let ctx = &*context;
block_on(message::get_msg_info(&ctx, MsgId::new(msg_id))).strdup()
block_on(async move {
message::get_msg_info(ctx, MsgId::new(msg_id))
.await
.unwrap_or_log_default(ctx, "failed get_msg_info")
})
.strdup()
}
#[no_mangle]
@@ -1829,7 +1829,11 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
}
let ctx = &*context;
block_on({ location::send_locations_to_chat(&ctx, ChatId::new(chat_id), seconds as i64) });
block_on(location::send_locations_to_chat(
&ctx,
ChatId::new(chat_id),
seconds as i64,
));
}
#[no_mangle]
@@ -1862,7 +1866,11 @@ pub unsafe extern "C" fn dc_set_location(
}
let ctx = &*context;
block_on(location::set(&ctx, latitude, longitude, accuracy)) as _
block_on(async move {
location::set(ctx, latitude, longitude, accuracy)
.await
.unwrap_or_log_default(ctx, "failed location::set")
}) as _
}
#[no_mangle]
@@ -2262,7 +2270,12 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut
let ctx = &*ffi_chat.context;
block_on(async move {
match ffi_chat.chat.get_profile_image(&ctx).await {
match ffi_chat
.chat
.get_profile_image(&ctx)
.await
.unwrap_or_log_default(ctx, "failed get_profile_image")
{
Some(p) => p.to_string_lossy().strdup(),
None => ptr::null_mut(),
}
@@ -2278,7 +2291,13 @@ pub unsafe extern "C" fn dc_chat_get_color(chat: *mut dc_chat_t) -> u32 {
let ffi_chat = &*chat;
let ctx = &*ffi_chat.context;
block_on(ffi_chat.chat.get_color(&ctx))
block_on(async move {
ffi_chat
.chat
.get_color(&ctx)
.await
.unwrap_or_log_default(ctx, "failed dc_chat_get_color")
})
}
#[no_mangle]
@@ -2812,7 +2831,7 @@ pub unsafe extern "C" fn dc_msg_set_file(
let ffi_msg = &mut *msg;
ffi_msg.message.set_file(
to_string_lossy(file),
to_opt_string_lossy(filemime).as_ref().map(|x| x.as_str()),
to_opt_string_lossy(filemime).as_deref(),
)
}

View File

@@ -8,36 +8,33 @@ use quote::quote;
// data. If this assumption is violated, compiler error will point to
// generated code, which is not very user-friendly.
#[proc_macro_derive(ToSql)]
pub fn to_sql_derive(input: TokenStream) -> TokenStream {
#[proc_macro_derive(Sqlx)]
pub fn sqlx_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
let gen = quote! {
impl rusqlite::types::ToSql for #name {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let num = *self as i64;
let value = rusqlite::types::Value::Integer(num);
let output = rusqlite::types::ToSqlOutput::Owned(value);
std::result::Result::Ok(output)
}
}
};
gen.into()
}
#[proc_macro_derive(FromSql)]
pub fn from_sql_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
let gen = quote! {
impl rusqlite::types::FromSql for #name {
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
let inner = rusqlite::types::FromSql::column_result(col)?;
Ok(num_traits::FromPrimitive::from_i64(inner).unwrap_or_default())
impl<'q> sqlx::encode::Encode<'q, sqlx::sqlite::Sqlite> for #name {
fn encode_by_ref(&self, buf: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>) -> sqlx::encode::IsNull {
num_traits::ToPrimitive::to_i32(self).expect("invalid type").encode(buf)
}
}
impl<'de> sqlx::decode::Decode<'de, sqlx::sqlite::Sqlite> for #name {
fn decode(value: sqlx::sqlite::SqliteValueRef) -> std::result::Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
let raw: i32 = sqlx::decode::Decode::decode(value)?;
Ok(num_traits::FromPrimitive::from_i32(raw).unwrap_or_default())
}
}
impl sqlx::types::Type<sqlx::sqlite::Sqlite> for #name {
fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
<i32 as sqlx::types::Type<_>>::type_info()
}
}
};
gen.into()
}

View File

@@ -28,7 +28,7 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 1 {
context
.sql()
.execute("DELETE FROM jobs;", paramsv![])
.execute("DELETE FROM jobs;", paramsx![])
.await
.unwrap();
println!("(1) Jobs reset.");
@@ -36,7 +36,7 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 2 {
context
.sql()
.execute("DELETE FROM acpeerstates;", paramsv![])
.execute("DELETE FROM acpeerstates;", paramsx![])
.await
.unwrap();
println!("(2) Peerstates reset.");
@@ -44,7 +44,7 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 4 {
context
.sql()
.execute("DELETE FROM keypairs;", paramsv![])
.execute("DELETE FROM keypairs;", paramsx![])
.await
.unwrap();
println!("(4) Private keypairs reset.");
@@ -52,35 +52,35 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 8 {
context
.sql()
.execute("DELETE FROM contacts WHERE id>9;", paramsv![])
.execute("DELETE FROM contacts WHERE id>9;", paramsx![])
.await
.unwrap();
context
.sql()
.execute("DELETE FROM chats WHERE id>9;", paramsv![])
.execute("DELETE FROM chats WHERE id>9;", paramsx![])
.await
.unwrap();
context
.sql()
.execute("DELETE FROM chats_contacts;", paramsv![])
.execute("DELETE FROM chats_contacts;", paramsx![])
.await
.unwrap();
context
.sql()
.execute("DELETE FROM msgs WHERE id>9;", paramsv![])
.execute("DELETE FROM msgs WHERE id>9;", paramsx![])
.await
.unwrap();
context
.sql()
.execute(
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
paramsv![],
paramsx![],
)
.await
.unwrap();
context
.sql()
.execute("DELETE FROM leftgrps;", paramsv![])
.execute("DELETE FROM leftgrps;", paramsx![])
.await
.unwrap();
println!("(8) Rest but server config reset.");
@@ -118,7 +118,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
.await
.unwrap();
} else {
let rs = context.sql().get_raw_config(context, "import_spec").await;
let rs = context.sql().get_raw_config("import_spec").await;
if rs.is_none() {
error!(context, "Import: No file or folder given.");
return false;
@@ -276,7 +276,7 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) {
}
);
let peerstate = Peerstate::from_addr(context, &addr).await;
if peerstate.is_some() && contact_id != 1 as libc::c_uint {
if peerstate.is_ok() && contact_id != 1 as libc::c_uint {
line2 = format!(
", prefer-encrypt={}",
peerstate.as_ref().unwrap().prefer_encrypt
@@ -484,10 +484,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
context.maybe_network().await;
}
"housekeeping" => {
sql::housekeeping(&context).await;
sql::housekeeping(&context).await?;
}
"listchats" | "listarchived" | "chats" => {
let listflags = if arg0 == "listarchived" { 0x01 } else { 0 };
let time_start = std::time::SystemTime::now();
let chatlist = Chatlist::try_load(
&context,
listflags,
@@ -495,6 +496,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
None,
)
.await?;
let time_needed = std::time::SystemTime::now()
.duration_since(time_start)
.unwrap_or_default();
let cnt = chatlist.len();
if cnt > 0 {
@@ -553,6 +557,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Location streaming enabled.");
}
println!("{} chats", cnt);
println!("{:?} to create this list", time_needed);
}
"chat" => {
if sel_chat.is_none() && arg1.is_empty() {
@@ -568,8 +573,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "Failed to select chat");
let sel_chat = sel_chat.as_ref().unwrap();
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await;
let members = chat::get_chat_contacts(&context, sel_chat.id).await;
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await?;
let members = chat::get_chat_contacts(&context, sel_chat.id).await?;
let subtitle = if sel_chat.is_device_talk() {
"device-talk".to_string()
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
@@ -589,7 +594,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
} else {
""
},
match sel_chat.get_profile_image(&context).await {
match sel_chat.get_profile_image(&context).await? {
Some(icon) => match icon.to_str() {
Some(icon) => format!(" Icon: {}", icon),
_ => " Icon: Err".to_string(),
@@ -686,7 +691,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected.");
let contacts =
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await;
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
println!("Memberlist:");
log_contactlist(&context, &contacts).await;
@@ -757,7 +762,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let latitude = arg1.parse()?;
let longitude = arg2.parse()?;
let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
let continue_streaming = location::set(&context, latitude, longitude, 0.).await?;
if continue_streaming {
println!("Success, streaming should be continued.");
} else {
@@ -853,7 +858,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Viewtype::Gif,
Viewtype::Video,
)
.await;
.await?;
println!("{} images or videos: ", images.len());
for (i, data) in images.iter().enumerate() {
if 0 == i {
@@ -887,7 +892,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?);
let res = message::get_msg_info(&context, id).await;
let res = message::get_msg_info(&context, id).await?;
println!("{}", res);
}
"listfresh" => {

View File

@@ -12,7 +12,7 @@ class EchoPlugin:
message.account.shutdown()
else:
# unconditionally accept the chat
message.accept_sender_contact()
message.create_chat()
addr = message.get_sender_contact().addr
if message.is_system_message():
message.chat.send_text("echoing system message from {}:\n{}".format(addr, message))

View File

@@ -12,7 +12,7 @@ class GroupTrackingPlugin:
message.account.shutdown()
else:
# unconditionally accept the chat
message.accept_sender_contact()
message.create_chat()
addr = message.get_sender_contact().addr
text = message.text
message.chat.send_text("echoing from {}:\n{}".format(addr, text))

View File

@@ -26,15 +26,15 @@ def test_echo_quit_plugin(acfactory, lp):
lp.sec("sending a message to the bot")
bot_contact = ac1.create_contact(botproc.addr)
ch1 = ac1.create_chat_by_contact(bot_contact)
ch1.send_text("hello")
bot_chat = bot_contact.create_chat()
bot_chat.send_text("hello")
lp.sec("waiting for the bot-reply to arrive")
lp.sec("waiting for the reply message from the bot to arrive")
reply = ac1._evtracker.wait_next_incoming_message()
assert reply.chat == bot_chat
assert "hello" in reply.text
assert reply.chat == ch1
lp.sec("send quit sequence")
ch1.send_text("/quit")
bot_chat.send_text("/quit")
botproc.wait()
@@ -47,8 +47,8 @@ def test_group_tracking_plugin(acfactory, lp):
botproc.fnmatch_lines("""
*ac_configure_completed*
""")
ac1.add_account_plugin(FFIEventLogger(ac1, "ac1"))
ac2.add_account_plugin(FFIEventLogger(ac2, "ac2"))
ac1.add_account_plugin(FFIEventLogger(ac1))
ac2.add_account_plugin(FFIEventLogger(ac2))
lp.sec("creating bot test group with bot")
bot_contact = ac1.create_contact(botproc.addr)

View File

@@ -18,7 +18,7 @@ def main():
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
long_description=long_description,
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
install_requires=['cffi>=1.0.0', 'pluggy'],
install_requires=['cffi>=1.0.0', 'pluggy', 'imapclient'],
packages=setuptools.find_packages('src'),
package_dir={'': 'src'},
cffi_modules=['src/deltachat/_build.py:ffibuilder'],

View File

@@ -60,7 +60,8 @@ def run_cmdline(argv=None, account_plugins=None):
ac = Account(args.db)
if args.show_ffi:
log = events.FFIEventLogger(ac, "bot")
ac.set_config("displayname", "bot")
log = events.FFIEventLogger(ac)
ac.add_account_plugin(log)
for plugin in account_plugins or []:
@@ -76,8 +77,8 @@ 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.configure()
ac.wait_configure_finish()
configtracker = ac.configure()
configtracker.wait_finish()
# start IO threads and configure if neccessary
ac.start_io()

View File

@@ -1,65 +1,64 @@
import distutils.ccompiler
import distutils.log
import distutils.sysconfig
import tempfile
import platform
import os
import cffi
import platform
import re
import shutil
from os.path import dirname as dn
import subprocess
import tempfile
import textwrap
import types
from os.path import abspath
from os.path import dirname as dn
import cffi
def ffibuilder():
projdir = os.environ.get('DCC_RS_DEV')
if not projdir:
p = dn(dn(dn(dn(abspath(__file__)))))
projdir = os.environ["DCC_RS_DEV"] = p
target = os.environ.get('DCC_RS_TARGET', 'release')
if projdir:
if platform.system() == 'Darwin':
libs = ['resolv', 'dl']
extra_link_args = [
'-framework', 'CoreFoundation',
'-framework', 'CoreServices',
'-framework', 'Security',
]
elif platform.system() == 'Linux':
libs = ['rt', 'dl', 'm']
extra_link_args = []
else:
raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
target_dir = os.environ.get("CARGO_TARGET_DIR")
if target_dir is None:
target_dir = os.path.join(projdir, 'target')
objs = [os.path.join(target_dir, target, 'libdeltachat.a')]
assert os.path.exists(objs[0]), objs
incs = [os.path.join(projdir, 'deltachat-ffi')]
def local_build_flags(projdir, target):
"""Construct build flags for building against a checkout.
:param projdir: The root directory of the deltachat-core-rust project.
:param target: The rust build target, `debug` or `release`.
"""
flags = types.SimpleNamespace()
if platform.system() == 'Darwin':
flags.libs = ['resolv', 'dl']
flags.extra_link_args = [
'-framework', 'CoreFoundation',
'-framework', 'CoreServices',
'-framework', 'Security',
]
elif platform.system() == 'Linux':
flags.libs = ['rt', 'dl', 'm']
flags.extra_link_args = []
else:
libs = ['deltachat']
objs = []
incs = []
extra_link_args = []
builder = cffi.FFI()
builder.set_source(
'deltachat.capi',
"""
#include <deltachat.h>
int dc_event_has_string_data(int e)
{
return DC_EVENT_DATA2_IS_STRING(e);
}
""",
include_dirs=incs,
libraries=libs,
extra_objects=objs,
extra_link_args=extra_link_args,
)
builder.cdef("""
typedef int... time_t;
void free(void *ptr);
extern int dc_event_has_string_data(int);
""")
raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
target_dir = os.environ.get("CARGO_TARGET_DIR")
if target_dir is None:
target_dir = os.path.join(projdir, 'target')
flags.objs = [os.path.join(target_dir, target, 'libdeltachat.a')]
assert os.path.exists(flags.objs[0]), flags.objs
flags.incs = [os.path.join(projdir, 'deltachat-ffi')]
return flags
def system_build_flags():
"""Construct build flags for building against an installed libdeltachat."""
flags = types.SimpleNamespace()
flags.libs = ['deltachat']
flags.objs = []
flags.incs = []
flags.extra_link_args = []
def extract_functions(flags):
"""Extract the function definitions from deltachat.h.
This creates a .h file with a single `#include <deltachat.h>` line
in it. It then runs the C preprocessor to create an output file
which contains all function definitions found in `deltachat.h`.
"""
distutils.log.set_verbosity(distutils.log.INFO)
cc = distutils.ccompiler.new_compiler(force=True)
distutils.sysconfig.customize_compiler(cc)
@@ -71,13 +70,133 @@ def ffibuilder():
src_fp.write('#include <deltachat.h>')
cc.preprocess(source=src_name,
output_file=dst_name,
include_dirs=incs,
include_dirs=flags.incs,
macros=[('PY_CFFI', '1')])
with open(dst_name, "r") as dst_fp:
builder.cdef(dst_fp.read())
return dst_fp.read()
finally:
shutil.rmtree(tmpdir)
def find_header(flags):
"""Use the compiler to find the deltachat.h header location.
This uses a small utility in deltachat.h to find the location of
the header file location.
"""
distutils.log.set_verbosity(distutils.log.INFO)
cc = distutils.ccompiler.new_compiler(force=True)
distutils.sysconfig.customize_compiler(cc)
tmpdir = tempfile.mkdtemp()
try:
src_name = os.path.join(tmpdir, "where.c")
obj_name = os.path.join(tmpdir, "where.o")
dst_name = os.path.join(tmpdir, "where")
with open(src_name, "w") as src_fp:
src_fp.write(textwrap.dedent("""
#include <stdio.h>
#include <deltachat.h>
int main(void) {
printf("%s", _dc_header_file_location());
return 0;
}
"""))
cwd = os.getcwd()
try:
os.chdir(tmpdir)
cc.compile(sources=["where.c"],
include_dirs=flags.incs,
macros=[("PY_CFFI_INC", "1")])
finally:
os.chdir(cwd)
cc.link_executable(objects=[obj_name],
output_progname="where",
output_dir=tmpdir)
return subprocess.check_output(dst_name)
finally:
shutil.rmtree(tmpdir)
def extract_defines(flags):
"""Extract the required #DEFINEs from deltachat.h.
Since #DEFINEs are interpreted by the C preprocessor we can not
use the compiler to extract these and need to parse the header
file ourselves.
The defines are returned in a string that can be passed to CFFIs
cdef() method.
"""
header = find_header(flags)
defines_re = re.compile(r"""
\#define\s+ # The start of a define.
( # Begin capturing group which captures the define name.
(?: # A nested group which is not captured, this allows us
# to build the list of prefixes to extract without
# creation another capture group.
DC_EVENT
| DC_QR
| DC_MSG
| DC_LP
| DC_EMPTY
| DC_CERTCK
| DC_STATE
| DC_STR
| DC_CONTACT_ID
| DC_GCL
| DC_CHAT
| DC_PROVIDER
| DC_KEY_GEN
) # End of prefix matching
_[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
) # Close the capturing group, this contains
# the entire name e.g. DC_MSG_TEXT.
\s+\S+ # Ensure there is whitespace followed by a value.
""", re.VERBOSE)
defines = []
with open(header) as fp:
for line in fp:
match = defines_re.match(line)
if match:
defines.append(match.group(1))
return '\n'.join('#define {} ...'.format(d) for d in defines)
def ffibuilder():
projdir = os.environ.get('DCC_RS_DEV')
if not projdir:
p = dn(dn(dn(dn(abspath(__file__)))))
projdir = os.environ["DCC_RS_DEV"] = p
target = os.environ.get('DCC_RS_TARGET', 'release')
if projdir:
flags = local_build_flags(projdir, target)
else:
flags = system_build_flags()
builder = cffi.FFI()
builder.set_source(
'deltachat.capi',
"""
#include <deltachat.h>
int dc_event_has_string_data(int e)
{
return DC_EVENT_DATA2_IS_STRING(e);
}
""",
include_dirs=flags.incs,
libraries=flags.libs,
extra_objects=flags.objs,
extra_link_args=flags.extra_link_args,
)
builder.cdef("""
typedef int... time_t;
void free(void *ptr);
extern int dc_event_has_string_data(int);
""")
function_defs = extract_functions(flags)
defines = extract_defines(flags)
builder.cdef(function_defs)
builder.cdef(defines)
return builder

View File

@@ -213,22 +213,40 @@ class Account(object):
"""
return Contact(self, const.DC_CONTACT_ID_SELF)
def create_contact(self, email, name=None):
""" create a (new) Contact. If there already is a Contact
with that e-mail address, it is unblocked and its name is
updated.
def create_contact(self, obj, name=None):
""" create a (new) Contact or return an existing one.
:param email: email-address (text type)
:param name: display name for this contact (optional)
Calling this method will always resulut 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.
:param obj: email-address, Account or Contact instance.
:param name: (optional) display name for this contact
:returns: :class:`deltachat.contact.Contact` instance.
"""
realname, addr = parseaddr(email)
if name:
realname = name
realname = as_dc_charpointer(realname)
if isinstance(obj, Account):
if not obj.is_configured():
raise ValueError("can only add addresses from configured accounts")
addr, displayname = obj.get_config("addr"), obj.get_config("displayname")
elif isinstance(obj, Contact):
if obj.account != self:
raise ValueError("account mismatch {}".format(obj))
addr, displayname = obj.addr, obj.name
elif isinstance(obj, str):
displayname, addr = parseaddr(obj)
else:
raise TypeError("don't know how to create chat for %r" % (obj, ))
if name is None and displayname:
name = displayname
return self._create_contact(addr, name)
def _create_contact(self, addr, name):
addr = as_dc_charpointer(addr)
contact_id = lib.dc_create_contact(self._dc_context, realname, addr)
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
name = as_dc_charpointer(name)
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL, contact_id
return Contact(self, contact_id)
def delete_contact(self, contact):
@@ -250,6 +268,13 @@ class Account(object):
if contact_id:
return self.get_contact_by_id(contact_id)
def get_contact_by_id(self, contact_id):
""" return Contact instance or None.
:param contact_id: integer id of this contact.
:returns: None or :class:`deltachat.contact.Contact` instance.
"""
return Contact(self, contact_id)
def get_contacts(self, query=None, with_self=False, only_verified=False):
""" get a (filtered) list of contacts.
@@ -279,53 +304,29 @@ class Account(object):
)
yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
def create_chat_by_contact(self, contact):
""" create or get an existing 1:1 chat object for the specified contact or contact id.
def create_chat(self, obj):
""" Create a 1:1 chat with Account, Contact or e-mail address. """
return self.create_contact(obj).create_chat()
:param contact: chat_id (int) or contact object.
:returns: a :class:`deltachat.chat.Chat` object.
"""
if hasattr(contact, "id"):
if contact.account != self:
raise ValueError("Contact belongs to a different Account")
contact_id = contact.id
else:
assert isinstance(contact, int)
contact_id = contact
chat_id = lib.dc_create_chat_by_contact_id(self._dc_context, contact_id)
return Chat(self, chat_id)
def _create_chat_by_message_id(self, msg_id):
return Chat(self, lib.dc_create_chat_by_msg_id(self._dc_context, msg_id))
def create_chat_by_message(self, message):
""" create or get an existing chat object for the
the specified message.
If this message is in the deaddrop chat then
the sender will become an accepted contact.
:param message: messsage id or message instance.
:returns: a :class:`deltachat.chat.Chat` object.
"""
if hasattr(message, "id"):
if message.account != self:
raise ValueError("Message belongs to a different Account")
msg_id = message.id
else:
assert isinstance(message, int)
msg_id = message
chat_id = lib.dc_create_chat_by_msg_id(self._dc_context, msg_id)
return Chat(self, chat_id)
def create_group_chat(self, name, verified=False):
def create_group_chat(self, name, contacts=None, verified=False):
""" create a new group chat object.
Chats are unpromoted until the first message is sent.
:param contacts: list of contacts to add
:param verified: if true only verified contacts can be added.
:returns: a :class:`deltachat.chat.Chat` object.
"""
bytes_name = name.encode("utf8")
chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name)
return Chat(self, chat_id)
chat = Chat(self, chat_id)
if contacts is not None:
for contact in contacts:
chat.add_contact(contact)
return chat
def get_chats(self):
""" return list of chats.
@@ -354,13 +355,6 @@ class Account(object):
"""
return Message.from_db(self, msg_id)
def get_contact_by_id(self, contact_id):
""" return Contact instance or None.
:param contact_id: integer id of this contact.
:returns: None or :class:`deltachat.contact.Contact` instance.
"""
return Contact(self, contact_id)
def get_chat_by_id(self, chat_id):
""" return Chat instance.
:param chat_id: integer id of this chat.
@@ -567,29 +561,24 @@ class Account(object):
:raises MissingCredentials: if `addr` and `mail_pw` values are not set.
:raises ConfigureFailed: if the account could not be configured.
:returns: None (account is configured and with io-scheduling running)
:returns: None
"""
if not self.is_configured():
raise ValueError("account not configured, cannot start io")
lib.dc_start_io(self._dc_context)
def configure(self):
""" Start configuration process and return a Configtracker instance
on which you can block with wait_finish() to get a True/False success
value for the configuration process.
"""
assert not self.is_configured()
assert not hasattr(self, "_configtracker")
if not self.get_config("addr") or not self.get_config("mail_pw"):
raise MissingCredentials("addr or mail_pwd not set in config")
if hasattr(self, "_configtracker"):
self.remove_account_plugin(self._configtracker)
self._configtracker = ConfigureTracker()
self.add_account_plugin(self._configtracker)
configtracker = ConfigureTracker(self)
self.add_account_plugin(configtracker)
lib.dc_configure(self._dc_context)
def wait_configure_finish(self):
try:
self._configtracker.wait_finish()
finally:
self.remove_account_plugin(self._configtracker)
del self._configtracker
return configtracker
def is_started(self):
return self._event_thread.is_alive() and bool(lib.dc_is_io_running(self._dc_context))

View File

@@ -18,6 +18,8 @@ class Chat(object):
"""
def __init__(self, account, id):
from .account import Account
assert isinstance(account, Account), repr(account)
self.account = account
self.id = id
@@ -328,33 +330,34 @@ class Chat(object):
# ------ group management API ------------------------------
def add_contact(self, contact):
def add_contact(self, obj):
""" add a contact to this chat.
:params: contact object.
:params obj: Contact, Account or e-mail address.
:raises ValueError: if contact could not be added
:returns: None
"""
contact = self.account.create_contact(obj)
ret = lib.dc_add_contact_to_chat(self.account._dc_context, self.id, contact.id)
if ret != 1:
raise ValueError("could not add contact {!r} to chat".format(contact))
return contact
def remove_contact(self, contact):
def remove_contact(self, obj):
""" remove a contact from this chat.
:params: contact object.
:params obj: Contact, Account or e-mail address.
:raises ValueError: if contact could not be removed
:returns: None
"""
contact = self.account.create_contact(obj)
ret = lib.dc_remove_contact_from_chat(self.account._dc_context, self.id, contact.id)
if ret != 1:
raise ValueError("could not remove contact {!r} from chat".format(contact))
def get_contacts(self):
""" get all contacts for this chat.
:params: contact object.
:returns: list of :class:`deltachat.contact.Contact` objects for this chat
"""
from .contact import Contact
dc_array = ffi.gc(
@@ -365,6 +368,14 @@ class Chat(object):
dc_array, lambda id: Contact(self.account, id))
)
def num_contacts(self):
""" return number of contacts in this chat. """
dc_array = ffi.gc(
lib.dc_get_chat_contacts(self.account._dc_context, self.id),
lib.dc_array_unref
)
return lib.dc_array_get_cnt(dc_array)
def set_profile_image(self, img_path):
"""Set group profile image.

View File

@@ -1,197 +1,7 @@
import sys
import re
import os
from os.path import dirname, abspath
from os.path import join as joinpath
# the following const are generated from deltachat.h
# this works well when you in a git-checkout
# run "python deltachat/const.py" to regenerate events
# begin const generated
DC_GCL_ARCHIVED_ONLY = 0x01
DC_GCL_NO_SPECIALS = 0x02
DC_GCL_ADD_ALLDONE_HINT = 0x04
DC_GCL_FOR_FORWARDING = 0x08
DC_GCL_VERIFIED_ONLY = 0x01
DC_GCL_ADD_SELF = 0x02
DC_QR_ASK_VERIFYCONTACT = 200
DC_QR_ASK_VERIFYGROUP = 202
DC_QR_FPR_OK = 210
DC_QR_FPR_MISMATCH = 220
DC_QR_FPR_WITHOUT_ADDR = 230
DC_QR_ACCOUNT = 250
DC_QR_ADDR = 320
DC_QR_TEXT = 330
DC_QR_URL = 332
DC_QR_ERROR = 400
DC_CHAT_ID_DEADDROP = 1
DC_CHAT_ID_TRASH = 3
DC_CHAT_ID_MSGS_IN_CREATION = 4
DC_CHAT_ID_STARRED = 5
DC_CHAT_ID_ARCHIVED_LINK = 6
DC_CHAT_ID_ALLDONE_HINT = 7
DC_CHAT_ID_LAST_SPECIAL = 9
DC_CHAT_TYPE_UNDEFINED = 0
DC_CHAT_TYPE_SINGLE = 100
DC_CHAT_TYPE_GROUP = 120
DC_CHAT_TYPE_VERIFIED_GROUP = 130
DC_MSG_ID_MARKER1 = 1
DC_MSG_ID_DAYMARKER = 9
DC_MSG_ID_LAST_SPECIAL = 9
DC_STATE_UNDEFINED = 0
DC_STATE_IN_FRESH = 10
DC_STATE_IN_NOTICED = 13
DC_STATE_IN_SEEN = 16
DC_STATE_OUT_PREPARING = 18
DC_STATE_OUT_DRAFT = 19
DC_STATE_OUT_PENDING = 20
DC_STATE_OUT_FAILED = 24
DC_STATE_OUT_DELIVERED = 26
DC_STATE_OUT_MDN_RCVD = 28
DC_CONTACT_ID_SELF = 1
DC_CONTACT_ID_INFO = 2
DC_CONTACT_ID_DEVICE = 5
DC_CONTACT_ID_LAST_SPECIAL = 9
DC_MSG_TEXT = 10
DC_MSG_IMAGE = 20
DC_MSG_GIF = 21
DC_MSG_STICKER = 23
DC_MSG_AUDIO = 40
DC_MSG_VOICE = 41
DC_MSG_VIDEO = 50
DC_MSG_FILE = 60
DC_LP_AUTH_OAUTH2 = 0x2
DC_LP_AUTH_NORMAL = 0x4
DC_LP_IMAP_SOCKET_STARTTLS = 0x100
DC_LP_IMAP_SOCKET_SSL = 0x200
DC_LP_IMAP_SOCKET_PLAIN = 0x400
DC_LP_SMTP_SOCKET_STARTTLS = 0x10000
DC_LP_SMTP_SOCKET_SSL = 0x20000
DC_LP_SMTP_SOCKET_PLAIN = 0x40000
DC_CERTCK_AUTO = 0
DC_CERTCK_STRICT = 1
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3
DC_EMPTY_MVBOX = 0x01
DC_EMPTY_INBOX = 0x02
DC_EVENT_INFO = 100
DC_EVENT_SMTP_CONNECTED = 101
DC_EVENT_IMAP_CONNECTED = 102
DC_EVENT_SMTP_MESSAGE_SENT = 103
DC_EVENT_IMAP_MESSAGE_DELETED = 104
DC_EVENT_IMAP_MESSAGE_MOVED = 105
DC_EVENT_IMAP_FOLDER_EMPTIED = 106
DC_EVENT_NEW_BLOB_FILE = 150
DC_EVENT_DELETED_BLOB_FILE = 151
DC_EVENT_WARNING = 300
DC_EVENT_ERROR = 400
DC_EVENT_ERROR_NETWORK = 401
DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410
DC_EVENT_MSGS_CHANGED = 2000
DC_EVENT_INCOMING_MSG = 2005
DC_EVENT_MSG_DELIVERED = 2010
DC_EVENT_MSG_FAILED = 2012
DC_EVENT_MSG_READ = 2015
DC_EVENT_CHAT_MODIFIED = 2020
DC_EVENT_CONTACTS_CHANGED = 2030
DC_EVENT_LOCATION_CHANGED = 2035
DC_EVENT_CONFIGURE_PROGRESS = 2041
DC_EVENT_IMEX_PROGRESS = 2051
DC_EVENT_IMEX_FILE_WRITTEN = 2052
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
DC_EVENT_FILE_COPIED = 2055
DC_EVENT_IS_OFFLINE = 2081
DC_EVENT_GET_STRING = 2091
DC_STR_SELFNOTINGRP = 21
DC_KEY_GEN_DEFAULT = 0
DC_KEY_GEN_RSA2048 = 1
DC_KEY_GEN_ED25519 = 2
DC_PROVIDER_STATUS_OK = 1
DC_PROVIDER_STATUS_PREPARATION = 2
DC_PROVIDER_STATUS_BROKEN = 3
DC_CHAT_VISIBILITY_NORMAL = 0
DC_CHAT_VISIBILITY_ARCHIVED = 1
DC_CHAT_VISIBILITY_PINNED = 2
DC_STR_NOMESSAGES = 1
DC_STR_SELF = 2
DC_STR_DRAFT = 3
DC_STR_VOICEMESSAGE = 7
DC_STR_DEADDROP = 8
DC_STR_IMAGE = 9
DC_STR_VIDEO = 10
DC_STR_AUDIO = 11
DC_STR_FILE = 12
DC_STR_STATUSLINE = 13
DC_STR_NEWGROUPDRAFT = 14
DC_STR_MSGGRPNAME = 15
DC_STR_MSGGRPIMGCHANGED = 16
DC_STR_MSGADDMEMBER = 17
DC_STR_MSGDELMEMBER = 18
DC_STR_MSGGROUPLEFT = 19
DC_STR_GIF = 23
DC_STR_ENCRYPTEDMSG = 24
DC_STR_E2E_AVAILABLE = 25
DC_STR_ENCR_TRANSP = 27
DC_STR_ENCR_NONE = 28
DC_STR_CANTDECRYPT_MSG_BODY = 29
DC_STR_FINGERPRINTS = 30
DC_STR_READRCPT = 31
DC_STR_READRCPT_MAILBODY = 32
DC_STR_MSGGRPIMGDELETED = 33
DC_STR_E2E_PREFERRED = 34
DC_STR_CONTACT_VERIFIED = 35
DC_STR_CONTACT_NOT_VERIFIED = 36
DC_STR_CONTACT_SETUP_CHANGED = 37
DC_STR_ARCHIVEDCHATS = 40
DC_STR_STARREDMSGS = 41
DC_STR_AC_SETUP_MSG_SUBJECT = 42
DC_STR_AC_SETUP_MSG_BODY = 43
DC_STR_CANNOT_LOGIN = 60
DC_STR_SERVER_RESPONSE = 61
DC_STR_MSGACTIONBYUSER = 62
DC_STR_MSGACTIONBYME = 63
DC_STR_MSGLOCATIONENABLED = 64
DC_STR_MSGLOCATIONDISABLED = 65
DC_STR_LOCATION = 66
DC_STR_STICKER = 67
DC_STR_DEVICE_MESSAGES = 68
DC_STR_COUNT = 68
# end const generated
from .capi import lib
def read_event_defines(f):
rex = re.compile(r'#define\s+((?:DC_EVENT|DC_QR|DC_MSG|DC_LP|DC_EMPTY|DC_CERTCK|DC_STATE|DC_STR|'
r'DC_CONTACT_ID|DC_GCL|DC_CHAT|DC_PROVIDER|DC_KEY_GEN)_\S+)\s+([x\d]+).*')
for line in f:
m = rex.match(line)
if m:
yield m.groups()
if __name__ == "__main__":
here = abspath(__file__).rstrip("oc")
here_dir = dirname(here)
if len(sys.argv) >= 2:
deltah = sys.argv[1]
else:
deltah = joinpath(dirname(dirname(dirname(here_dir))), "deltachat-ffi", "deltachat.h")
assert os.path.exists(deltah)
lines = []
skip_to_end = False
for orig_line in open(here):
if skip_to_end:
if not orig_line.startswith("# end const"):
continue
skip_to_end = False
lines.append(orig_line)
if orig_line.startswith("# begin const"):
with open(deltah) as f:
for name, item in read_event_defines(f):
lines.append("{} = {}\n".format(name, item))
skip_to_end = True
tmpname = here + ".tmp"
with open(tmpname, "w") as f:
f.write("".join(lines))
os.rename(tmpname, here)
for name in dir(lib):
if name.startswith("DC_"):
globals()[name] = getattr(lib, name)
del name

View File

@@ -3,6 +3,8 @@
from . import props
from .cutil import from_dc_charpointer
from .capi import lib, ffi
from .chat import Chat
from . import const
class Contact(object):
@@ -11,6 +13,8 @@ class Contact(object):
You obtain instances of it through :class:`deltachat.account.Account`.
"""
def __init__(self, account, id):
from .account import Account
assert isinstance(account, Account), repr(account)
self.account = account
self.id = id
@@ -36,10 +40,13 @@ class Contact(object):
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
@props.with_doc
def display_name(self):
def name(self):
""" display name for this contact. """
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
# deprecated alias
display_name = name
def is_blocked(self):
""" Return True if the contact is blocked. """
return lib.dc_contact_is_blocked(self._dc_contact)
@@ -58,6 +65,16 @@ class Contact(object):
return None
return from_dc_charpointer(dc_res)
def get_chat(self):
"""return 1:1 chat for this contact. """
return self.account.create_chat_by_contact(self)
def create_chat(self):
""" create or get an existing 1:1 chat object for the specified contact or contact id.
:param contact: chat_id (int) or contact object.
:returns: a :class:`deltachat.chat.Chat` object.
"""
dc_context = self.account._dc_context
chat_id = lib.dc_create_chat_by_contact_id(dc_context, self.id)
assert chat_id > const.DC_CHAT_ID_LAST_SPECIAL, chat_id
return Chat(self.account, chat_id)
# deprecated name
get_chat = create_chat

View File

@@ -0,0 +1,213 @@
"""
Internal Python-level IMAP handling used by the testplugin
and for cleaning up inbox/mvbox for each test function run.
"""
import io
import email
import ssl
import pathlib
from imapclient import IMAPClient
from imapclient.exceptions import IMAPClientError
import deltachat
SEEN = b'\\Seen'
DELETED = b'\\Deleted'
FLAGS = b'FLAGS'
FETCH = b'FETCH'
ALL = "1:*"
@deltachat.global_hookimpl
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"):
imap.delete(ALL, expunge=True)
assert imap.select_config_folder("inbox")
imap.delete(ALL, expunge=True)
setattr(account, "direct_imap", imap)
@deltachat.global_hookimpl
def dc_account_after_shutdown(account):
""" shutdown the imap connection if there is one. """
imap = getattr(account, "direct_imap", None)
if imap is not None:
imap.shutdown()
del account.direct_imap
class DirectImap:
def __init__(self, account):
self.account = account
self.logid = account.get_config("displayname") or id(account)
self._idling = False
self.connect()
def connect(self):
ssl_context = ssl.create_default_context()
# don't check if certificate hostname doesn't match target hostname
ssl_context.check_hostname = False
# don't check if the certificate is trusted by a certificate authority
ssl_context.verify_mode = ssl.CERT_NONE
host = self.account.get_config("configured_mail_server")
user = self.account.get_config("addr")
pw = self.account.get_config("mail_pw")
self.conn = IMAPClient(host, ssl_context=ssl_context)
self.conn.login(user, pw)
self.select_folder("INBOX")
def shutdown(self):
try:
self.conn.idle_done()
except (OSError, IMAPClientError):
pass
try:
self.conn.logout()
except (OSError, IMAPClientError):
print("Could not logout direct_imap conn")
def select_folder(self, foldername):
assert not self._idling
return self.conn.select_folder(foldername)
def select_config_folder(self, config_name):
""" Return info about selected folder if it is
configured, otherwise None. """
if "_" not in config_name:
config_name = "configured_{}_folder".format(config_name)
foldername = self.account.get_config(config_name)
if foldername:
return self.select_folder(foldername)
def list_folders(self):
""" return list of all existing folder names"""
assert not self._idling
folders = []
for meta, sep, foldername in self.conn.list_folders():
folders.append(foldername)
return folders
def delete(self, range, expunge=True):
""" delete a range of messages (imap-syntax).
If expunge is true, perform the expunge-operation
to make sure the messages are really gone and not
just flagged as deleted.
"""
self.conn.set_flags(range, [DELETED])
if expunge:
self.conn.expunge()
def get_all_messages(self):
assert not self._idling
return self.conn.fetch(ALL, [FLAGS])
def get_unread_messages(self):
assert not self._idling
res = self.conn.fetch(ALL, [FLAGS])
return [uid for uid in res
if SEEN not in res[uid][FLAGS]]
def mark_all_read(self):
messages = self.get_unread_messages()
if messages:
res = self.conn.set_flags(messages, [SEEN])
print("marked seen:", messages, res)
def get_unread_cnt(self):
return len(self.get_unread_messages())
def dump_account_info(self, logfile):
def log(*args, **kwargs):
kwargs["file"] = logfile
print(*args, **kwargs)
cursor = 0
for name, val in self.account.get_info().items():
entry = "{}={}".format(name.upper(), val)
if cursor + len(entry) > 80:
log("")
cursor = 0
log(entry, end=" ")
cursor += len(entry) + 1
log("")
def dump_imap_structures(self, dir, logfile):
assert not self._idling
stream = io.StringIO()
def log(*args, **kwargs):
kwargs["file"] = stream
print(*args, **kwargs)
empty_folders = []
for imapfolder in self.list_folders():
self.select_folder(imapfolder)
messages = list(self.get_all_messages())
if not messages:
empty_folders.append(imapfolder)
continue
log("---------", imapfolder, len(messages), "messages ---------")
# get message content without auto-marking it as seen
# fetching 'RFC822' would mark it as seen.
requested = [b'BODY.PEEK[HEADER]', FLAGS]
for uid, data in self.conn.fetch(messages, requested).items():
body_bytes = data[b'BODY[HEADER]']
flags = data[FLAGS]
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
path.mkdir(parents=True, exist_ok=True)
fn = path.joinpath(str(uid))
fn.write_bytes(body_bytes)
log("Message", uid, fn)
email_message = email.message_from_bytes(body_bytes)
log("Message", uid, flags, "Message-Id:", email_message.get("Message-Id"))
if empty_folders:
log("--------- EMPTY FOLDERS:", empty_folders)
print(stream.getvalue(), file=logfile)
def idle_start(self):
""" switch this connection to idle mode. non-blocking. """
assert not self._idling
res = self.conn.idle()
self._idling = True
return res
def idle_check(self, terminate=False):
""" (blocking) wait for next idle message from server. """
assert self._idling
self.account.log("imap-direct: calling idle_check")
res = self.conn.idle_check(timeout=30)
if len(res) == 0:
raise TimeoutError
if terminate:
self.idle_done()
return res
def idle_wait_for_seen(self):
""" Return first message with SEEN flag
from a running idle-stream REtiurn.
"""
while 1:
for item in self.idle_check():
if item[1] == FETCH:
if item[2][0] == FLAGS:
if SEEN in item[2][1]:
return item[0]
def idle_done(self):
""" send idle-done to server if we are currently in idle mode. """
if self._idling:
res = self.conn.idle_done()
self._idling = False
return res

View File

@@ -28,13 +28,9 @@ class FFIEventLogger:
# to prevent garbled logging
_loglock = threading.RLock()
def __init__(self, account, logid):
"""
:param logid: an optional logging prefix that should be used with
the default internal logging.
"""
def __init__(self, account):
self.account = account
self.logid = logid
self.logid = self.account.get_config("displayname")
self.init_time = time.time()
@account_hookimpl
@@ -127,6 +123,12 @@ class FFIEventTracker:
if ev.data2 > 0:
return self.account.get_message_by_id(ev.data2)
def wait_msg_delivered(self, msg):
ev = self.get_matching("DC_EVENT_MSG_DELIVERED")
assert ev.data1 == msg.chat.id
assert ev.data2 == msg.id
assert msg.is_out_delivered()
class EventThread(threading.Thread):
""" Event Thread for an account.

View File

@@ -43,7 +43,7 @@ class PerAccount:
@account_hookspec
def ac_configure_completed(self, success):
""" Called when a configure process completed. """
""" Called after a configure process completed. """
@account_hookspec
def ac_incoming_message(self, message):
@@ -88,6 +88,14 @@ class Global:
def dc_account_init(self, account):
""" called when `Account::__init__()` function starts executing. """
@global_hookspec
def dc_account_extra_configure(self, account):
""" Called when account configuration successfully finished.
This hook can be used to perform extra work before
ac_configure_completed is called.
"""
@global_hookspec
def dc_account_after_shutdown(self, account):
""" Called after the account has been shutdown. """

View File

@@ -53,15 +53,19 @@ class Message(object):
lib.dc_msg_unref
))
def accept_sender_contact(self):
""" ensure that the sender is an accepted contact
and that the message has a non-deaddrop chat object.
def create_chat(self):
""" create or get an existing chat (group) object for this message.
If the message is a deaddrop contact request
the sender will become an accepted contact.
:returns: a :class:`deltachat.chat.Chat` object.
"""
self.account.create_chat_by_message(self)
self._dc_msg = ffi.gc(
lib.dc_get_msg(self.account._dc_context, self.id),
lib.dc_msg_unref
)
from .chat import Chat
chat_id = lib.dc_create_chat_by_msg_id(self.account._dc_context, self.id)
ctx = self.account._dc_context
self._dc_msg = ffi.gc(lib.dc_get_msg(ctx, self.id), lib.dc_msg_unref)
return Chat(self.account, chat_id)
@props.with_doc
def text(self):

View File

@@ -1,6 +1,7 @@
from __future__ import print_function
import os
import sys
import io
import subprocess
import queue
import threading
@@ -16,6 +17,7 @@ from . import Account, const
from .capi import lib
from .events import FFIEventLogger, FFIEventTracker
from _pytest._code import Source
from deltachat import direct_imap
import deltachat
@@ -33,9 +35,6 @@ def pytest_addoption(parser):
def pytest_configure(config):
config.addinivalue_line(
"markers", "ignored: Mark test as bing slow, skipped unless --ignored is used."
)
cfg = config.getoption('--liveconfig')
if not cfg:
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
@@ -216,6 +215,7 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
self._generated_keys = ["alice", "bob", "charlie",
"dom", "elena", "fiona"]
self.set_logging_default(False)
deltachat.register_global_plugin(direct_imap)
def finalize(self):
while self._finalizers:
@@ -226,13 +226,15 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
acc = self._accounts.pop()
acc.shutdown()
acc.disable_logging()
deltachat.unregister_global_plugin(direct_imap)
def make_account(self, path, logid, quiet=False):
ac = Account(path, logging=self._logging)
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
ac.addr = ac.get_self_contact().addr
ac.set_config("displayname", logid)
if not quiet:
ac.add_account_plugin(FFIEventLogger(ac, logid=logid))
ac.add_account_plugin(FFIEventLogger(ac))
self._accounts.append(ac)
return ac
@@ -301,31 +303,27 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
configdict["mvbox_move"] = str(int(move))
configdict["sentbox_watch"] = str(int(sentbox))
ac.update_config(configdict)
ac.configure()
ac._configtracker = ac.configure()
return ac
def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False):
ac1 = self.get_online_configuring_account(
pre_generated_key=pre_generated_key, mvbox=mvbox, move=move)
ac1.wait_configure_finish()
ac1.start_io()
self.wait_configure_and_start_io()
return ac1
def get_two_online_accounts(self, move=False, quiet=False):
ac1 = self.get_online_configuring_account(move=True, quiet=quiet)
ac1 = self.get_online_configuring_account(move=move, quiet=quiet)
ac2 = self.get_online_configuring_account(quiet=quiet)
ac1.wait_configure_finish()
ac1.start_io()
ac2.wait_configure_finish()
ac2.start_io()
self.wait_configure_and_start_io()
return ac1, ac2
def get_many_online_accounts(self, num, move=True, quiet=True):
accounts = [self.get_online_configuring_account(move=move, quiet=quiet)
def get_many_online_accounts(self, num, move=True):
accounts = [self.get_online_configuring_account(move=move, quiet=True)
for i in range(num)]
self.wait_configure_and_start_io()
for acc in accounts:
acc._configtracker.wait_finish()
acc.start_io()
acc.add_account_plugin(FFIEventLogger(acc))
return accounts
def clone_online_account(self, account, pre_generated_key=True):
@@ -343,14 +341,29 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
mvbox_move=account.get_config("mvbox_move"),
sentbox_watch=account.get_config("sentbox_watch"),
))
ac.configure()
ac._configtracker = ac.configure()
return ac
def wait_configure_and_start_io(self):
for acc in self._accounts:
if hasattr(acc, "_configtracker"):
acc._configtracker.wait_finish()
del acc._configtracker
acc.set_config("bcc_self", "0")
if acc.is_configured() and not acc.is_started():
acc.start_io()
print("{}: {} account was successfully setup".format(
acc.get_config("displayname"), acc.get_config("addr")))
def run_bot_process(self, module, ffi=True):
fn = module.__file__
bot_ac, bot_cfg = self.get_online_config()
# Avoid starting ac so we don't interfere with the bot operating on
# the same database.
self._accounts.remove(bot_ac)
args = [
sys.executable,
"-u",
@@ -375,9 +388,42 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
self._finalizers.append(bot.kill)
return bot
def dump_imap_summary(self, logfile):
for ac in self._accounts:
imap = getattr(ac, "direct_imap", None)
if imap is not None:
try:
imap.idle_done()
except Exception:
pass
imap.dump_account_info(logfile=logfile)
imap.dump_imap_structures(tmpdir, logfile=logfile)
def get_accepted_chat(self, ac1, ac2):
ac2.create_chat(ac1)
return ac1.create_chat(ac2)
def introduce_each_other(self, accounts, sending=True):
to_wait = []
for i, acc in enumerate(accounts):
for acc2 in accounts[i + 1:]:
chat = self.get_accepted_chat(acc, acc2)
if sending:
chat.send_text("hi")
to_wait.append(acc2)
acc2.create_chat(acc).send_text("hi back")
to_wait.append(acc)
for acc in to_wait:
acc._evtracker.wait_next_incoming_message()
am = AccountMaker()
request.addfinalizer(am.finalize)
return am
yield am
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
logfile = io.StringIO()
am.dump_imap_summary(logfile=logfile)
print(logfile.getvalue())
# request.node.add_report_section("call", "imap-server-state", s)
class BotProcess:
@@ -445,4 +491,17 @@ def lp():
def step(self, msg):
print("-" * 5, "step " + msg, "-" * 5)
return Printer()
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
# execute all other hooks to obtain the report object
outcome = yield
rep = outcome.get_result()
# set a report attribute for each phase of a call, which can
# be "setup", "call", "teardown"
setattr(item, "rep_" + rep.when, rep)

View File

@@ -2,7 +2,7 @@
from queue import Queue
from threading import Event
from .hookspec import account_hookimpl
from .hookspec import account_hookimpl, Global
class ImexFailed(RuntimeError):
@@ -40,12 +40,14 @@ class ConfigureFailed(RuntimeError):
class ConfigureTracker:
ConfigureFailed = ConfigureFailed
def __init__(self):
def __init__(self, account):
self.account = account
self._configure_events = Queue()
self._smtp_finished = Event()
self._imap_finished = Event()
self._ffi_events = []
self._progress = Queue()
self._gm = Global._get_plugin_manager()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
@@ -59,7 +61,10 @@ class ConfigureTracker:
@account_hookimpl
def ac_configure_completed(self, success):
if success:
self._gm.hook.dc_account_extra_configure(account=self.account)
self._configure_events.put(success)
self.account.remove_account_plugin(self)
def wait_smtp_connected(self):
""" wait until smtp is configured. """

View File

@@ -110,7 +110,7 @@ class AutoReplier:
if self.current_sent >= self.num_send:
self.report_func(self, ReportType.exit)
return
message.accept_sender_contact()
message.create_chat()
message.mark_seen()
self.log("incoming message: {}".format(message))

File diff suppressed because it is too large Load Diff

View File

@@ -36,9 +36,7 @@ def wait_msgs_changed(account, msgs_list):
class TestOnlineInCreation:
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
chat = ac1.create_chat(ac2)
lp.sec("Creating in-creation file outside of blobdir")
assert tmpdir.strpath != ac1.get_blobdir()
@@ -48,9 +46,7 @@ class TestOnlineInCreation:
def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
chat = ac1.create_chat(ac2)
lp.sec("Creating file outside of blobdir")
assert tmpdir.strpath != ac1.get_blobdir()
@@ -64,9 +60,7 @@ class TestOnlineInCreation:
def test_forward_increation(self, acfactory, data, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
chat = ac1.create_chat(ac2)
wait_msgs_changed(ac1, [(0, 0)]) # why no chat id?
lp.sec("create a message with a file in creation")
@@ -80,7 +74,7 @@ class TestOnlineInCreation:
lp.sec("forward the message while still in creation")
chat2 = ac1.create_group_chat("newgroup")
chat2.add_contact(c2)
chat2.add_contact(ac2)
wait_msgs_changed(ac1, [(0, 0)]) # why not chat id?
ac1.forward_messages([prepared_original], chat2)
# XXX there might be two EVENT_MSGS_CHANGED and only one of them

View File

@@ -69,8 +69,8 @@ def test_sig():
def test_markseen_invalid_message_ids(acfactory):
ac1 = acfactory.get_configured_offline_account()
contact1 = ac1.create_contact(email="some1@example.com", name="some1")
chat = ac1.create_chat_by_contact(contact1)
contact1 = ac1.create_contact("some1@example.com", name="some1")
chat = contact1.create_chat()
chat.send_text("one messae")
ac1._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
msg_ids = [9]

View File

@@ -71,6 +71,8 @@ norecursedirs = .tox
xfail_strict=true
timeout = 90
timeout_method = thread
markers =
ignored: ignore this test in default test runs, use --ignored to run.
[flake8]
max-line-length = 120

21
spec.md
View File

@@ -1,10 +1,12 @@
# Chat-over-Email specification
# chat-mail specification
Version 0.30.0
Version: 0.32.0
Status: In-progress
Format: [Semantic Line Breaks](https://sembr.org/)
This document describes how emails can be used
to implement typical messenger functions
while staying compatible to existing MUAs.
This document roughly describes how chat-mail
apps use the standard e-mail system
to implement typical messenger functions.
- [Encryption](#encryption)
- [Outgoing messages](#outgoing-messages)
@@ -30,17 +32,14 @@ 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 [Memoryhole](https://github.com/autocrypt/memoryhole) standard.
If Memoryhole is not used,
the subject of encrypted messages SHOULD be replaced by the string `...`.
by the [Protected Headers](https://www.ietf.org/id/draft-autocrypt-lamps-protected-headers-02.html) standard.
# Outgoing messages
Messengers MUST add a `Chat-Version: 1.0` header to outgoing messages.
For filtering and smart appearance of the messages in normal MUAs,
the `Subject` header SHOULD start with the characters `Chat:`
and SHOULD be an excerpt of the message.
the `Subject` header SHOULD be `Message from <sender name>`.
Replies to messages MAY follow the typical `Re:`-format.
The body MAY contain text which MUST have the content type `text/plain`
@@ -58,7 +57,7 @@ Full quotes, footers or sth. like that MUST NOT go to the user-text-part.
To: rcpt@domain
Chat-Version: 1.0
Content-Type: text/plain
Subject: Chat: Hello ...
Subject: Message from sender@domain
Hello world!

View File

@@ -6,13 +6,15 @@ use std::collections::BTreeMap;
use std::str::FromStr;
use std::{fmt, str};
use deltachat_derive::*;
use crate::contact::*;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, SignedPublicKey};
/// Possible values for encryption preference
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive)]
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive, Sqlx)]
#[repr(u8)]
pub enum EncryptPreference {
NoPreference = 0,

View File

@@ -8,11 +8,15 @@ use async_std::prelude::*;
use async_std::{fs, io};
use image::GenericImageView;
use num_traits::FromPrimitive;
use thiserror::Error;
use crate::constants::AVATAR_SIZE;
use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::error::Error;
use crate::events::Event;
use crate::message;
/// Represents a file in the blob directory.
///
@@ -57,7 +61,7 @@ impl<'a> BlobObject<'a> {
.map_err(|err| BlobError::WriteFailure {
blobdir: blobdir.to_path_buf(),
blobname: name.clone(),
cause: err,
cause: err.into(),
})?;
let blob = BlobObject {
blobdir,
@@ -372,11 +376,73 @@ impl<'a> BlobObject<'a> {
img.save(&blob_abs).map_err(|err| BlobError::WriteFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
cause: err.into(),
})?;
Ok(())
}
pub async fn recode_to_image_size(&self, context: &Context) -> Result<(), BlobError> {
let blob_abs = self.to_abs_path();
if message::guess_msgtype_from_suffix(Path::new(&blob_abs))
!= Some((Viewtype::Image, "image/jpeg"))
{
return Ok(());
}
let img = image::open(&blob_abs).map_err(|err| BlobError::RecodeFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
})?;
let img_wh = if MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await)
.unwrap_or_default()
== MediaQuality::Balanced
{
BALANCED_IMAGE_SIZE
} else {
WORSE_IMAGE_SIZE
};
if img.width() <= img_wh && img.height() <= img_wh {
return Ok(());
}
let mut img = img.thumbnail(img_wh, img_wh);
match self.get_exif_orientation(context) {
Ok(90) => img = img.rotate90(),
Ok(180) => img = img.rotate180(),
Ok(270) => img = img.rotate270(),
_ => {}
}
img.save(&blob_abs).map_err(|err| BlobError::WriteFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err.into(),
})?;
Ok(())
}
pub fn get_exif_orientation(&self, context: &Context) -> Result<i32, Error> {
let file = std::fs::File::open(self.to_abs_path())?;
let mut bufreader = std::io::BufReader::new(&file);
let exifreader = exif::Reader::new();
let exif = exifreader.read_from_container(&mut bufreader)?;
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
// we only use rotation, in practise, flipping is not used.
match orientation.value.get_uint(0) {
Some(3) => return Ok(180),
Some(6) => return Ok(90),
Some(8) => return Ok(270),
other => warn!(context, "exif orientation value ignored: {:?}", other),
}
}
Ok(0)
}
}
impl<'a> fmt::Display for BlobObject<'a> {
@@ -400,7 +466,7 @@ pub enum BlobError {
blobdir: PathBuf,
blobname: String,
#[source]
cause: std::io::Error,
cause: anyhow::Error,
},
#[error("Failed to copy data from {} to blob {blobname} in {}", .src.display(), .blobdir.display())]
CopyFailure {
@@ -431,7 +497,7 @@ mod tests {
#[async_std::test]
async fn test_create() {
let t = dummy_context().await;
let t = TestContext::new().await;
let blob = BlobObject::create(&t.ctx, "foo", b"hello").await.unwrap();
let fname = t.ctx.get_blobdir().join("foo");
let data = fs::read(fname).await.unwrap();
@@ -442,7 +508,7 @@ mod tests {
#[async_std::test]
async fn test_lowercase_ext() {
let t = dummy_context().await;
let t = TestContext::new().await;
let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello")
.await
.unwrap();
@@ -451,7 +517,7 @@ mod tests {
#[async_std::test]
async fn test_as_file_name() {
let t = dummy_context().await;
let t = TestContext::new().await;
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
@@ -460,7 +526,7 @@ mod tests {
#[async_std::test]
async fn test_as_rel_path() {
let t = dummy_context().await;
let t = TestContext::new().await;
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
@@ -469,7 +535,7 @@ mod tests {
#[async_std::test]
async fn test_suffix() {
let t = dummy_context().await;
let t = TestContext::new().await;
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
@@ -480,7 +546,7 @@ mod tests {
#[async_std::test]
async fn test_create_dup() {
let t = dummy_context().await;
let t = TestContext::new().await;
BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
@@ -504,7 +570,7 @@ mod tests {
#[async_std::test]
async fn test_double_ext_preserved() {
let t = dummy_context().await;
let t = TestContext::new().await;
BlobObject::create(&t.ctx, "foo.tar.gz", b"hello")
.await
.unwrap();
@@ -529,7 +595,7 @@ mod tests {
#[async_std::test]
async fn test_create_long_names() {
let t = dummy_context().await;
let t = TestContext::new().await;
let s = "1".repeat(150);
let blob = BlobObject::create(&t.ctx, &s, b"data").await.unwrap();
let blobname = blob.as_name().split('/').last().unwrap();
@@ -538,7 +604,7 @@ mod tests {
#[async_std::test]
async fn test_create_and_copy() {
let t = dummy_context().await;
let t = TestContext::new().await;
let src = t.dir.path().join("src");
fs::write(&src, b"boo").await.unwrap();
let blob = BlobObject::create_and_copy(&t.ctx, &src).await.unwrap();
@@ -554,7 +620,7 @@ mod tests {
#[async_std::test]
async fn test_create_from_path() {
let t = dummy_context().await;
let t = TestContext::new().await;
let src_ext = t.dir.path().join("external");
fs::write(&src_ext, b"boo").await.unwrap();
@@ -572,7 +638,7 @@ mod tests {
}
#[async_std::test]
async fn test_create_from_name_long() {
let t = dummy_context().await;
let t = TestContext::new().await;
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
fs::write(&src_ext, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap();
@@ -594,8 +660,40 @@ mod tests {
#[test]
fn test_sanitise_name() {
let (_, ext) =
let (stem, ext) =
BlobObject::sanitise_name("Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt");
assert_eq!(ext, ".txt");
assert!(!stem.is_empty());
// the extensions are kept together as between stem and extension a number may be added -
// and `foo.tar.gz` should become `foo-1234.tar.gz` and not `foo.tar-1234.gz`
let (stem, ext) = BlobObject::sanitise_name("wot.tar.gz");
assert_eq!(stem, "wot");
assert_eq!(ext, ".tar.gz");
let (stem, ext) = BlobObject::sanitise_name(".foo.bar");
assert_eq!(stem, "");
assert_eq!(ext, ".foo.bar");
let (stem, ext) = BlobObject::sanitise_name("foo?.bar");
assert!(stem.contains("foo"));
assert!(!stem.contains("?"));
assert_eq!(ext, ".bar");
let (stem, ext) = BlobObject::sanitise_name("no-extension");
assert_eq!(stem, "no-extension");
assert_eq!(ext, "");
let (stem, ext) = BlobObject::sanitise_name("path/ignored\\this: is* forbidden?.c");
assert_eq!(ext, ".c");
assert!(!stem.contains("path"));
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("?"));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -105,17 +105,6 @@ impl Chatlist {
let mut add_archived_link_item = false;
let process_row = |row: &rusqlite::Row| {
let chat_id: ChatId = row.get(0)?;
let msg_id: MsgId = row.get(1).unwrap_or_default();
Ok((chat_id, msg_id))
};
let process_rows = |rows: rusqlite::MappedRows<_>| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
};
let skip_id = if flag_for_forwarding {
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
.await
@@ -142,24 +131,23 @@ impl Chatlist {
// shown at all permanent in the chatlist.
let mut ids = if let Some(query_contact_id) = query_contact_id {
// show chats shared with a given contact
context.sql.query_map(
context.sql.query_rows(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.timestamp=(
SELECT MAX(timestamp)
AND m.id=(
SELECT id
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?1))
AND (hidden=0 OR state=?)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9
AND c.blocked=0
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
process_row,
process_rows,
ORDER BY c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsx![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
).await?
} else if flag_archived_only {
// show archived chats
@@ -168,24 +156,23 @@ impl Chatlist {
// and adapting the number requires larger refactorings and seems not to be worth the effort)
context
.sql
.query_map(
.query_rows(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.timestamp=(
SELECT MAX(timestamp)
AND m.id=(
SELECT id
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?))
AND (hidden=0 OR state=?)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9
AND c.blocked=0
AND c.archived=1
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft],
process_row,
process_rows,
paramsx![MessageState::OutDraft],
)
.await?
} else if let Some(query) = query {
@@ -201,24 +188,23 @@ impl Chatlist {
let str_like_cmd = format!("%{}%", query);
context
.sql
.query_map(
.query_rows(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.timestamp=(
SELECT MAX(timestamp)
AND m.id=(
SELECT id
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?1))
AND (hidden=0 OR state=?1)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9 AND c.id!=?2
AND c.blocked=0
AND c.name LIKE ?3
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, skip_id, str_like_cmd],
process_row,
process_rows,
paramsx![MessageState::OutDraft, skip_id, str_like_cmd],
)
.await?
} else {
@@ -231,25 +217,25 @@ impl Chatlist {
} else {
ChatId::new(0)
};
let mut ids = context.sql.query_map(
let mut ids = context.sql.query_rows(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.timestamp=(
SELECT MAX(timestamp)
AND m.id=(
SELECT id
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?1))
AND (hidden=0 OR state=?1)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9 AND c.id!=?2
AND c.blocked=0
AND NOT c.archived=?3
GROUP BY c.id
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
process_row,
process_rows,
paramsx![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
).await?;
if !flag_no_specials {
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await
{
@@ -383,15 +369,15 @@ impl Chatlist {
/// Returns the number of archived chats
pub async fn dc_get_archived_cnt(context: &Context) -> u32 {
context
let v: i32 = context
.sql
.query_get_value(
context,
.query_value(
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
paramsv![],
paramsx![],
)
.await
.unwrap_or_default()
.unwrap_or_default();
v as u32
}
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
@@ -399,21 +385,21 @@ async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
// sufficient as there are typically only few fresh messages.
context
.sql
.query_get_value(
context,
concat!(
"SELECT m.id",
" FROM msgs m",
" LEFT JOIN chats c",
" ON c.id=m.chat_id",
" WHERE m.state=10",
" AND m.hidden=0",
" AND c.blocked=2",
" ORDER BY m.timestamp DESC, m.id DESC;"
),
paramsv![],
.query_value(
r#"
SELECT m.id
FROM msgs m
LEFT JOIN chats c
ON c.id=m.chat_id
WHERE m.state=10
AND m.hidden=0
AND c.blocked=2
ORDER BY m.timestamp DESC, m.id DESC;
"#,
paramsx![],
)
.await
.ok()
}
#[cfg(test)]
@@ -424,7 +410,7 @@ mod tests {
#[async_std::test]
async fn test_try_load() {
let t = dummy_context().await;
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();
@@ -472,7 +458,7 @@ mod tests {
#[async_std::test]
async fn test_sort_self_talk_up_on_forward() {
let t = dummy_context().await;
let t = TestContext::new().await;
t.ctx.update_device_chats().await.unwrap();
create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
@@ -497,7 +483,7 @@ mod tests {
#[async_std::test]
async fn test_search_special_chat_names() {
let t = dummy_context().await;
let t = TestContext::new().await;
t.ctx.update_device_chats().await.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None)
@@ -530,7 +516,7 @@ mod tests {
#[async_std::test]
async fn test_get_summary_unwrap() {
let t = dummy_context().await;
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();

View File

@@ -104,6 +104,9 @@ pub enum Config {
ConfiguredServerFlags,
ConfiguredSendSecurity,
ConfiguredE2EEEnabled,
ConfiguredInboxFolder,
ConfiguredMvboxFolder,
ConfiguredSentboxFolder,
Configured,
#[strum(serialize = "sys.version")]
@@ -117,17 +120,21 @@ pub enum Config {
}
impl Context {
pub async fn config_exists(&self, key: Config) -> bool {
self.sql.get_raw_config(key).await.is_some()
}
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
pub async fn get_config(&self, key: Config) -> Option<String> {
let value = match key {
Config::Selfavatar => {
let rel_path = self.sql.get_raw_config(self, key).await;
let rel_path = self.sql.get_raw_config(key).await;
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
}
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(self, key).await,
_ => self.sql.get_raw_config(key).await,
};
if value.is_some() {
@@ -137,6 +144,7 @@ impl Context {
// Default values
match key {
Config::Selfstatus => Some(self.stock_str(StockMessage::StatusLine).await.into_owned()),
Config::ConfiguredInboxFolder => Some("INBOX".to_owned()),
_ => key.get_str("default").map(|s| s.to_string()),
}
}
@@ -181,7 +189,7 @@ impl Context {
match key {
Config::Selfavatar => {
self.sql
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![])
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsx![])
.await?;
self.sql
.set_raw_config_bool(self, "attach_selfavatar", true)
@@ -197,21 +205,6 @@ impl Context {
None => self.sql.set_raw_config(self, key, None).await,
}
}
Config::InboxWatch => {
let ret = self.sql.set_raw_config(self, key, value).await;
self.interrupt_inbox(false).await;
ret
}
Config::SentboxWatch => {
let ret = self.sql.set_raw_config(self, key, value).await;
self.interrupt_sentbox(false).await;
ret
}
Config::MvboxWatch => {
let ret = self.sql.set_raw_config(self, key, value).await;
self.interrupt_mvbox(false).await;
ret
}
Config::Selfstatus => {
let def = self.stock_str(StockMessage::StatusLine).await;
let val = if value.is_none() || value.unwrap() == def {
@@ -281,7 +274,7 @@ mod tests {
#[async_std::test]
async fn test_selfavatar_outside_blobdir() {
let t = dummy_context().await;
let t = TestContext::new().await;
let avatar_src = t.dir.path().join("avatar.jpg");
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
File::create(&avatar_src)
@@ -310,7 +303,7 @@ mod tests {
#[async_std::test]
async fn test_selfavatar_in_blobdir() {
let t = dummy_context().await;
let t = TestContext::new().await;
let avatar_src = t.ctx.get_blobdir().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
File::create(&avatar_src)
@@ -336,7 +329,7 @@ mod tests {
#[async_std::test]
async fn test_selfavatar_copy_without_recode() {
let t = dummy_context().await;
let t = TestContext::new().await;
let avatar_src = t.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
File::create(&avatar_src)
@@ -360,7 +353,7 @@ mod tests {
#[async_std::test]
async fn test_media_quality_config_option() {
let t = dummy_context().await;
let t = TestContext::new().await;
let media_quality = t.ctx.get_config_int(Config::MediaQuality).await;
assert_eq!(media_quality, 0);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();

View File

@@ -1,10 +1,12 @@
//! Email accounts autoconfiguration process module
#![forbid(clippy::indexing_slicing)]
mod auto_mozilla;
mod auto_outlook;
mod read_url;
use anyhow::{bail, ensure, format_err, Result};
use anyhow::{bail, ensure, format_err, Context as _, Result};
use async_std::prelude::*;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
@@ -35,7 +37,7 @@ macro_rules! progress {
impl Context {
/// Checks if the context is already configured.
pub async fn is_configured(&self) -> bool {
self.sql.get_raw_config_bool(self, "configured").await
self.sql.get_raw_config_bool("configured").await
}
/// Configures this account with the currently set parameters.
@@ -66,82 +68,22 @@ impl Context {
}
async fn inner_configure(&self) -> Result<()> {
let mut success = false;
let mut param_autoconfig: Option<LoginParam> = None;
info!(self, "Configure ...");
// Variables that are shared between steps:
let mut param = LoginParam::from_database(self, "").await;
// need all vars here to be mutable because rust thinks the same step could be called multiple times
// and also initialize, because otherwise rust thinks it's used while unitilized, even if thats not the case as the loop goes only forward
let mut param_domain = "undefined.undefined".to_owned();
let mut param_addr_urlencoded: String =
"Internal Error: this value should never be used".to_owned();
let mut keep_flags = 0;
let mut step_counter: u8 = 0;
let (_s, r) = async_std::sync::channel(1);
let mut imap = Imap::new(r);
let was_configured_before = self.is_configured().await;
while !self.shall_stop_ongoing().await {
step_counter += 1;
match exec_step(
self,
&mut imap,
&mut param,
&mut param_domain,
&mut param_autoconfig,
&mut param_addr_urlencoded,
&mut keep_flags,
&mut step_counter,
)
.await
{
Ok(step) => {
success = true;
match step {
Step::Continue => {}
Step::Done => break,
}
}
Err(err) => {
error!(self, "{}", err);
success = false;
break;
}
}
}
if imap.is_connected() {
imap.disconnect(self).await;
}
// remember the entered parameters on success
// and restore to last-entered on failure.
// this way, the parameters visible to the ui are always in-sync with the current configuration.
if success {
LoginParam::from_database(self, "")
.await
.save_to_database(self, "configured_raw_")
.await
.ok();
} else {
LoginParam::from_database(self, "configured_raw_")
.await
.save_to_database(self, "")
.await
.ok();
}
let success = configure(self, &mut param).await;
if let Some(provider) = provider::get_provider_info(&param.addr) {
if !was_configured_before {
if let Some(config_defaults) = &provider.config_defaults {
for def in config_defaults.iter() {
if let Some(config_defaults) = &provider.config_defaults {
for def in config_defaults.iter() {
if !self.config_exists(def.key).await {
info!(self, "apply config_defaults {}={}", def.key, def.value);
self.set_config(def.key, Some(def.value)).await?;
} else {
info!(
self,
"skip already set config_defaults {}={}", def.key, def.value
);
}
}
}
@@ -158,314 +100,294 @@ impl Context {
}
}
if success {
progress!(self, 1000);
Ok(())
match success {
Ok(_) => {
progress!(self, 1000);
Ok(())
}
Err(err) => {
error!(self, "Configure Failed: {}", err);
progress!(self, 0);
Err(err)
}
}
}
}
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);
ensure!(!param.addr.is_empty(), "Please enter an email address.");
// Step 1: Load the parameters and check email-address and password
if 0 != param.server_flags & DC_LP_AUTH_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)
.await
.and_then(|e| e.parse().ok())
{
info!(ctx, "Authorized address is {}", oauth2_addr);
param.addr = oauth2_addr;
ctx.sql
.set_raw_config(ctx, "addr", Some(param.addr.as_str()))
.await?;
}
progress!(ctx, 20);
}
// no oauth? - just continue it's no error
let parsed: EmailAddress = param.addr.parse().context("Bad email-address")?;
let param_domain = parsed.domain;
let param_addr_urlencoded = utf8_percent_encode(&param.addr, NON_ALPHANUMERIC).to_string();
// 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
{
// 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() {
param_autoconfig =
get_autoconfig(ctx, param, &param_domain, &param_addr_urlencoded).await;
}
}
// 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.mail_server.is_empty() {
param.mail_server = format!("imap.{}", param_domain,)
}
if param.mail_port == 0 {
param.mail_port = if 0 != param.server_flags & (0x100 | 0x400) {
143
} else {
progress!(self, 0);
Err(format_err!("Configure failed"))
993
}
}
if param.mail_user.is_empty() {
param.mail_user = param.addr.clone();
}
if param.send_server.is_empty() && !param.mail_server.is_empty() {
param.send_server = param.mail_server.clone();
if param.send_server.starts_with("imap.") {
param.send_server = param.send_server.replacen("imap", "smtp", 1);
}
}
if param.send_port == 0 {
param.send_port = if 0 != param.server_flags & DC_LP_SMTP_SOCKET_STARTTLS as i32 {
587
} else if 0 != param.server_flags & DC_LP_SMTP_SOCKET_PLAIN as i32 {
25
} else {
465
}
}
if param.send_user.is_empty() && !param.mail_user.is_empty() {
param.send_user = param.mail_user.clone();
}
if param.send_pw.is_empty() && !param.mail_pw.is_empty() {
param.send_pw = param.mail_pw.clone()
}
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
}
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);
param.server_flags |= if param.send_port == 143 {
DC_LP_IMAP_SOCKET_STARTTLS as i32
} else {
DC_LP_IMAP_SOCKET_SSL 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);
param.server_flags |= if param.send_port == 587 {
DC_LP_SMTP_SOCKET_STARTTLS as i32
} else if param.send_port == 25 {
DC_LP_SMTP_SOCKET_PLAIN as i32
} else {
DC_LP_SMTP_SOCKET_SSL as i32
}
}
// do we have a complete configuration?
ensure!(
!param.mail_server.is_empty()
&& param.mail_port != 0
&& !param.mail_user.is_empty()
&& !param.mail_pw.is_empty()
&& !param.send_server.is_empty()
&& param.send_port != 0
&& !param.send_user.is_empty()
&& !param.send_pw.is_empty()
&& param.server_flags != 0,
"Account settings incomplete."
);
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
let (_s, r) = async_std::sync::channel(1);
let mut imap = Imap::new(r);
try_imap_connections(ctx, param, param_autoconfig.is_some(), &mut imap).await?;
progress!(ctx, 800);
try_smtp_connections(ctx, param, param_autoconfig.is_some()).await?;
progress!(ctx, 900);
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await
|| ctx.get_config_bool(Config::MvboxMove).await;
imap.configure_folders(ctx, create_mvbox).await?;
imap.select_with_uidvalidity(ctx, "INBOX")
.await
.context("could not read INBOX status")?;
drop(imap);
progress!(ctx, 910);
// configuration success - write back the configured parameters with the
// "configured_" prefix; also write the "configured"-flag */
// the trailing underscore is correct
param.save_to_database(ctx, "configured_").await?;
ctx.sql.set_raw_config_bool(ctx, "configured", true).await?;
progress!(ctx, 920);
e2ee::ensure_secret_key_exists(ctx).await?;
info!(ctx, "key generation completed");
progress!(ctx, 940);
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn exec_step(
ctx: &Context,
imap: &mut Imap,
param: &mut LoginParam,
param_domain: &mut String,
param_autoconfig: &mut Option<LoginParam>,
param_addr_urlencoded: &mut String,
keep_flags: &mut i32,
step_counter: &mut u8,
) -> Result<Step> {
const STEP_12_USE_AUTOCONFIG: u8 = 12;
const STEP_13_AFTER_AUTOCONFIG: u8 = 13;
#[derive(Debug, PartialEq, Eq)]
enum AutoconfigProvider {
Mozilla,
Outlook,
}
match *step_counter {
// Read login parameters from the database
1 => {
progress!(ctx, 1);
ensure!(!param.addr.is_empty(), "Please enter an email address.");
}
// Step 1: Load the parameters and check email-address and password
2 => {
if 0 != param.server_flags & DC_LP_AUTH_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)
.await
.and_then(|e| e.parse().ok())
{
info!(ctx, "Authorized address is {}", oauth2_addr);
param.addr = oauth2_addr;
ctx.sql
.set_raw_config(ctx, "addr", Some(param.addr.as_str()))
.await?;
}
progress!(ctx, 20);
}
// no oauth? - just continue it's no error
}
3 => {
if let Ok(parsed) = param.addr.parse() {
let parsed: EmailAddress = parsed;
*param_domain = parsed.domain;
*param_addr_urlencoded =
utf8_percent_encode(&param.addr, NON_ALPHANUMERIC).to_string();
} else {
bail!("Bad email-address.");
}
}
// Step 2: Autoconfig
4 => {
progress!(ctx, 200);
#[derive(Debug, PartialEq, Eq)]
struct AutoconfigSource {
provider: AutoconfigProvider,
url: String,
}
if param.mail_server.is_empty()
&& param.mail_port == 0
/* && param.mail_user.is_empty() -- the user can enter a loginname which is used by autoconfig then */
&& param.send_server.is_empty()
&& param.send_port == 0
&& param.send_user.is_empty()
/* && param.send_pw.is_empty() -- the password cannot be auto-configured and is no criterion for autoconfig or not */
&& (param.server_flags & !DC_LP_AUTH_OAUTH2) == 0
{
// 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);
*step_counter = STEP_12_USE_AUTOCONFIG - 1; // minus one as step_counter is increased on next loop
}
} else {
// advanced parameters entered by the user: skip Autoconfig
*step_counter = STEP_13_AFTER_AUTOCONFIG - 1; // minus one as step_counter is increased on next loop
}
}
/* A. Search configurations from the domain used in the email-address, prefer encrypted */
5 => {
if param_autoconfig.is_none() {
let url = format!(
impl AutoconfigSource {
fn all(domain: &str, addr: &str) -> [Self; 5] {
[
AutoconfigSource {
provider: AutoconfigProvider::Mozilla,
url: format!(
"https://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}",
param_domain, param_addr_urlencoded
);
*param_autoconfig = moz_autoconfigure(ctx, &url, &param).await.ok();
}
}
6 => {
progress!(ctx, 300);
if param_autoconfig.is_none() {
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see https://releases.mozilla.org/pub/thunderbird/ , which makes some sense
let url = format!(
domain, addr,
),
},
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see https://releases.mozilla.org/pub/thunderbird/ , which makes some sense
AutoconfigSource {
provider: AutoconfigProvider::Mozilla,
url: format!(
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
param_domain, param_addr_urlencoded
);
*param_autoconfig = moz_autoconfigure(ctx, &url, &param).await.ok();
}
}
/* Outlook section start ------------- */
/* Outlook uses always SSL but different domains (this comment describes the next two steps) */
7 => {
progress!(ctx, 310);
if param_autoconfig.is_none() {
let url = format!("https://{}/autodiscover/autodiscover.xml", param_domain);
*param_autoconfig = outlk_autodiscover(ctx, &url, &param).await.ok();
}
}
8 => {
progress!(ctx, 320);
if param_autoconfig.is_none() {
let url = format!(
domain, addr
),
},
AutoconfigSource {
provider: AutoconfigProvider::Outlook,
url: format!("https://{}/autodiscover/autodiscover.xml", domain),
},
// Outlook uses always SSL but different domains (this comment describes the next two steps)
AutoconfigSource {
provider: AutoconfigProvider::Outlook,
url: format!(
"https://{}{}/autodiscover/autodiscover.xml",
"autodiscover.", param_domain
);
*param_autoconfig = outlk_autodiscover(ctx, &url, &param).await.ok();
}
}
/* ----------- Outlook section end */
9 => {
progress!(ctx, 330);
if param_autoconfig.is_none() {
let url = format!(
"http://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}",
param_domain, param_addr_urlencoded
);
*param_autoconfig = moz_autoconfigure(ctx, &url, &param).await.ok();
}
}
10 => {
progress!(ctx, 340);
if param_autoconfig.is_none() {
// do not transfer the email-address unencrypted
let url = format!(
"http://{}/.well-known/autoconfig/mail/config-v1.1.xml",
param_domain
);
*param_autoconfig = moz_autoconfigure(ctx, &url, &param).await.ok();
}
}
/* B. If we have no configuration yet, search configuration in Thunderbird's centeral database */
11 => {
progress!(ctx, 350);
if param_autoconfig.is_none() {
/* always SSL for Thunderbird's database */
let url = format!("https://autoconfig.thunderbird.net/v1.1/{}", param_domain);
*param_autoconfig = moz_autoconfigure(ctx, &url, &param).await.ok();
}
}
/* C. Do we have any autoconfig result?
If you change the match-number here, also update STEP_12_COPY_AUTOCONFIG above
*/
STEP_12_USE_AUTOCONFIG => {
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();
}
param.mail_server = cfg.mail_server.clone(); /* all other values are always NULL when entering autoconfig */
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 you change the match-number here, also update STEP_13_AFTER_AUTOCONFIG above
STEP_13_AFTER_AUTOCONFIG => {
if param.mail_server.is_empty() {
param.mail_server = format!("imap.{}", param_domain,)
}
if param.mail_port == 0 {
param.mail_port = if 0 != param.server_flags & (0x100 | 0x400) {
143
} else {
993
}
}
if param.mail_user.is_empty() {
param.mail_user = param.addr.clone();
}
if param.send_server.is_empty() && !param.mail_server.is_empty() {
param.send_server = param.mail_server.clone();
if param.send_server.starts_with("imap.") {
param.send_server = param.send_server.replacen("imap", "smtp", 1);
}
}
if param.send_port == 0 {
param.send_port = if 0 != param.server_flags & DC_LP_SMTP_SOCKET_STARTTLS as i32 {
587
} else if 0 != param.server_flags & DC_LP_SMTP_SOCKET_PLAIN as i32 {
25
} else {
465
}
}
if param.send_user.is_empty() && !param.mail_user.is_empty() {
param.send_user = param.mail_user.clone();
}
if param.send_pw.is_empty() && !param.mail_pw.is_empty() {
param.send_pw = param.mail_pw.clone()
}
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
}
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);
param.server_flags |= if param.send_port == 143 {
DC_LP_IMAP_SOCKET_STARTTLS as i32
} else {
DC_LP_IMAP_SOCKET_SSL 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);
param.server_flags |= if param.send_port == 587 {
DC_LP_SMTP_SOCKET_STARTTLS as i32
} else if param.send_port == 25 {
DC_LP_SMTP_SOCKET_PLAIN as i32
} else {
DC_LP_SMTP_SOCKET_SSL as i32
}
}
/* do we have a complete configuration? */
if param.mail_server.is_empty()
|| param.mail_port == 0
|| param.mail_user.is_empty()
|| param.mail_pw.is_empty()
|| param.send_server.is_empty()
|| param.send_port == 0
|| param.send_user.is_empty()
|| param.send_pw.is_empty()
|| param.server_flags == 0
{
bail!("Account settings incomplete.");
}
}
14 => {
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 */
try_imap_connections(ctx, param, param_autoconfig.is_some(), imap).await?;
}
15 => {
progress!(ctx, 800);
try_smtp_connections(ctx, param, param_autoconfig.is_some()).await?;
}
16 => {
progress!(ctx, 900);
"autodiscover.", domain
),
},
// always SSL for Thunderbird's database
AutoconfigSource {
provider: AutoconfigProvider::Mozilla,
url: format!("https://autoconfig.thunderbird.net/v1.1/{}", domain),
},
]
}
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await
|| ctx.get_config_bool(Config::MvboxMove).await;
async fn fetch(&self, ctx: &Context, param: &LoginParam) -> Result<LoginParam> {
let params = match self.provider {
AutoconfigProvider::Mozilla => moz_autoconfigure(ctx, &self.url, &param).await?,
AutoconfigProvider::Outlook => outlk_autodiscover(ctx, &self.url, &param).await?,
};
if let Err(err) = imap.configure_folders(ctx, create_mvbox).await {
bail!("configuring folders failed: {:?}", err);
}
Ok(params)
}
}
if let Err(err) = imap.select_with_uidvalidity(ctx, "INBOX").await {
bail!("could not read INBOX status: {:?}", err);
}
}
17 => {
progress!(ctx, 910);
// configuration success - write back the configured parameters with the
// "configured_" prefix; also write the "configured"-flag */
// the trailing underscore is correct
param.save_to_database(ctx, "configured_").await?;
ctx.sql.set_raw_config_bool(ctx, "configured", true).await?;
}
18 => {
progress!(ctx, 920);
// we generate the keypair just now - we could also postpone this until the first message is sent, however,
// this may result in a unexpected and annoying delay when the user sends his very first message
// (~30 seconds on a Moto G4 play) and might looks as if message sending is always that slow.
e2ee::ensure_secret_key_exists(ctx).await?;
info!(ctx, "key generation completed");
progress!(ctx, 940);
return Ok(Step::Done);
}
_ => {
bail!("Internal error: step counter out of bound");
/// Retrieve available autoconfigurations.
///
/// A Search configurations from the domain used in the email-address, prefer encrypted
/// B. If we have no configuration yet, search configuration in Thunderbird's centeral database
async fn get_autoconfig(
ctx: &Context,
param: &LoginParam,
param_domain: &str,
param_addr_urlencoded: &str,
) -> Option<LoginParam> {
let sources = AutoconfigSource::all(param_domain, param_addr_urlencoded);
let mut progress = 300;
for source in &sources {
let res = source.fetch(ctx, param).await;
progress!(ctx, progress);
progress += 10;
if let Ok(res) = res {
return Some(res);
}
}
Ok(Step::Continue)
None
}
#[derive(Debug)]
enum Step {
Done,
Continue,
}
#[allow(clippy::unnecessary_unwrap)]
fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option<LoginParam> {
info!(
context,
@@ -481,37 +403,35 @@ fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option<Login
// however, rewriting the code to "if let" would make things less obvious,
// esp. if we allow more combinations of servers (pop, jmap).
// therefore, #[allow(clippy::unnecessary_unwrap)] is added above.
if imap.is_some() && smtp.is_some() {
let imap = imap.unwrap();
let smtp = smtp.unwrap();
if let Some(imap) = imap {
if let Some(smtp) = smtp {
let mut p = LoginParam::new();
p.addr = param.addr.clone();
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::AcceptInvalidCertificates;
p.server_flags |= match imap.socket {
provider::Socket::STARTTLS => DC_LP_IMAP_SOCKET_STARTTLS,
provider::Socket::SSL => DC_LP_IMAP_SOCKET_SSL,
};
p.mail_server = imap.hostname.to_string();
p.mail_user = imap.apply_username_pattern(param.addr.clone());
p.mail_port = imap.port as i32;
p.imap_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags |= match imap.socket {
provider::Socket::STARTTLS => DC_LP_IMAP_SOCKET_STARTTLS,
provider::Socket::SSL => DC_LP_IMAP_SOCKET_SSL,
};
p.send_server = smtp.hostname.to_string();
p.send_user = smtp.apply_username_pattern(param.addr.clone());
p.send_port = smtp.port as i32;
p.smtp_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags |= match smtp.socket {
provider::Socket::STARTTLS => DC_LP_SMTP_SOCKET_STARTTLS as i32,
provider::Socket::SSL => DC_LP_SMTP_SOCKET_SSL as i32,
};
p.send_server = smtp.hostname.to_string();
p.send_user = smtp.apply_username_pattern(param.addr.clone());
p.send_port = smtp.port as i32;
p.smtp_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags |= match smtp.socket {
provider::Socket::STARTTLS => DC_LP_SMTP_SOCKET_STARTTLS as i32,
provider::Socket::SSL => DC_LP_SMTP_SOCKET_SSL as i32,
};
info!(context, "offline autoconfig found: {}", p);
return Some(p);
} else {
info!(context, "offline autoconfig found, but no servers defined");
return None;
info!(context, "offline autoconfig found: {}", p);
return Some(p);
}
}
info!(context, "offline autoconfig found, but no servers defined");
return None;
}
provider::Status::BROKEN => {
info!(context, "offline autoconfig found, provider is broken");
@@ -525,19 +445,32 @@ fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option<Login
async fn try_imap_connections(
context: &Context,
mut param: &mut LoginParam,
param: &mut LoginParam,
was_autoconfig: bool,
imap: &mut Imap,
) -> Result<bool> {
// progress 650 and 660
if let Ok(val) = try_imap_connection(context, &mut param, was_autoconfig, 0, imap).await {
return Ok(val);
}
progress!(context, 670);
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS);
param.server_flags |= DC_LP_IMAP_SOCKET_SSL;
param.mail_port = 993;
) -> Result<()> {
// manually_set_param is used to check whether a particular setting was set manually by the user.
// If yes, we do not want to change it to avoid confusing error messages
// (you set port 443, but the app tells you it couldn't connect on port 993).
let manually_set_param = LoginParam::from_database(context, "").await;
// progress 650 and 660
if try_imap_connection(context, param, &manually_set_param, was_autoconfig, 0, imap)
.await
.is_ok()
{
return Ok(()); // we directly return here if it was autoconfig or the connection succeeded
}
progress!(context, 670);
// try_imap_connection() changed the flags and port. Change them back:
if manually_set_param.server_flags & DC_LP_IMAP_SOCKET_FLAGS == 0 {
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS);
param.server_flags |= DC_LP_IMAP_SOCKET_SSL;
}
if manually_set_param.mail_port == 0 {
param.mail_port = 993;
}
if let Some(at) = param.mail_user.find('@') {
param.mail_user = param.mail_user.split_at(at).0.to_string();
}
@@ -545,35 +478,43 @@ async fn try_imap_connections(
param.send_user = param.send_user.split_at(at).0.to_string();
}
// progress 680 and 690
try_imap_connection(context, &mut param, was_autoconfig, 1, imap).await
try_imap_connection(context, param, &manually_set_param, was_autoconfig, 1, imap).await
}
async fn try_imap_connection(
context: &Context,
param: &mut LoginParam,
manually_set_param: &LoginParam,
was_autoconfig: bool,
variation: usize,
imap: &mut Imap,
) -> Result<bool> {
if try_imap_one_param(context, &param, imap).await.is_ok() {
return Ok(true);
) -> Result<()> {
if try_imap_one_param(context, param, imap).await.is_ok() {
return Ok(());
}
if was_autoconfig {
return Ok(false);
return Ok(());
}
progress!(context, 650 + variation * 30);
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS);
param.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS;
if try_imap_one_param(context, &param, imap).await.is_ok() {
return Ok(true);
if manually_set_param.server_flags & DC_LP_IMAP_SOCKET_FLAGS == 0 {
param.server_flags &= !(DC_LP_IMAP_SOCKET_FLAGS);
param.server_flags |= DC_LP_IMAP_SOCKET_STARTTLS;
if try_imap_one_param(context, &param, imap).await.is_ok() {
return Ok(());
}
}
progress!(context, 660 + variation * 30);
param.mail_port = 143;
try_imap_one_param(context, &param, imap).await?;
Ok(true)
if manually_set_param.mail_port == 0 {
param.mail_port = 143;
try_imap_one_param(context, param, imap).await
} else {
Err(format_err!("no more possible configs"))
}
}
async fn try_imap_one_param(context: &Context, param: &LoginParam, imap: &mut Imap) -> Result<()> {
@@ -592,41 +533,51 @@ async fn try_imap_one_param(context: &Context, param: &LoginParam, imap: &mut Im
return Ok(());
}
if context.shall_stop_ongoing().await {
bail!("Interrupted");
}
bail!("Could not connect: {}", inf);
}
async fn try_smtp_connections(
context: &Context,
mut param: &mut LoginParam,
param: &mut LoginParam,
was_autoconfig: bool,
) -> Result<()> {
// manually_set_param is used to check whether a particular setting was set manually by the user.
// If yes, we do not want to change it to avoid confusing error messages
// (you set port 443, but the app tells you it couldn't connect on port 993).
let manually_set_param = LoginParam::from_database(context, "").await;
let mut smtp = Smtp::new();
/* try to connect to SMTP - if we did not got an autoconfig, the first try was SSL-465 and we do a second try with STARTTLS-587 */
if try_smtp_one_param(context, &param, &mut smtp).await.is_ok() {
// try to connect to SMTP - if we did not got an autoconfig, the first try was SSL-465 and we do
// a second try with STARTTLS-587
if try_smtp_one_param(context, param, &mut smtp).await.is_ok() {
return Ok(());
}
if was_autoconfig {
return Ok(());
}
progress!(context, 850);
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32;
param.send_port = 587;
if try_smtp_one_param(context, &param, &mut smtp).await.is_ok() {
if manually_set_param.server_flags & (DC_LP_SMTP_SOCKET_FLAGS as i32) == 0 {
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32;
}
if manually_set_param.send_port == 0 {
param.send_port = 587;
}
if try_smtp_one_param(context, param, &mut smtp).await.is_ok() {
return Ok(());
}
progress!(context, 860);
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32;
param.send_port = 25;
try_smtp_one_param(context, &param, &mut smtp).await?;
Ok(())
if manually_set_param.server_flags & (DC_LP_SMTP_SOCKET_FLAGS as i32) == 0 {
param.server_flags &= !(DC_LP_SMTP_SOCKET_FLAGS as i32);
param.server_flags |= DC_LP_SMTP_SOCKET_STARTTLS as i32;
}
if manually_set_param.send_port == 0 {
param.send_port = 25;
}
try_smtp_one_param(context, param, &mut smtp).await
}
async fn try_smtp_one_param(context: &Context, param: &LoginParam, smtp: &mut Smtp) -> Result<()> {
@@ -676,7 +627,7 @@ mod tests {
#[async_std::test]
async fn test_no_panic_on_bad_credentials() {
let t = dummy_context().await;
let t = TestContext::new().await;
t.ctx
.set_config(Config::Addr, Some("probably@unexistant.addr"))
.await
@@ -690,7 +641,7 @@ mod tests {
#[async_std::test]
async fn test_get_offline_autoconfig() {
let context = dummy_context().await.ctx;
let context = TestContext::new().await.ctx;
let mut params = LoginParam::new();
params.addr = "someone123@example.org".to_string();

View File

@@ -1,7 +1,6 @@
//! # Constants
#![allow(dead_code)]
use deltachat_derive::*;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
@@ -25,12 +24,11 @@ const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
sqlx::Type,
)]
#[repr(u8)]
#[repr(i32)]
pub enum Blocked {
Not = 0,
Manually = 1,
@@ -43,8 +41,8 @@ impl Default for Blocked {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(u8)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
#[repr(i32)]
pub enum ShowEmails {
Off = 0,
AcceptedContacts = 1,
@@ -57,8 +55,8 @@ impl Default for ShowEmails {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(u8)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
#[repr(i32)]
pub enum MediaQuality {
Balanced = 0,
Worse = 1,
@@ -70,7 +68,7 @@ impl Default for MediaQuality {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
#[repr(u8)]
pub enum KeyGenType {
Default = 0,
@@ -127,13 +125,12 @@ pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
IntoStaticStr,
Serialize,
Deserialize,
sqlx::Type,
)]
#[repr(u32)]
#[repr(i32)]
pub enum Chattype {
Undefined = 0,
Single = 100,
@@ -227,6 +224,10 @@ pub const DC_BOB_SUCCESS: i32 = 1;
// max. width/height of an avatar
pub const AVATAR_SIZE: u32 = 192;
// max. width/height of images
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
pub const WORSE_IMAGE_SIZE: u32 = 640;
// this value can be increased if the folder configuration is changed and must be redone on next program start
pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
@@ -239,10 +240,9 @@ pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
sqlx::Type,
)]
#[repr(i32)]
pub enum Viewtype {

View File

@@ -1,8 +1,14 @@
//! Contacts module
#![forbid(clippy::indexing_slicing)]
use std::convert::TryFrom;
use async_std::path::PathBuf;
use deltachat_derive::*;
use async_std::prelude::*;
use itertools::Itertools;
use lazy_static::lazy_static;
use regex::Regex;
use sqlx::Row;
use crate::aheader::EncryptPreference;
use crate::chat::ChatId;
@@ -12,7 +18,7 @@ use crate::context::Context;
use crate::dc_tools::*;
use crate::error::{bail, ensure, format_err, Result};
use crate::events::Event;
use crate::key::{DcKey, Key, SignedPublicKey};
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::message::{MessageState, MsgId};
use crate::mimeparser::AvatarAction;
@@ -68,7 +74,7 @@ pub struct Contact {
/// Possible origins of a contact.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, FromSql, ToSql,
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, sqlx::Type,
)]
#[repr(i32)]
pub enum Origin {
@@ -129,6 +135,31 @@ impl Default for Origin {
Origin::Unknown
}
}
impl<'a> sqlx::FromRow<'a, sqlx::sqlite::SqliteRow> for Contact {
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
let contact = Self {
id: u32::try_from(row.try_get::<i32, _>("id")?)
.map_err(|err| sqlx::Error::Decode(err.into()))?,
name: row
.try_get::<Option<String>, _>("name")?
.unwrap_or_default(),
authname: row
.try_get::<Option<String>, _>("authname")?
.unwrap_or_default(),
addr: row.try_get::<String, _>("addr")?,
blocked: row
.try_get::<Option<i32>, _>("blocked")?
.unwrap_or_default()
!= 0,
origin: row.try_get("origin")?,
param: row
.try_get::<String, _>("param")?
.parse()
.unwrap_or_default(),
};
Ok(contact)
}
}
impl Origin {
/// Contacts that are known, i. e. they came in via accepted contacts or
@@ -159,27 +190,18 @@ pub enum VerifiedStatus {
impl Contact {
pub async fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result<Self> {
let mut res = context
let mut res: Contact = context
.sql
.query_row(
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param
FROM contacts c
WHERE c.id=?;",
paramsv![contact_id as i32],
|row| {
let contact = Self {
id: contact_id,
name: row.get::<_, String>(0)?,
authname: row.get::<_, String>(4)?,
addr: row.get::<_, String>(1)?,
blocked: row.get::<_, Option<i32>>(3)?.unwrap_or_default() != 0,
origin: row.get(2)?,
param: row.get::<_, String>(5)?.parse().unwrap_or_default(),
};
Ok(contact)
},
r#"
SELECT id, name, addr, origin, blocked, authname, param
FROM contacts
WHERE id=?;
"#,
paramsx![contact_id as i32],
)
.await?;
if contact_id == DC_CONTACT_ID_SELF {
res.name = context.stock_str(StockMessage::SelfMsg).await.to_string();
res.addr = context
@@ -238,6 +260,8 @@ impl Contact {
"Cannot create contact with empty address"
);
let (name, addr) = sanitize_name_and_addr(name, addr);
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;
@@ -263,8 +287,11 @@ impl Contact {
if context
.sql
.execute(
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh],
r#"
UPDATE msgs SET state=?
WHERE from_id=? AND state=?;
"#,
paramsx![MessageState::InNoticed, id as i32, MessageState::InFresh],
)
.await
.is_ok()
@@ -299,15 +326,15 @@ impl Contact {
if addr_cmp(addr_normalized, addr_self) {
return DC_CONTACT_ID_SELF;
}
context.sql.query_get_value(
context,
"SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE AND id>?2 AND origin>=?3 AND blocked=0;",
paramsv![
let v: i32 = context.sql.query_value(
"SELECT id FROM contacts WHERE addr=? COLLATE NOCASE AND id>? AND origin>=? AND blocked=0;",
paramsx![
addr_normalized,
DC_CONTACT_ID_LAST_SPECIAL as i32,
min_origin as u32,
min_origin as i32
],
).await.unwrap_or_default()
).await.unwrap_or_default();
v as u32
}
/// Lookup a contact and create it if it does not exist yet.
@@ -378,39 +405,32 @@ impl Contact {
let mut update_authname = false;
let mut row_id = 0;
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context.sql.query_row(
let res: Result<(i32, String, String, Origin, String), _> = context.sql.query_row(
"SELECT id, name, addr, origin, authname FROM contacts WHERE addr=? COLLATE NOCASE;",
paramsv![addr.to_string()],
|row| {
let row_id = row.get(0)?;
let row_name: String = row.get(1)?;
let row_addr: String = row.get(2)?;
let row_origin: Origin = row.get(3)?;
let row_authname: String = row.get(4)?;
paramsx![addr.to_string()],
)
.await;
if !name.as_ref().is_empty() {
if !row_name.is_empty() {
if (origin >= row_origin || row_name == row_authname)
&& name.as_ref() != row_name
{
update_name = true;
}
} else {
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = res {
if !name.as_ref().is_empty() {
if !row_name.is_empty() {
if (origin >= row_origin || row_name == row_authname)
&& name.as_ref() != row_name
{
update_name = true;
}
if origin == Origin::IncomingUnknownFrom && name.as_ref() != row_authname {
update_authname = true;
}
} else if origin == Origin::ManuallyCreated && !row_authname.is_empty() {
// no name given on manual edit, this will update the name to the authname
} else {
update_name = true;
}
if origin == Origin::IncomingUnknownFrom && name.as_ref() != row_authname {
update_authname = true;
}
} else if origin == Origin::ManuallyCreated && !row_authname.is_empty() {
// no name given on manual edit, this will update the name to the authname
update_name = true;
}
Ok((row_id, row_name, row_addr, row_origin, row_authname))
},
)
.await {
row_id = id;
row_id = id as u32;
if origin as i32 >= row_origin as i32 && addr != row_addr {
update_addr = true;
}
@@ -429,9 +449,13 @@ impl Contact {
.sql
.execute(
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
paramsv![
new_name,
if update_addr { addr.to_string() } else { row_addr },
paramsx![
&new_name,
if update_addr {
addr.to_string()
} else {
row_addr
},
if origin > row_origin {
origin
} else {
@@ -442,7 +466,7 @@ impl Contact {
} else {
row_authname
},
row_id
row_id as i32
],
)
.await
@@ -453,7 +477,7 @@ impl Contact {
// 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]
paramsx![&new_name, Chattype::Single, row_id as i32]
).await.ok();
}
sth_modified = Modifier::Modified;
@@ -467,20 +491,17 @@ impl Contact {
.sql
.execute(
"INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);",
paramsv![
name.as_ref().to_string(),
addr,
paramsx![
name.as_ref(),
&addr,
origin,
if update_authname { name.as_ref().to_string() } else { "".to_string() }
if update_authname { name.as_ref() } else { "" }
],
)
.await
.is_ok()
{
row_id = context
.sql
.get_rowid(context, "contacts", "addr", &addr)
.await?;
row_id = context.sql.get_rowid("contacts", "addr", &addr).await?;
sth_modified = Modifier::Created;
info!(context, "added contact id={} addr={}", row_id, &addr);
} else {
@@ -512,8 +533,9 @@ impl Contact {
let mut modify_cnt = 0;
for (name, addr) in split_address_book(addr_book.as_ref()).into_iter() {
let (name, addr) = sanitize_name_and_addr(name, addr);
let name = normalize_name(name);
match Contact::add_or_lookup(context, name, addr, Origin::AddressBook).await {
match Contact::add_or_lookup(context, name, &addr, Origin::AddressBook).await {
Err(err) => {
warn!(
context,
@@ -566,35 +588,32 @@ impl Contact {
.map(|s| s.as_ref().to_string())
.unwrap_or_default()
);
context
.sql
.query_map(
"SELECT c.id FROM contacts c \
LEFT JOIN acpeerstates ps ON c.addr=ps.addr \
WHERE c.addr!=?1 \
AND c.id>?2 \
AND c.origin>=?3 \
AND c.blocked=0 \
AND (c.name LIKE ?4 OR c.addr LIKE ?5) \
AND (1=?6 OR LENGTH(ps.verified_key_fingerprint)!=0) \
ORDER BY LOWER(c.name||c.addr),c.id;",
paramsv![
self_addr,
DC_CONTACT_ID_LAST_SPECIAL as i32,
Origin::IncomingReplyTo,
s3str_like_cmd,
s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 },
],
|row| row.get::<_, i32>(0),
|ids| {
for id in ids {
ret.push(id? as u32);
}
Ok(())
},
)
.await?;
let pool = context.sql.get_pool().await?;
let mut rows = sqlx::query_as(
r#"
SELECT c.id FROM contacts c
LEFT JOIN acpeerstates ps ON c.addr=ps.addr
WHERE c.addr!=?
AND c.id>?
AND c.origin>=?
AND c.blocked=0
AND (c.name LIKE ? OR c.addr LIKE ?)
AND (1=? OR LENGTH(ps.verified_key_fingerprint)!=0)
ORDER BY LOWER(c.name||c.addr),c.id
"#,
)
.bind(&self_addr)
.bind(DC_CONTACT_ID_LAST_SPECIAL as i32)
.bind(Origin::IncomingReplyTo)
.bind(&s3str_like_cmd)
.bind(&s3str_like_cmd)
.bind(if flag_verified_only { 0i32 } else { 1i32 })
.fetch(&pool);
while let Some(id) = rows.next().await {
let (id,): (i32,) = id?;
ret.push(id as u32);
}
let self_name = context
.get_config(Config::Displayname)
@@ -615,17 +634,15 @@ impl Contact {
} else {
add_self = true;
context.sql.query_map(
"SELECT id FROM contacts WHERE addr!=?1 AND id>?2 AND origin>=?3 AND blocked=0 ORDER BY LOWER(name||addr),id;",
paramsv![self_addr, DC_CONTACT_ID_LAST_SPECIAL as i32, 0x100],
|row| row.get::<_, i32>(0),
|ids| {
for id in ids {
ret.push(id? as u32);
}
Ok(())
}
).await?;
let pool = context.sql.get_pool().await?;
let mut rows = sqlx::query_as(
"SELECT id FROM contacts WHERE addr!=? AND id>? AND origin>=? AND blocked=0 ORDER BY LOWER(name || addr), id;"
).bind(self_addr).bind(DC_CONTACT_ID_LAST_SPECIAL as i32).bind(0x100).fetch(&pool);
while let Some(id) = rows.next().await {
let (id,): (i32,) = id?;
ret.push(id as u32);
}
}
if flag_add_self && add_self {
@@ -636,32 +653,30 @@ impl Contact {
}
pub async fn get_blocked_cnt(context: &Context) -> usize {
context
let v: i32 = context
.sql
.query_get_value::<isize>(
context,
.query_value(
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
paramsx![DC_CONTACT_ID_LAST_SPECIAL as i32],
)
.await
.unwrap_or_default() as usize
.unwrap_or_default();
v as usize
}
/// Get blocked contacts.
pub async fn get_all_blocked(context: &Context) -> Vec<u32> {
context
.sql
.query_map(
.query_values(
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(name||addr),id;",
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|row| row.get::<_, u32>(0),
|ids| {
ids.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
paramsx![DC_CONTACT_ID_LAST_SPECIAL as i32],
)
.await
.unwrap_or_default()
.into_iter()
.map(|id: i32| id as u32)
.collect()
}
/// Returns a textual summary of the encryption state for the contact.
@@ -676,9 +691,10 @@ impl Contact {
let peerstate = Peerstate::from_addr(context, &contact.addr).await;
let loginparam = LoginParam::from_database(context, "configured_").await;
if peerstate.is_some()
if peerstate.is_ok()
&& peerstate
.as_ref()
.ok()
.and_then(|p| p.peek_key(PeerstateVerifiedStatus::Unverified))
.is_some()
{
@@ -691,18 +707,20 @@ impl Contact {
})
.await;
ret += &p;
let self_key = Key::from(SignedPublicKey::load_self(context).await?);
let p = context.stock_str(StockMessage::FingerPrints).await;
ret += &format!(" {}:", p);
let fingerprint_self = self_key.formatted_fingerprint();
let fingerprint_self = SignedPublicKey::load_self(context)
.await?
.fingerprint()
.to_string();
let fingerprint_other_verified = peerstate
.peek_key(PeerstateVerifiedStatus::BidirectVerified)
.map(|k| k.formatted_fingerprint())
.map(|k| k.fingerprint().to_string())
.unwrap_or_default();
let fingerprint_other_unverified = peerstate
.peek_key(PeerstateVerifiedStatus::Unverified)
.map(|k| k.formatted_fingerprint())
.map(|k| k.fingerprint().to_string())
.unwrap_or_default();
if loginparam.addr < peerstate.addr {
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
@@ -745,10 +763,9 @@ impl Contact {
let count_contacts: i32 = context
.sql
.query_get_value(
context,
.query_value(
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
paramsv![contact_id as i32],
paramsx![contact_id as i32],
)
.await
.unwrap_or_default();
@@ -756,10 +773,9 @@ impl Contact {
let count_msgs: i32 = if count_contacts > 0 {
context
.sql
.query_get_value(
context,
.query_value(
"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;",
paramsv![contact_id as i32, contact_id as i32],
paramsx![contact_id as i32, contact_id as i32],
)
.await
.unwrap_or_default()
@@ -772,7 +788,7 @@ impl Contact {
.sql
.execute(
"DELETE FROM contacts WHERE id=?;",
paramsv![contact_id as i32],
paramsx![contact_id as i32],
)
.await
{
@@ -810,7 +826,7 @@ impl Contact {
.sql
.execute(
"UPDATE contacts SET param=? WHERE id=?",
paramsv![self.param.to_string(), self.id as i32],
paramsx![self.param.to_string(), self.id as i32],
)
.await?;
Ok(())
@@ -918,7 +934,7 @@ impl Contact {
pub async fn is_verified_ex(
&self,
context: &Context,
peerstate: Option<&Peerstate<'_>>,
peerstate: Option<&Peerstate>,
) -> VerifiedStatus {
// We're always sort of secured-verified as we could verify the key on this device any time with the key
// on this device
@@ -933,7 +949,7 @@ impl Contact {
}
let peerstate = Peerstate::from_addr(context, &self.addr).await;
if let Some(ps) = peerstate {
if let Ok(ps) = peerstate {
if ps.verified_key.is_some() {
return VerifiedStatus::BidirectVerified;
}
@@ -968,15 +984,15 @@ impl Contact {
return 0;
}
context
let v: i32 = context
.sql
.query_get_value::<isize>(
context,
.query_value(
"SELECT COUNT(*) FROM contacts WHERE id>?;",
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
paramsx![DC_CONTACT_ID_LAST_SPECIAL as i32],
)
.await
.unwrap_or_default() as usize
.unwrap_or_default();
v as usize
}
pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> bool {
@@ -988,7 +1004,7 @@ impl Contact {
.sql
.exists(
"SELECT id FROM contacts WHERE id=?;",
paramsv![contact_id as i32],
paramsx![contact_id as i32],
)
.await
.unwrap_or_default()
@@ -999,7 +1015,7 @@ impl Contact {
.sql
.execute(
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
paramsv![origin, contact_id as i32, origin],
paramsx![origin, contact_id as i32, origin],
)
.await
.is_ok()
@@ -1022,10 +1038,32 @@ pub fn addr_normalize(addr: &str) -> &str {
let norm = addr.trim();
if norm.starts_with("mailto:") {
return &norm[7..];
norm.get(7..).unwrap_or(norm)
} else {
norm
}
}
norm
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();
}
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
(
if name.as_ref().is_empty() {
captures
.get(1)
.map_or("".to_string(), |m| normalize_name(m.as_str()))
} else {
name.as_ref().to_string()
},
captures
.get(2)
.map_or("".to_string(), |m| m.as_str().to_string()),
)
} else {
(name.as_ref().to_string(), addr.as_ref().to_string())
}
}
async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
@@ -1039,7 +1077,7 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
.sql
.execute(
"UPDATE contacts SET blocked=? WHERE id=?;",
paramsv![new_blocking as i32, contact_id as i32],
paramsx![new_blocking as i32, contact_id as i32],
)
.await
.is_ok()
@@ -1049,13 +1087,23 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
// 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(Event::ContactsChanged(None));
}
if context
.sql
.execute(
r#"
UPDATE chats
SET blocked=?
WHERE type=? AND id
IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);
"#,
paramsx![new_blocking, 100i32, contact_id as i32],
)
.await
.is_ok()
{
Contact::mark_noticed(context, contact_id).await;
context.emit_event(Event::ContactsChanged(None));
}
}
}
}
@@ -1088,38 +1136,21 @@ pub(crate) async fn set_profile_image(
/// Normalize a name.
///
/// - Remove quotes (come from some bad MUA implementations)
/// - Convert names as "Petersen, Björn" to "Björn Petersen"
/// - Trims the resulting string
///
/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`.
pub fn normalize_name(full_name: impl AsRef<str>) -> String {
let mut full_name = full_name.as_ref().trim();
let full_name = full_name.as_ref().trim();
if full_name.is_empty() {
return full_name.into();
}
let len = full_name.len();
if len > 1 {
let firstchar = full_name.as_bytes()[0];
let lastchar = full_name.as_bytes()[len - 1];
if firstchar == b'\'' && lastchar == b'\''
|| firstchar == b'\"' && lastchar == b'\"'
|| firstchar == b'<' && lastchar == b'>'
{
full_name = &full_name[1..len - 1];
}
match full_name.as_bytes() {
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name
.get(1..full_name.len() - 1)
.map_or("".to_string(), |s| s.trim().into()),
_ => full_name.to_string(),
}
if let Some(p1) = full_name.find(',') {
let (last_name, first_name) = full_name.split_at(p1);
let last_name = last_name.trim();
let first_name = (&first_name[1..]).trim();
return format!("{} {}", first_name, last_name);
}
full_name.trim().into()
}
fn cat_fingerprint(
@@ -1202,11 +1233,14 @@ mod tests {
assert_eq!(may_be_valid_addr("u@d.tt"), true);
assert_eq!(may_be_valid_addr("u@.tt"), false);
assert_eq!(may_be_valid_addr("@d.tt"), false);
assert_eq!(may_be_valid_addr("<da@d.tt"), false);
assert_eq!(may_be_valid_addr("sk <@d.tt>"), false);
assert_eq!(may_be_valid_addr("as@sd.de>"), false);
assert_eq!(may_be_valid_addr("ask dkl@dd.tt"), false);
}
#[test]
fn test_normalize_name() {
assert_eq!(&normalize_name("Doe, John"), "John Doe");
assert_eq!(&normalize_name(" hello world "), "hello world");
assert_eq!(&normalize_name("<"), "<");
assert_eq!(&normalize_name(">"), ">");
@@ -1241,7 +1275,7 @@ mod tests {
#[async_std::test]
async fn test_get_contacts() {
let context = dummy_context().await;
let context = TestContext::new().await;
let contacts = Contact::get_all(&context.ctx, 0, Some("some2"))
.await
.unwrap();
@@ -1265,10 +1299,10 @@ mod tests {
#[async_std::test]
async fn test_is_self_addr() -> Result<()> {
let t = test_context().await;
let t = TestContext::new().await;
assert!(t.ctx.is_self_addr("me@me.org").await.is_err());
let addr = configure_alice_keypair(&t.ctx).await;
let addr = t.configure_alice().await;
assert_eq!(t.ctx.is_self_addr("me@me.org").await?, false);
assert_eq!(t.ctx.is_self_addr(&addr).await?, true);
@@ -1278,15 +1312,16 @@ mod tests {
#[async_std::test]
async fn test_add_or_lookup() {
// add some contacts, this also tests add_address_book()
let t = dummy_context().await;
let t = TestContext::new().await;
let book = concat!(
" Name one \n one@eins.org \n",
"Name two\ntwo@deux.net\n",
"Invalid\n+1234567890\n", // invalid, should be ignored
"\nthree@drei.sam\n",
"Name two\ntwo@deux.net\n" // should not be added again
"Name two\ntwo@deux.net\n", // should not be added again
"\nWonderland, Alice <alice@w.de>\n",
);
assert_eq!(Contact::add_address_book(&t.ctx, book).await.unwrap(), 3);
assert_eq!(Contact::add_address_book(&t.ctx, book).await.unwrap(), 4);
// check first added contact, this does not modify because of lower origin
let (contact_id, sth_modified) =
@@ -1362,6 +1397,19 @@ mod tests {
assert_eq!(contact.get_name_n_addr(), "schnucki (three@drei.sam)");
assert!(!contact.is_blocked());
// Fourth contact:
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t.ctx, "", "alice@w.de", Origin::IncomingUnknownTo)
.await
.unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::None);
let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "Wonderland, Alice");
assert_eq!(contact.get_display_name(), "Wonderland, Alice");
assert_eq!(contact.get_addr(), "alice@w.de");
assert_eq!(contact.get_name_n_addr(), "Wonderland, Alice (alice@w.de)");
// check SELF
let contact = Contact::load_from_db(&t.ctx, DC_CONTACT_ID_SELF)
.await
@@ -1377,7 +1425,7 @@ mod tests {
#[async_std::test]
async fn test_remote_authnames() {
let t = dummy_context().await;
let t = TestContext::new().await;
// incoming mail `From: bob1 <bob@example.org>` - this should init authname and name
let (contact_id, sth_modified) = Contact::add_or_lookup(
@@ -1440,7 +1488,7 @@ mod tests {
#[async_std::test]
async fn test_remote_authnames_create_empty() {
let t = dummy_context().await;
let t = TestContext::new().await;
// manually create "claire@example.org" without a given name
let contact_id = Contact::create(&t.ctx, "", "claire@example.org")
@@ -1487,7 +1535,7 @@ mod tests {
#[async_std::test]
async fn test_remote_authnames_edit_empty() {
let t = dummy_context().await;
let t = TestContext::new().await;
// manually create "dave@example.org"
let contact_id = Contact::create(&t.ctx, "dave1", "dave@example.org")
@@ -1528,4 +1576,50 @@ mod tests {
assert!(addr_cmp(" aa@aa.ORG ", "AA@AA.ORG"));
assert!(addr_cmp(" mailto:AA@AA.ORG", "Aa@Aa.orG"));
}
#[async_std::test]
async fn test_name_in_address() {
let t = TestContext::new().await;
let contact_id = Contact::create(&t.ctx, "", "<dave@example.org>")
.await
.unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_addr(), "dave@example.org");
let contact_id = Contact::create(&t.ctx, "", "Mueller, Dave <dave@example.org>")
.await
.unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "Mueller, Dave");
assert_eq!(contact.get_addr(), "dave@example.org");
let contact_id = Contact::create(&t.ctx, "name1", "name2 <dave@example.org>")
.await
.unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "name1");
assert_eq!(contact.get_addr(), "dave@example.org");
assert!(Contact::create(&t.ctx, "", "<dskjfdslk@sadklj.dk")
.await
.is_err());
assert!(Contact::create(&t.ctx, "", "<dskjf>dslk@sadklj.dk>")
.await
.is_err());
assert!(Contact::create(&t.ctx, "", "dskjfdslksadklj.dk")
.await
.is_err());
assert!(Contact::create(&t.ctx, "", "dskjfdslk@sadklj.dk>")
.await
.is_err());
assert!(Contact::create(&t.ctx, "", "dskjf@dslk@sadkljdk")
.await
.is_err());
assert!(Contact::create(&t.ctx, "", "dskjf dslk@d.e").await.is_err());
assert!(Contact::create(&t.ctx, "", "<dskjf dslk@sadklj.dk")
.await
.is_err());
}
}

View File

@@ -15,7 +15,7 @@ use crate::dc_tools::duration_to_str;
use crate::error::*;
use crate::events::{Event, EventEmitter, Events};
use crate::job::{self, Action};
use crate::key::{DcKey, Key, SignedPublicKey};
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::lot::Lot;
use crate::message::{self, Message, MessengerMessage, MsgId};
@@ -76,7 +76,11 @@ pub struct RunningState {
pub fn get_info() -> BTreeMap<&'static str, String> {
let mut res = BTreeMap::new();
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("sqlite_version", rusqlite::version().to_string());
let version =
String::from_utf8(libsqlite3_sys::SQLITE_VERSION.to_vec()).expect("invalid version");
res.insert("sqlite_version", version);
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
res.insert("level", "awesome".into());
res
@@ -85,8 +89,6 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
impl Context {
/// Creates new context.
pub async fn new(os_name: String, dbfile: PathBuf) -> Result<Context> {
// pretty_env_logger::try_init_timed().ok();
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
@@ -127,10 +129,8 @@ 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)
}
@@ -263,29 +263,31 @@ impl Context {
let is_configured = self.get_config_int(Config::Configured).await;
let dbversion = self
.sql
.get_raw_config_int(self, "dbversion")
.get_raw_config_int("dbversion")
.await
.unwrap_or_default();
let journal_mode = self
.sql
.query_get_value(self, "PRAGMA journal_mode;", paramsv![])
.query_value("PRAGMA journal_mode;", paramsx![])
.await
.unwrap_or_else(|| "unknown".to_string());
.unwrap_or_else(|_| "unknown".to_string());
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await;
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await;
let bcc_self = self.get_config_int(Config::BccSelf).await;
let prv_key_cnt: Option<isize> = self
let prv_key_cnt: Option<i32> = self
.sql
.query_get_value(self, "SELECT COUNT(*) FROM keypairs;", paramsv![])
.await;
.query_value("SELECT COUNT(*) FROM keypairs;", paramsx![])
.await
.ok();
let pub_key_cnt: Option<isize> = self
let pub_key_cnt: Option<i32> = self
.sql
.query_get_value(self, "SELECT COUNT(*) FROM acpeerstates;", paramsv![])
.await;
.query_value("SELECT COUNT(*) FROM acpeerstates;", paramsx![])
.await
.ok();
let fingerprint_str = match SignedPublicKey::load_self(self).await {
Ok(key) => Key::from(key).fingerprint(),
Ok(key) => key.fingerprint().hex(),
Err(err) => format!("<key failure: {}>", err),
};
@@ -295,18 +297,16 @@ impl Context {
let mvbox_move = self.get_config_int(Config::MvboxMove).await;
let folders_configured = self
.sql
.get_raw_config_int(self, "folders_configured")
.get_raw_config_int("folders_configured")
.await
.unwrap_or_default();
let configured_sentbox_folder = self
.sql
.get_raw_config(self, "configured_sentbox_folder")
.get_config(Config::ConfiguredSentboxFolder)
.await
.unwrap_or_else(|| "<unset>".to_string());
let configured_mvbox_folder = self
.sql
.get_raw_config(self, "configured_mvbox_folder")
.get_config(Config::ConfiguredMvboxFolder)
.await
.unwrap_or_else(|| "<unset>".to_string());
@@ -358,30 +358,22 @@ impl Context {
pub async fn get_fresh_msgs(&self) -> Vec<MsgId> {
let show_deaddrop: i32 = 0;
self.sql
.query_map(
concat!(
"SELECT m.id",
" FROM msgs m",
" LEFT JOIN contacts ct",
" ON m.from_id=ct.id",
" LEFT JOIN chats c",
" ON m.chat_id=c.id",
" WHERE m.state=?",
" AND m.hidden=0",
" AND m.chat_id>?",
" AND ct.blocked=0",
" AND (c.blocked=0 OR c.blocked=?)",
" ORDER BY m.timestamp DESC,m.id DESC;"
),
paramsv![10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
|row| row.get::<_, MsgId>(0),
|rows| {
let mut ret = Vec::new();
for row in rows {
ret.push(row?);
}
Ok(ret)
},
.query_values(
r#"
SELECT m.id
FROM msgs m
LEFT JOIN contacts ct
ON m.from_id=ct.id
LEFT JOIN chats c
ON m.chat_id=c.id
WHERE m.state=?
AND m.hidden=0
AND m.chat_id>?
AND ct.blocked=0
AND (c.blocked=0 OR c.blocked=?)
ORDER BY m.timestamp DESC,m.id DESC;
"#,
paramsx![10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
)
.await
.unwrap_or_default()
@@ -397,78 +389,53 @@ impl Context {
let strLikeBeg = format!("{}%", real_query);
let query = if !chat_id.is_unset() {
concat!(
"SELECT m.id AS id, m.timestamp AS timestamp",
" FROM msgs m",
" LEFT JOIN contacts ct",
" ON m.from_id=ct.id",
" WHERE m.chat_id=?",
" AND m.hidden=0",
" AND ct.blocked=0",
" AND (txt LIKE ? OR ct.name LIKE ?)",
" ORDER BY m.timestamp,m.id;"
)
r#"
SELECT m.id
FROM msgs
LEFT JOIN contacts ct
ON m.from_id=ct.id
WHERE m.chat_id=?
AND m.hidden=0
AND ct.blocked=0
AND (txt LIKE ? OR ct.name LIKE ?)
ORDER BY m.timestamp,m.id;
"#
} else {
concat!(
"SELECT m.id AS id, m.timestamp AS timestamp",
" FROM msgs m",
" LEFT JOIN contacts ct",
" ON m.from_id=ct.id",
" LEFT JOIN chats c",
" ON m.chat_id=c.id",
" WHERE m.chat_id>9",
" AND m.hidden=0",
" AND (c.blocked=0 OR c.blocked=?)",
" AND ct.blocked=0",
" AND (m.txt LIKE ? OR ct.name LIKE ?)",
" ORDER BY m.timestamp DESC,m.id DESC;"
)
r#"
SELECT m.id
FROM msgs m
LEFT JOIN contacts ct
ON m.from_id=ct.id
LEFT JOIN chats c
ON m.chat_id=c.id
WHERE m.chat_id>9
AND m.hidden=0
AND (c.blocked=0 OR c.blocked=?)
AND ct.blocked=0
AND (m.txt LIKE ? OR ct.name LIKE ?)
ORDER BY m.timestamp DESC,m.id DESC;
"#
};
self.sql
.query_map(
query,
paramsv![chat_id, strLikeInText, strLikeBeg],
|row| row.get::<_, MsgId>("id"),
|rows| {
let mut ret = Vec::new();
for id in rows {
ret.push(id?);
}
Ok(ret)
},
)
.query_values(query, paramsx![chat_id, strLikeInText, strLikeBeg])
.await
.unwrap_or_default()
}
pub fn is_inbox(&self, folder_name: impl AsRef<str>) -> bool {
folder_name.as_ref() == "INBOX"
pub async fn is_inbox(&self, folder_name: impl AsRef<str>) -> bool {
self.get_config(Config::ConfiguredInboxFolder).await
== Some(folder_name.as_ref().to_string())
}
pub async fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
let sentbox_name = self
.sql
.get_raw_config(self, "configured_sentbox_folder")
.await;
if let Some(name) = sentbox_name {
name == folder_name.as_ref()
} else {
false
}
self.get_config(Config::ConfiguredSentboxFolder).await
== Some(folder_name.as_ref().to_string())
}
pub async fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
let mvbox_name = self
.sql
.get_raw_config(self, "configured_mvbox_folder")
.await;
if let Some(name) = mvbox_name {
name == folder_name.as_ref()
} else {
false
}
self.get_config(Config::ConfiguredMvboxFolder).await
== Some(folder_name.as_ref().to_string())
}
pub async fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) {
@@ -556,7 +523,7 @@ mod tests {
#[async_std::test]
async fn test_get_fresh_msgs() {
let t = dummy_context().await;
let t = TestContext::new().await;
let fresh = t.ctx.get_fresh_msgs().await;
assert!(fresh.is_empty())
}
@@ -611,13 +578,13 @@ mod tests {
#[async_std::test]
async fn no_crashes_on_context_deref() {
let t = dummy_context().await;
let t = TestContext::new().await;
std::mem::drop(t.ctx);
}
#[async_std::test]
async fn test_get_info() {
let t = dummy_context().await;
let t = TestContext::new().await;
let info = t.ctx.get_info().await;
assert!(info.get("database_dir").is_some());

File diff suppressed because it is too large Load Diff

View File

@@ -482,15 +482,6 @@ pub struct InvalidEmailError {
addr: String,
}
impl InvalidEmailError {
fn new(msg: impl Into<String>, addr: impl Into<String>) -> InvalidEmailError {
InvalidEmailError {
message: msg.into(),
addr: addr.into(),
}
}
}
/// Very simple email address wrapper.
///
/// Represents an email address, right now just the `name@domain` portion.
@@ -530,17 +521,24 @@ impl FromStr for EmailAddress {
/// Performs a dead-simple parse of an email address.
fn from_str(input: &str) -> Result<EmailAddress, InvalidEmailError> {
if input.is_empty() {
return Err(InvalidEmailError::new("empty string is not valid", input));
}
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
let err = |msg: &str| {
Err(InvalidEmailError {
message: msg.to_string(),
addr: input.to_string(),
})
};
if input.is_empty() {
return err("empty string is not valid");
}
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
if input
.chars()
.any(|c| c.is_whitespace() || c == '<' || c == '>')
{
return err("Email must not contain whitespaces, '>' or '<'");
}
match &parts[..] {
[domain, local] => {
if local.is_empty() {
@@ -570,14 +568,6 @@ impl FromStr for EmailAddress {
}
}
impl rusqlite::types::ToSql for EmailAddress {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Text(self.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
/// Utility to check if a in the binary represantion of listflags
/// the bit at position bitindex is 1.
pub(crate) fn listflags_has(listflags: u32, bitindex: usize) -> bool {
@@ -776,7 +766,7 @@ mod tests {
#[async_std::test]
async fn test_file_handling() {
let t = dummy_context().await;
let t = TestContext::new().await;
let context = &t.ctx;
macro_rules! dc_file_exist {
($ctx:expr, $fname:expr) => {
@@ -855,7 +845,7 @@ mod tests {
#[async_std::test]
async fn test_create_smeared_timestamp() {
let t = dummy_context().await;
let t = TestContext::new().await;
assert_ne!(
dc_create_smeared_timestamp(&t.ctx).await,
dc_create_smeared_timestamp(&t.ctx).await
@@ -871,7 +861,7 @@ mod tests {
#[async_std::test]
async fn test_create_smeared_timestamps() {
let t = dummy_context().await;
let t = TestContext::new().await;
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1;
let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
let next = dc_smeared_time(&t.ctx).await;

View File

@@ -11,7 +11,7 @@ use crate::context::Context;
use crate::error::*;
use crate::headerdef::HeaderDef;
use crate::headerdef::HeaderDefMap;
use crate::key::{DcKey, Key, SignedPublicKey, SignedSecretKey};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::*;
use crate::peerstate::*;
use crate::pgp;
@@ -91,9 +91,9 @@ impl EncryptHelper {
context: &Context,
min_verified: PeerstateVerifiedStatus,
mail_to_encrypt: lettre_email::PartBuilder,
peerstates: Vec<(Option<Peerstate<'_>>, &str)>,
peerstates: Vec<(Option<Peerstate>, &str)>,
) -> Result<String> {
let mut keyring = Keyring::default();
let mut keyring: Keyring<SignedPublicKey> = Keyring::new();
for (peerstate, addr) in peerstates
.into_iter()
@@ -104,9 +104,8 @@ impl EncryptHelper {
})?;
keyring.add(key);
}
let public_key = Key::from(self.public_key);
keyring.add(public_key);
let sign_key = Key::from(SignedSecretKey::load_self(context).await?);
keyring.add(self.public_key.clone());
let sign_key = SignedSecretKey::load_self(context).await?;
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
@@ -120,7 +119,7 @@ pub async fn try_decrypt(
context: &Context,
mail: &ParsedMail<'_>,
message_time: i64,
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
) -> Result<(Option<Vec<u8>>, HashSet<Fingerprint>)> {
let from = mail
.headers
.get_header(HeaderDef::From_)
@@ -133,7 +132,7 @@ pub async fn try_decrypt(
let autocryptheader = Aheader::from_headers(context, &from, &mail.headers);
if message_time > 0 {
peerstate = Peerstate::from_addr(context, &from).await;
peerstate = Peerstate::from_addr(context, &from).await.ok();
if let Some(ref mut peerstate) = peerstate {
if let Some(ref header) = autocryptheader {
@@ -144,47 +143,40 @@ pub async fn try_decrypt(
peerstate.save_to_db(&context.sql, false).await?;
}
} else if let Some(ref header) = autocryptheader {
let p = Peerstate::from_header(context, header, message_time);
let p = Peerstate::from_header(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
}
/* possibly perform decryption */
let mut public_keyring_for_validate = Keyring::default();
let mut out_mail = None;
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();
let self_addr = context.get_config(Config::ConfiguredAddr).await;
if let Some(self_addr) = self_addr {
if let Ok(private_keyring) =
Keyring::load_self_private_for_decrypting(context, self_addr).await
{
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 peerstate.degrade_event.is_some() {
handle_degrade_event(context, &peerstate).await?;
}
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);
}
}
out_mail = decrypt_if_autocrypt_message(
context,
mail,
private_keyring,
public_keyring_for_validate,
&mut signatures,
)
.await?;
if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 {
peerstate = Peerstate::from_addr(&context, &from).await.ok();
}
if let Some(peerstate) = peerstate {
if peerstate.degrade_event.is_some() {
handle_degrade_event(context, &peerstate).await?;
}
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);
}
}
let out_mail = decrypt_if_autocrypt_message(
context,
mail,
private_keyring,
public_keyring_for_validate,
&mut signatures,
)
.await?;
Ok((out_mail, signatures))
}
@@ -218,9 +210,9 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail
async fn decrypt_if_autocrypt_message<'a>(
context: &Context,
mail: &ParsedMail<'a>,
private_keyring: Keyring,
public_keyring_for_validate: Keyring,
ret_valid_signatures: &mut HashSet<String>,
private_keyring: Keyring<SignedSecretKey>,
public_keyring_for_validate: Keyring<SignedPublicKey>,
ret_valid_signatures: &mut HashSet<Fingerprint>,
) -> Result<Option<Vec<u8>>> {
// The returned bool is true if we detected an Autocrypt-encrypted
// message and successfully decrypted it. Decryption then modifies the
@@ -250,9 +242,9 @@ async fn decrypt_if_autocrypt_message<'a>(
/// Returns Ok(None) if nothing encrypted was found.
async fn decrypt_part(
mail: &ParsedMail<'_>,
private_keyring: Keyring,
public_keyring_for_validate: Keyring,
ret_valid_signatures: &mut HashSet<String>,
private_keyring: Keyring<SignedSecretKey>,
public_keyring_for_validate: Keyring<SignedPublicKey>,
ret_valid_signatures: &mut HashSet<Fingerprint>,
) -> Result<Option<Vec<u8>>> {
let data = mail.get_body_raw()?;
@@ -335,14 +327,14 @@ mod tests {
#[async_std::test]
async fn test_prexisting() {
let t = dummy_context().await;
let test_addr = configure_alice_keypair(&t.ctx).await;
let t = TestContext::new().await;
let test_addr = t.configure_alice().await;
assert_eq!(ensure_secret_key_exists(&t.ctx).await.unwrap(), test_addr);
}
#[async_std::test]
async fn test_not_configured() {
let t = dummy_context().await;
let t = TestContext::new().await;
assert!(ensure_secret_key_exists(&t.ctx).await.is_err());
}
}

View File

@@ -21,6 +21,7 @@ pub enum HeaderDef {
References,
InReplyTo,
Precedence,
ContentType,
ChatVersion,
ChatGroupId,
ChatGroupName,

View File

@@ -7,7 +7,7 @@ use async_imap::{
use async_std::net::{self, TcpStream};
use super::session::Session;
use crate::login_param::{dc_build_tls, CertificateChecks};
use crate::login_param::dc_build_tls;
use super::session::SessionStream;
@@ -78,10 +78,10 @@ impl Client {
pub async fn connect_secure<A: net::ToSocketAddrs, S: AsRef<str>>(
addr: A,
domain: S,
certificate_checks: CertificateChecks,
strict_tls: bool,
) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let tls = dc_build_tls(certificate_checks);
let tls = dc_build_tls(strict_tls);
let tls_stream: Box<dyn SessionStream> =
Box::new(tls.connect(domain.as_ref(), stream).await?);
let mut client = ImapClient::new(tls_stream);
@@ -118,16 +118,12 @@ impl Client {
})
}
pub async fn secure<S: AsRef<str>>(
self,
domain: S,
certificate_checks: CertificateChecks,
) -> ImapResult<Client> {
pub async fn secure<S: AsRef<str>>(self, domain: S, strict_tls: bool) -> ImapResult<Client> {
if self.is_secure {
Ok(self)
} else {
let Client { mut inner, .. } = self;
let tls = dc_build_tls(certificate_checks);
let tls = dc_build_tls(strict_tls);
inner.run_command_and_check_ok("STARTTLS", None).await?;
let stream = inner.into_inner();

View File

@@ -4,7 +4,7 @@ use async_imap::extensions::idle::IdleResponse;
use async_std::prelude::*;
use std::time::{Duration, SystemTime};
use crate::context::Context;
use crate::{context::Context, scheduler::InterruptInfo};
use super::select_folder;
use super::session::Session;
@@ -34,7 +34,11 @@ impl Imap {
self.config.can_idle
}
pub async fn idle(&mut self, context: &Context, watch_folder: Option<String>) -> Result<bool> {
pub async fn idle(
&mut self,
context: &Context,
watch_folder: Option<String>,
) -> Result<InterruptInfo> {
use futures::future::FutureExt;
if !self.can_idle() {
@@ -46,7 +50,7 @@ impl Imap {
let session = self.session.take();
let timeout = Duration::from_secs(23 * 60);
let mut probe_network = false;
let mut info = Default::default();
if let Some(session) = session {
let mut handle = session.idle();
@@ -58,7 +62,7 @@ impl Imap {
enum Event {
IdleResponse(IdleResponse),
Interrupt(bool),
Interrupt(InterruptInfo),
}
if self.skip_next_idle_wait {
@@ -90,8 +94,8 @@ impl Imap {
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
info!(context, "Idle wait was interrupted");
}
Ok(Event::Interrupt(probe)) => {
probe_network = probe;
Ok(Event::Interrupt(i)) => {
info = i;
info!(context, "Idle wait was interrupted");
}
Err(err) => {
@@ -125,14 +129,14 @@ impl Imap {
}
}
Ok(probe_network)
Ok(info)
}
pub(crate) async fn fake_idle(
&mut self,
context: &Context,
watch_folder: Option<String>,
) -> bool {
) -> InterruptInfo {
// Idle using polling. This is also needed if we're not yet configured -
// in this case, we're waiting for a configure job (and an interrupt).
@@ -144,7 +148,7 @@ impl Imap {
return self.idle_interrupt.recv().await.unwrap_or_default();
}
let mut probe_network = false;
let mut info: InterruptInfo = Default::default();
if self.skip_next_idle_wait {
// interrupt_idle has happened before we
// provided self.interrupt
@@ -157,10 +161,10 @@ impl Imap {
enum Event {
Tick,
Interrupt(bool),
Interrupt(InterruptInfo),
}
// loop until we are interrupted or if we fetched something
probe_network =
info =
loop {
use futures::future::FutureExt;
match interval
@@ -181,7 +185,7 @@ impl Imap {
}
if self.config.can_idle {
// we only fake-idled because network was gone during IDLE, probably
break false;
break InterruptInfo::new(false, None);
}
info!(context, "fake_idle is connected");
// we are connected, let's see if fetching messages results
@@ -194,7 +198,7 @@ impl Imap {
Ok(res) => {
info!(context, "fetch_new_messages returned {:?}", res);
if res {
break false;
break InterruptInfo::new(false, None);
}
}
Err(err) => {
@@ -204,9 +208,9 @@ impl Imap {
}
}
}
Event::Interrupt(probe_network) => {
Event::Interrupt(info) => {
// Interrupt
break probe_network;
break info;
}
}
};
@@ -222,6 +226,6 @@ impl Imap {
/ 1000.,
);
probe_network
info
}
}

View File

@@ -3,6 +3,8 @@
//! uses [async-email/async-imap](https://github.com/async-email/async-imap)
//! to implement connect, fetch, delete functionality with standard IMAP servers.
#![forbid(clippy::indexing_slicing)]
use std::collections::BTreeMap;
use async_imap::{
@@ -27,7 +29,8 @@ use crate::message::{self, update_server_uid};
use crate::mimeparser;
use crate::oauth2::dc_get_oauth2_access_token;
use crate::param::Params;
use crate::stock::StockMessage;
use crate::provider::get_provider_info;
use crate::{scheduler::InterruptInfo, stock::StockMessage};
mod client;
mod idle;
@@ -109,7 +112,7 @@ const SELECT_ALL: &str = "1:*";
#[derive(Debug)]
pub struct Imap {
idle_interrupt: Receiver<bool>,
idle_interrupt: Receiver<InterruptInfo>,
config: ImapConfig,
session: Option<Session>,
connected: bool,
@@ -149,7 +152,7 @@ struct ImapConfig {
pub imap_port: u16,
pub imap_user: String,
pub imap_pw: String,
pub certificate_checks: CertificateChecks,
pub strict_tls: bool,
pub server_flags: usize,
pub selected_folder: Option<String>,
pub selected_mailbox: Option<Mailbox>,
@@ -169,7 +172,7 @@ impl Default for ImapConfig {
imap_port: 0,
imap_user: "".into(),
imap_pw: "".into(),
certificate_checks: Default::default(),
strict_tls: false,
server_flags: 0,
selected_folder: None,
selected_mailbox: None,
@@ -181,7 +184,7 @@ impl Default for ImapConfig {
}
impl Imap {
pub fn new(idle_interrupt: Receiver<bool>) -> Self {
pub fn new(idle_interrupt: Receiver<InterruptInfo>) -> Self {
Imap {
idle_interrupt,
config: Default::default(),
@@ -228,7 +231,7 @@ impl Imap {
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.certificate_checks).await
client.secure(imap_server, config.strict_tls).await
} else {
Ok(client)
}
@@ -240,12 +243,8 @@ impl Imap {
let imap_server: &str = config.imap_server.as_ref();
let imap_port = config.imap_port;
Client::connect_secure(
(imap_server, imap_port),
imap_server,
config.certificate_checks,
)
.await
Client::connect_secure((imap_server, imap_port), imap_server, config.strict_tls)
.await
};
let login_res = match connection_res {
@@ -295,6 +294,8 @@ impl Imap {
match login_res {
Ok(session) => {
// needs to be set here to ensure it is set on reconnects.
self.connected = true;
self.session = Some(session);
Ok(())
}
@@ -315,9 +316,15 @@ impl Imap {
}
async fn unsetup_handle(&mut self, context: &Context) {
// Close folder if messages should be expunged
if let Err(err) = self.close_folder(context).await {
warn!(context, "failed to close folder: {:?}", err);
}
// Logout from the server
if let Some(mut session) = self.session.take() {
if let Err(err) = session.close().await {
warn!(context, "failed to close connection: {:?}", err);
if let Err(err) = session.logout().await {
warn!(context, "failed to logout: {:?}", err);
}
}
self.connected = false;
@@ -377,7 +384,15 @@ impl Imap {
config.imap_port = imap_port;
config.imap_user = imap_user.to_string();
config.imap_pw = imap_pw.to_string();
config.certificate_checks = lp.imap_certificate_checks;
let provider = get_provider_info(&lp.addr);
config.strict_tls = match lp.imap_certificate_checks {
CertificateChecks::Automatic => {
provider.map_or(false, |provider| provider.strict_tls)
}
CertificateChecks::Strict => true,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => false,
};
config.server_flags = server_flags;
}
@@ -458,7 +473,7 @@ impl Imap {
folder: S,
) -> (u32, u32) {
let key = format!("imap.mailbox.{}", folder.as_ref());
if let Some(entry) = context.sql.get_raw_config(context, &key).await {
if let Some(entry) = context.sql.get_raw_config(&key).await {
// the entry has the format `imap.mailbox.<folder>=<uidvalidity>:<lastseenuid>`
let mut parts = entry.split(':');
(
@@ -719,9 +734,17 @@ impl Imap {
folder: S,
server_uids: &[u32],
) -> (Option<u32>, usize) {
if server_uids.is_empty() {
return (None, 0);
}
let set = match server_uids {
[] => return (None, 0),
[server_uid] => server_uid.to_string(),
[first_uid, .., last_uid] => {
// XXX: it is assumed that UIDs are sorted and
// contiguous. If UIDs are not contiguous, more
// messages than needed will be downloaded.
debug_assert!(first_uid < last_uid, "uids must be sorted");
format!("{}:{}", first_uid, last_uid)
}
};
if !self.is_connected() {
warn!(context, "Not connected");
@@ -737,15 +760,6 @@ impl Imap {
let session = self.session.as_mut().unwrap();
let set = if server_uids.len() == 1 {
server_uids[0].to_string()
} else {
let first_uid = server_uids[0];
let last_uid = server_uids[server_uids.len() - 1];
assert!(first_uid < last_uid, "uids must be sorted");
format!("{}:{}", first_uid, last_uid)
};
let mut msgs = match session.uid_fetch(&set, BODY_FLAGS).await {
Ok(msgs) => msgs,
Err(err) => {
@@ -798,7 +812,6 @@ impl Imap {
Ok(_) => Some(server_uid),
Err(err) => {
warn!(context, "dc_receive_imf error: {}", err);
read_errors += 1;
None
}
}
@@ -972,7 +985,7 @@ impl Imap {
uid: u32,
) -> Option<ImapActionResult> {
if uid == 0 {
return Some(ImapActionResult::Failed);
return Some(ImapActionResult::RetryLater);
}
if !self.is_connected() {
// currently jobs are only performed on the INBOX thread
@@ -1116,10 +1129,7 @@ impl Imap {
context: &Context,
create_mvbox: bool,
) -> Result<()> {
let folders_configured = context
.sql
.get_raw_config_int(context, "folders_configured")
.await;
let folders_configured = context.sql.get_raw_config_int("folders_configured").await;
if folders_configured.unwrap_or_default() >= DC_FOLDERS_CONFIGURED_VERSION {
return Ok(());
}
@@ -1140,54 +1150,54 @@ impl Imap {
}
};
let mut delimiter = ".".to_string();
let mut delimiter_is_default = true;
let mut sentbox_folder = None;
let mut mvbox_folder = None;
let mut delimiter = ".".to_string();
if let Some(folder) = folders.next().await {
let folder = folder.map_err(|err| Error::Other(err.to_string()))?;
if let Some(d) = folder.delimiter() {
if !d.is_empty() {
delimiter = d.to_string();
}
}
}
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
let fallback_folder = format!("INBOX{}DeltaChat", delimiter);
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()))?;
info!(context, "Scanning folder: {:?}", folder);
if mvbox_folder.is_none()
&& (folder.name() == "DeltaChat" || folder.name() == fallback_folder)
{
mvbox_folder = Some(folder.name().to_string());
}
if sentbox_folder.is_none() {
if let FolderMeaning::SentObjects = get_folder_meaning(&folder) {
sentbox_folder = Some(folder);
} else if let FolderMeaning::SentObjects = get_folder_meaning_by_name(&folder) {
sentbox_folder = Some(folder);
// Update the delimiter iff there is a different one, but only once.
if let Some(d) = folder.delimiter() {
if delimiter_is_default && !d.is_empty() && delimiter != d {
delimiter = d.to_string();
fallback_folder = get_fallback_folder(&delimiter);
delimiter_is_default = false;
}
}
if mvbox_folder.is_some() && sentbox_folder.is_some() {
break;
if folder.name() == "DeltaChat" {
// Always takes precendent
mvbox_folder = Some(folder.name().to_string());
} else if folder.name() == fallback_folder {
// only set iff none has been already set
if mvbox_folder.is_none() {
mvbox_folder = Some(folder.name().to_string());
}
} 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) {
// only set iff none has been already set
if sentbox_folder.is_none() {
sentbox_folder = Some(folder.name().to_string());
}
}
}
info!(context, "sentbox folder is {:?}", sentbox_folder);
drop(folders);
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
info!(context, "sentbox folder is {:?}", sentbox_folder);
if mvbox_folder.is_none() && create_mvbox {
info!(context, "Creating MVBOX-folder \"DeltaChat\"...",);
match session.create("DeltaChat").await {
Ok(_) => {
mvbox_folder = Some("DeltaChat".into());
info!(context, "MVBOX-folder created.",);
}
Err(err) => {
@@ -1221,23 +1231,16 @@ impl Imap {
}
}
context
.sql
.set_raw_config(context, "configured_inbox_folder", Some("INBOX"))
.set_config(Config::ConfiguredInboxFolder, Some("INBOX"))
.await?;
if let Some(ref mvbox_folder) = mvbox_folder {
context
.sql
.set_raw_config(context, "configured_mvbox_folder", Some(mvbox_folder))
.set_config(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
.await?;
}
if let Some(ref sentbox_folder) = sentbox_folder {
context
.sql
.set_raw_config(
context,
"configured_sentbox_folder",
Some(sentbox_folder.name()),
)
.set_config(Config::ConfiguredSentboxFolder, Some(sentbox_folder))
.await?;
}
context
@@ -1292,7 +1295,7 @@ impl Imap {
.sql
.execute(
"UPDATE msgs SET server_folder='',server_uid=0 WHERE server_folder=?",
paramsv![folder],
paramsx![folder],
)
.await
{
@@ -1395,7 +1398,11 @@ 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;
update_server_uid(context, rfc724_mid, server_folder, server_uid).await;
context
.interrupt_inbox(InterruptInfo::new(false, Some(msg_id)))
.await;
info!(context, "Updating server_uid and interrupting")
}
Ok(true)
} else {
@@ -1439,7 +1446,7 @@ async fn prefetch_is_reply_to_chat_message(
false
}
async fn prefetch_should_download(
pub(crate) async fn prefetch_should_download(
context: &Context,
headers: &[mailparse::MailHeader<'_>],
show_emails: ShowEmails,
@@ -1447,6 +1454,13 @@ async fn prefetch_should_download(
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;
let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
let from = from.to_ascii_lowercase();
from.contains("mailer-daemon") || from.contains("mail-daemon")
} else {
false
};
// Autocrypt Setup Message should be shown even if it is from non-chat client.
let is_autocrypt_setup_message = headers
.get_header_value(HeaderDef::AutocryptSetupMessage)
@@ -1457,6 +1471,7 @@ async fn prefetch_should_download(
let accepted_contact = origin.is_known();
let show = is_autocrypt_setup_message
|| maybe_ndn
|| match show_emails {
ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
ShowEmails::AcceptedContacts => {
@@ -1514,3 +1529,7 @@ async fn message_needs_processing(
true
}
fn get_fallback_folder(delimiter: &str) -> String {
format!("INBOX{}DeltaChat", delimiter)
}

View File

@@ -27,7 +27,7 @@ impl Imap {
///
/// CLOSE is considerably faster than an EXPUNGE, see
/// https://tools.ietf.org/html/rfc3501#section-6.4.2
async fn close_folder(&mut self, context: &Context) -> Result<()> {
pub(super) async fn close_folder(&mut self, context: &Context) -> Result<()> {
if let Some(ref folder) = self.config.selected_folder {
info!(context, "Expunge messages in \"{}\".", folder);

View File

@@ -1,5 +1,6 @@
//! # Import/export module
use std::any::Any;
use std::cmp::{max, min};
use async_std::path::{Path, PathBuf};
@@ -16,7 +17,7 @@ use crate::dc_tools::*;
use crate::e2ee;
use crate::error::*;
use crate::events::Event;
use crate::key::{self, DcKey, Key, SignedSecretKey};
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::*;
@@ -95,21 +96,21 @@ pub async fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result
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;
}
info!(context, "backup_time of {} is {}", name, curr_backup_time);
sql.close().await;
sql.open(context, &path, true).await?;
let curr_backup_time = sql
.get_raw_config_int("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;
}
}
}
match newest_backup_path {
Some(path) => Ok(path.to_string_lossy().into_owned()),
None => bail!("no backup found in {}", dir_name.display()),
@@ -181,7 +182,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
passphrase.len() >= 2,
"Passphrase must be at least 2 chars long."
);
let private_key = Key::from(SignedSecretKey::load_self(context).await?);
let private_key = SignedSecretKey::load_self(context).await?;
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await {
false => None,
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
@@ -244,7 +245,7 @@ pub fn create_setup_code(_context: &Context) -> String {
}
async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
if !context.sql.get_raw_config_bool(context, "bcc_self").await {
if !context.sql.get_raw_config_bool("bcc_self").await {
let mut msg = Message::new(Viewtype::Text);
// TODO: define this as a stockstring once the wording is settled.
msg.text = Some(
@@ -291,12 +292,8 @@ async fn set_self_key(
prefer_encrypt_required: bool,
) -> Result<()> {
// try hard to only modify key-state
let keys = Key::from_armored_string(armored, KeyType::Private)
.and_then(|(k, h)| if k.verify() { Some((k, h)) } else { None })
.and_then(|(k, h)| k.split_key().map(|pub_key| (k, pub_key, h)));
ensure!(keys.is_some(), "Not a valid private key");
let (private_key, public_key, header) = keys.unwrap();
let (private_key, header) = SignedSecretKey::from_asc(armored)?;
let public_key = private_key.split_public_key()?;
let preferencrypt = header.get("Autocrypt-Prefer-Encrypt");
match preferencrypt.map(|s| s.as_str()) {
Some(headerval) => {
@@ -322,15 +319,10 @@ async fn set_self_key(
let self_addr = context.get_config(Config::ConfiguredAddr).await;
ensure!(self_addr.is_some(), "Missing self addr");
let addr = EmailAddress::new(&self_addr.unwrap_or_default())?;
let (public, secret) = match (public_key, private_key) {
(Key::Public(p), Key::Secret(s)) => (p, s),
_ => bail!("wrong keys unpacked"),
};
let keypair = pgp::KeyPair {
addr,
public,
secret,
public: public_key,
secret: private_key,
};
key::store_self_keypair(
context,
@@ -404,6 +396,7 @@ async fn imex_inner(
Ok(())
}
Err(err) => {
error!(context, "IMEX FAILED: {}", err);
context.emit_event(Event::ImexProgress(0));
bail!("IMEX FAILED to complete: {}", err);
}
@@ -436,51 +429,35 @@ async fn import_backup(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?;
delete_and_reset_all_device_msgs(&context).await?;
let total_files_cnt = context
let total_files_cnt: i32 = context
.sql
.query_get_value::<isize>(context, "SELECT COUNT(*) FROM backup_blobs;", paramsv![])
.query_value("SELECT COUNT(*) FROM backup_blobs;", paramsx![])
.await
.unwrap_or_default() as usize;
.unwrap_or_default();
let total_files_cnt = total_files_cnt as usize;
info!(
context,
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
);
let files = context
.sql
.query_map(
"SELECT file_name, file_content FROM backup_blobs ORDER BY id;",
paramsv![],
|row| {
let name: String = row.get(0)?;
let blob: Vec<u8> = row.get(1)?;
let pool = context.sql.get_pool().await?;
let mut files = sqlx::query_as("SELECT file_name, file_content FROM backup_blobs ORDER BY id;")
.fetch(&pool);
Ok((name, blob))
},
|files| {
files
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
let mut all_files_extracted = true;
for (processed_files_cnt, (file_name, file_blob)) in files.into_iter().enumerate() {
let mut processed_files_cnt = 0;
while let Some(files_result) = files.next().await {
let (file_name, file_blob): (String, Vec<u8>) = files_result?;
if context.shall_stop_ongoing().await {
all_files_extracted = false;
break;
}
let mut permille = processed_files_cnt * 1000 / total_files_cnt;
if permille < 10 {
permille = 10
@@ -495,19 +472,22 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
let path_filename = context.get_blobdir().join(file_name);
dc_write_file(context, &path_filename, &file_blob).await?;
processed_files_cnt += 1;
}
if all_files_extracted {
// only delete backup_blobs if all files were successfully extracted
context
.sql
.execute("DROP TABLE backup_blobs;", paramsv![])
.await?;
context.sql.execute("VACUUM;", paramsv![]).await.ok();
Ok(())
} else {
bail!("received stop signal");
}
ensure!(
processed_files_cnt == total_files_cnt,
"received stop signal"
);
context
.sql
.execute("DROP TABLE backup_blobs;", paramsx![])
.await?;
context.sql.execute("VACUUM;", paramsx![]).await?;
Ok(())
}
/*******************************************************************************
@@ -523,9 +503,9 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
let dest_path_filename = dc_get_next_backup_path(dir, now).await?;
let dest_path_string = dest_path_filename.to_string_lossy().to_string();
sql::housekeeping(context).await;
sql::housekeeping(context).await?;
context.sql.execute("VACUUM;", paramsv![]).await.ok();
context.sql.execute("VACUUM;", paramsx![]).await.ok();
// we close the database during the copy of the dbfile
context.sql.close().await;
@@ -539,7 +519,7 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
context
.sql
.open(&context, &context.get_dbfile(), false)
.await;
.await?;
if !copied {
bail!(
@@ -549,17 +529,9 @@ async fn export_backup(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?;
let res = match add_files_to_export(context, &dest_sql).await {
Err(err) => {
dc_delete_file(context, &dest_path_filename).await;
error!(context, "backup failed: {}", err);
Err(err)
}
Ok(()) => {
dest_sql
.set_raw_config_int(context, "backup_time", now as i32)
@@ -567,6 +539,11 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
context.emit_event(Event::ImexFileWritten(dest_path_filename));
Ok(())
}
Err(err) => {
dc_delete_file(context, &dest_path_filename).await;
error!(context, "backup failed: {}", err);
Err(err)
}
};
dest_sql.close().await;
@@ -579,7 +556,7 @@ async fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> {
if !sql.table_exists("backup_blobs").await? {
sql.execute(
"CREATE TABLE backup_blobs (id INTEGER PRIMARY KEY, file_name, file_content);",
paramsv![],
paramsx![],
)
.await?;
}
@@ -591,41 +568,38 @@ async fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> {
info!(context, "EXPORT: total_files_cnt={}", total_files_cnt);
sql.with_conn_async(|conn| async move {
// scan directory, pass 2: copy files
let mut dir_handle = async_std::fs::read_dir(&dir).await?;
// scan directory, pass 2: copy files
let mut dir_handle = async_std::fs::read_dir(&dir).await?;
let mut processed_files_cnt = 0;
while let Some(entry) = dir_handle.next().await {
let entry = entry?;
if context.shall_stop_ongoing().await {
return Ok(());
}
processed_files_cnt += 1;
let permille = max(min(processed_files_cnt * 1000 / total_files_cnt, 990), 10);
context.emit_event(Event::ImexProgress(permille));
let mut processed_files_cnt = 0;
while let Some(entry) = dir_handle.next().await {
let entry = entry?;
if context.shall_stop_ongoing().await {
return Ok(());
}
processed_files_cnt += 1;
let permille = max(min(processed_files_cnt * 1000 / total_files_cnt, 990), 10);
context.emit_event(Event::ImexProgress(permille));
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
if name.starts_with("delta-chat") && name.ends_with(".bak") {
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
if name.starts_with("delta-chat") && name.ends_with(".bak") {
continue;
}
info!(context, "EXPORT: copying filename={}", name);
let curr_path_filename = context.get_blobdir().join(entry.file_name());
if let Ok(buf) = dc_read_file(context, &curr_path_filename).await {
if buf.is_empty() {
continue;
}
info!(context, "EXPORT: copying filename={}", name);
let curr_path_filename = context.get_blobdir().join(entry.file_name());
if let Ok(buf) = dc_read_file(context, &curr_path_filename).await {
if buf.is_empty() {
continue;
}
// bail out if we can't insert
let mut stmt = conn.prepare_cached(
"INSERT INTO backup_blobs (file_name, file_content) VALUES (?, ?);",
)?;
stmt.execute(paramsv![name, buf])?;
}
// bail out if we can't insert
sql.execute(
"INSERT INTO backup_blobs (file_name, file_content) VALUES (?, ?);",
paramsx![name.as_ref(), buf],
)
.await?;
}
Ok(())
})
.await?;
}
Ok(())
}
@@ -688,30 +662,19 @@ async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
let mut export_errors = 0;
let keys = context
.sql
.query_map(
"SELECT id, public_key, private_key, is_default FROM keypairs;",
paramsv![],
|row| {
let id = row.get(0)?;
let public_key_blob: Vec<u8> = row.get(1)?;
let public_key = Key::from_slice(&public_key_blob, KeyType::Public);
let private_key_blob: Vec<u8> = row.get(2)?;
let private_key = Key::from_slice(&private_key_blob, KeyType::Private);
let is_default: i32 = row.get(3)?;
let pool = context.sql.get_pool().await?;
Ok((id, public_key, private_key, is_default))
},
|keys| {
keys.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
let mut keys = sqlx::query_as("SELECT id, public_key, private_key, is_default FROM keypairs;")
.fetch(&pool);
while let Some(keys_result) = keys.next().await {
let (id, public_key_blob, private_key_blob, is_default): (i64, Vec<u8>, Vec<u8>, i32) =
keys_result?;
let public_key = SignedPublicKey::from_slice(&public_key_blob);
let private_key = SignedSecretKey::from_slice(&private_key_blob);
for (id, public_key, private_key, is_default) in keys {
let id = Some(id).filter(|_| is_default != 0);
if let Ok(key) = public_key {
if export_key_to_asc_file(context, &dir, id, &key)
.await
@@ -735,28 +698,39 @@ async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
}
ensure!(export_errors == 0, "errors while exporting keys");
Ok(())
}
/*******************************************************************************
* Classic key export
******************************************************************************/
async fn export_key_to_asc_file(
async fn export_key_to_asc_file<T>(
context: &Context,
dir: impl AsRef<Path>,
id: Option<i64>,
key: &Key,
) -> std::io::Result<()> {
key: &T,
) -> std::io::Result<()>
where
T: DcKey + Any,
{
let file_name = {
let kind = if key.is_public() { "public" } else { "private" };
let any_key = key as &dyn Any;
let kind = if any_key.downcast_ref::<SignedPublicKey>().is_some() {
"public"
} else if any_key.downcast_ref::<SignedPublicKey>().is_some() {
"private"
} else {
"unknown"
};
let id = id.map_or("default".into(), |i| i.to_string());
dir.as_ref().join(format!("{}-key-{}.asc", kind, &id))
};
info!(context, "Exporting key {}", file_name.display());
dc_delete_file(context, &file_name).await;
let res = key.write_asc_to_file(&file_name, context).await;
let content = key.to_asc(None).into_bytes();
let res = dc_write_file(context, &file_name, &content).await;
if res.is_err() {
error!(context, "Cannot write key to {}", file_name.display());
} else {
@@ -774,9 +748,9 @@ mod tests {
#[async_std::test]
async fn test_render_setup_file() {
let t = test_context().await;
let t = TestContext::new().await;
configure_alice_keypair(&t.ctx).await;
t.configure_alice().await;
let msg = render_setup_file(&t.ctx, "hello").await.unwrap();
println!("{}", &msg);
// Check some substrings, indicating things got substituted.
@@ -793,12 +767,12 @@ mod tests {
#[async_std::test]
async fn test_render_setup_file_newline_replace() {
let t = dummy_context().await;
let t = TestContext::new().await;
t.ctx
.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
.await
.unwrap();
configure_alice_keypair(&t.ctx).await;
t.configure_alice().await;
let msg = render_setup_file(&t.ctx, "pw").await.unwrap();
println!("{}", &msg);
assert!(msg.contains("<p>hello<br>there</p>"));
@@ -806,7 +780,7 @@ mod tests {
#[async_std::test]
async fn test_create_setup_code() {
let t = dummy_context().await;
let t = TestContext::new().await;
let setupcode = create_setup_code(&t.ctx);
assert_eq!(setupcode.len(), 44);
assert_eq!(setupcode.chars().nth(4).unwrap(), '-');
@@ -821,8 +795,8 @@ mod tests {
#[async_std::test]
async fn test_export_key_to_asc_file() {
let context = dummy_context().await;
let key = Key::from(alice_keypair().public);
let context = TestContext::new().await;
let key = alice_keypair().public;
let blobdir = "$BLOBDIR";
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key)
.await

View File

@@ -6,13 +6,13 @@
use std::fmt;
use std::future::Future;
use deltachat_derive::{FromSql, ToSql};
use itertools::Itertools;
use rand::{thread_rng, Rng};
use async_smtp::smtp::response::Category;
use async_smtp::smtp::response::Code;
use async_smtp::smtp::response::Detail;
use async_std::prelude::*;
use deltachat_derive::*;
use itertools::Itertools;
use rand::{thread_rng, Rng};
use crate::blob::BlobObject;
use crate::chat::{self, ChatId};
@@ -31,13 +31,13 @@ use crate::message::{self, Message, MessageState};
use crate::mimefactory::MimeFactory;
use crate::param::*;
use crate::smtp::Smtp;
use crate::sql;
use crate::{scheduler::InterruptInfo, sql};
// results in ~3 weeks for the last backoff timespan
const JOB_RETRIES: u32 = 17;
/// Thread IDs
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
#[repr(i32)]
pub(crate) enum Thread {
Unknown = 0,
@@ -75,17 +75,7 @@ impl Default for Thread {
}
#[derive(
Debug,
Display,
Copy,
Clone,
PartialEq,
Eq,
PartialOrd,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Debug, Display, Copy, Clone, PartialEq, Eq, PartialOrd, FromPrimitive, ToPrimitive, Sqlx,
)]
#[repr(i32)]
pub enum Action {
@@ -155,6 +145,32 @@ impl fmt::Display for Job {
}
}
impl<'a> sqlx::FromRow<'a, sqlx::sqlite::SqliteRow> for Job {
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
use sqlx::Row;
let foreign_id: i32 = row.try_get("foreign_id")?;
if foreign_id < 0 {
return Err(sqlx::Error::Decode(
anyhow::anyhow!("invalid foreign_id").into(),
));
}
Ok(Job {
job_id: row.try_get::<i32, _>("id")? as u32,
action: row.try_get("action")?,
foreign_id: foreign_id as u32,
desired_timestamp: row.try_get_unchecked("desired_timestamp")?,
added_timestamp: row.try_get_unchecked("added_timestamp")?,
tries: row.try_get::<i32, _>("tries")? as u32,
param: row
.try_get::<String, _>("param")?
.parse()
.unwrap_or_default(),
pending_error: None,
})
}
}
impl Job {
pub fn new(action: Action, foreign_id: u32, param: Params, delay_seconds: i64) -> Self {
let timestamp = time();
@@ -180,7 +196,7 @@ impl Job {
if self.job_id != 0 {
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32])
.execute("DELETE FROM jobs WHERE id=?;", paramsx![self.job_id as i32])
.await?;
}
@@ -190,7 +206,7 @@ impl Job {
/// Saves the job to the database, creating a new entry if necessary.
///
/// The Job is consumed by this method.
pub async fn save(self, context: &Context) -> Result<()> {
pub(crate) async fn save(self, context: &Context) -> Result<()> {
let thread: Thread = self.action.into();
info!(context, "saving job for {}-thread: {:?}", thread, self);
@@ -200,22 +216,22 @@ impl Job {
.sql
.execute(
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
paramsv![
paramsx![
self.desired_timestamp,
self.tries as i64,
self.param.to_string(),
self.job_id as i32,
self.job_id as i32
],
)
.await?;
} else {
context.sql.execute(
"INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);",
paramsv![
paramsx![
self.added_timestamp,
thread,
self.action,
self.foreign_id,
self.foreign_id as i32,
self.param.to_string(),
self.desired_timestamp
]
@@ -329,7 +345,7 @@ impl Job {
}
}
pub async fn send_msg_to_smtp(&mut self, context: &Context, smtp: &mut Smtp) -> Status {
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;
@@ -394,40 +410,32 @@ impl Job {
context: &Context,
contact_id: u32,
) -> sql::Result<(Vec<u32>, Vec<String>)> {
// Extract message IDs from job parameters
let res: Vec<(u32, MsgId)> = context
.sql
.query_map(
"SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?",
paramsv![contact_id, self.job_id],
|row| {
let job_id: u32 = row.get(0)?;
let params_str: String = row.get(1)?;
let params: Params = params_str.parse().unwrap_or_default();
Ok((job_id, params))
},
|jobs| {
let res = jobs
.filter_map(|row| {
let (job_id, params) = row.ok()?;
let msg_id = params.get_msg_id()?;
Some((job_id, msg_id))
})
.collect();
Ok(res)
},
)
.await?;
// Load corresponding RFC724 message IDs
let mut job_ids = Vec::new();
let mut rfc724_mids = Vec::new();
for (job_id, msg_id) in res {
if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id).await {
job_ids.push(job_id);
rfc724_mids.push(rfc724_mid);
let pool = context.sql.get_pool().await?;
let mut rows = sqlx::query_as("SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?")
.bind(contact_id as i64)
.bind(self.job_id as i64)
.fetch(&pool);
while let Some(row) = rows.next().await {
let (job_id, params): (i64, String) = row?;
let params: Params = params.parse().unwrap_or_default();
let msg_id = params.get_msg_id().unwrap_or_default();
match Message::load_from_db(context, msg_id).await {
Ok(Message { rfc724_mid, .. }) => {
job_ids.push(job_id as u32);
rfc724_mids.push(rfc724_mid);
}
Err(err) => {
warn!(context, "failed to load mdn job message: {}", err);
}
}
}
Ok((job_ids, rfc724_mids))
}
@@ -498,16 +506,13 @@ impl Job {
}
async fn move_msg(&mut self, context: &Context, imap: &mut Imap) -> Status {
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
if let Err(err) = imap.ensure_configured_folders(context, true).await {
warn!(context, "could not configure folders: {:?}", err);
if let Err(err) = imap.connect_configured(context).await {
warn!(context, "could not connect: {:?}", err);
return Status::RetryLater;
}
let dest_folder = context
.sql
.get_raw_config(context, "configured_mvbox_folder")
.await;
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
let dest_folder = context.get_config(Config::ConfiguredMvboxFolder).await;
if let Some(dest_folder) = dest_folder {
let server_folder = msg.server_folder.as_ref().unwrap();
@@ -518,7 +523,7 @@ impl Job {
{
ImapActionResult::RetryLater => Status::RetryLater,
ImapActionResult::Success => {
// XXX Rust-Imap provides no target uid on mv, so just set it to 0
// Rust-Imap provides no target uid on mv, so just set it to 0, update again when precheck_imf() is called for the moved message
message::update_server_uid(context, &msg.rfc724_mid, &dest_folder, 0).await;
Status::Finished(Ok(()))
}
@@ -541,6 +546,11 @@ impl Job {
/// records pointing to the same message on the server, the job
/// also removes the message on the server.
async fn delete_msg_on_imap(&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;
}
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
if !msg.rfc724_mid.is_empty() {
@@ -611,12 +621,13 @@ impl Job {
}
async fn empty_server(&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
.sql
.get_raw_config(context, "configured_mvbox_folder")
.await
{
if let Some(mvbox_folder) = &context.get_config(Config::ConfiguredMvboxFolder).await {
imap.empty_folder(context, &mvbox_folder).await;
}
}
@@ -627,6 +638,11 @@ impl Job {
}
async fn markseen_msg_on_imap(&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;
}
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
let folder = msg.server_folder.as_ref().unwrap();
@@ -656,21 +672,27 @@ impl Job {
pub async fn kill_action(context: &Context, action: Action) -> bool {
context
.sql
.execute("DELETE FROM jobs WHERE action=?;", paramsv![action])
.execute("DELETE FROM jobs WHERE action=?;", paramsx![action])
.await
.is_ok()
}
/// Remove jobs with specified IDs.
async fn kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> {
use sqlx::Arguments;
let mut args = sqlx::sqlite::SqliteArguments::default();
for job_id in job_ids {
args.add(*job_id as i32);
}
context
.sql
.execute(
format!(
&format!(
"DELETE FROM jobs WHERE id IN({})",
job_ids.iter().map(|_| "?").join(",")
),
job_ids.iter().map(|i| i as &dyn crate::ToSql).collect(),
args,
)
.await?;
Ok(())
@@ -679,7 +701,7 @@ async fn kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> {
pub async fn action_exists(context: &Context, action: Action) -> bool {
context
.sql
.exists("SELECT id FROM jobs WHERE action=?;", paramsv![action])
.exists("SELECT id FROM jobs WHERE action=?;", paramsx![action])
.await
.unwrap_or_default()
}
@@ -688,11 +710,7 @@ async fn set_delivered(context: &Context, msg_id: MsgId) {
message::update_msg_state(context, msg_id, MessageState::OutDelivered).await;
let chat_id: ChatId = context
.sql
.query_get_value(
context,
"SELECT chat_id FROM msgs WHERE id=?",
paramsv![msg_id],
)
.query_value("SELECT chat_id FROM msgs WHERE id=?", paramsx![])
.await
.unwrap_or_default();
context.emit_event(Event::MsgDelivered { chat_id, msg_id });
@@ -798,7 +816,7 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
if rendered_msg.is_encrypted && !needs_encryption {
msg.param.set_int(Param::GuaranteeE2ee, 1);
msg.save_param_to_disk(context).await;
msg.update_param(context).await;
}
ensure!(!recipients.is_empty(), "no recipients for smtp job set");
@@ -815,8 +833,7 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
Ok(Some(job))
}
#[derive(Debug)]
pub enum Connection<'a> {
pub(crate) enum Connection<'a> {
Inbox(&'a mut Imap),
Smtp(&'a mut Smtp),
}
@@ -827,12 +844,12 @@ async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId
context
.sql
.query_row_optional(
"SELECT id FROM msgs \
WHERE timestamp < ? \
AND server_uid != 0",
paramsv![threshold_timestamp],
|row| row.get::<_, MsgId>(0),
.query_value_optional(
r#"
SELECT id FROM msgs
WHERE timestamp < ? AND server_uid != 0
"#,
paramsx![threshold_timestamp],
)
.await
} else {
@@ -968,7 +985,9 @@ async fn perform_job_action(
Action::MarkseenMsgOnImap => job.markseen_msg_on_imap(context, connection.inbox()).await,
Action::MoveMsg => job.move_msg(context, connection.inbox()).await,
Action::Housekeeping => {
sql::housekeeping(context).await;
if let Err(err) = sql::housekeeping(context).await {
error!(context, "housekeeping failed: {}", err);
}
Status::Finished(Ok(()))
}
};
@@ -1029,14 +1048,18 @@ pub async fn add(context: &Context, job: Job) {
| Action::MarkseenMsgOnImap
| Action::MoveMsg => {
info!(context, "interrupt: imap");
context.interrupt_inbox(false).await;
context
.interrupt_inbox(InterruptInfo::new(false, None))
.await;
}
Action::MaybeSendLocations
| Action::MaybeSendLocationsEnded
| Action::SendMdn
| Action::SendMsgToSmtp => {
info!(context, "interrupt: smtp");
context.interrupt_smtp(false).await;
context
.interrupt_smtp(InterruptInfo::new(false, None))
.await;
}
}
}
@@ -1051,58 +1074,52 @@ pub async fn add(context: &Context, job: Job) {
pub(crate) async fn load_next(
context: &Context,
thread: Thread,
probe_network: bool,
info: &InterruptInfo,
) -> Option<Job> {
info!(context, "loading job for {}-thread", thread);
let query = if !probe_network {
let query;
let params: Box<dyn Fn() -> sqlx::sqlite::SqliteArguments<'static> + 'static + Send>;
let t = time();
let thread_i = thread as i64;
if let Some(msg_id) = info.msg_id {
query = r#"
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
FROM jobs
WHERE thread=? AND foreign_id=?
ORDER BY action DESC, added_timestamp
LIMIT 1;
"#;
params = Box::new(move || paramsx![thread_i, msg_id]);
} else if !info.probe_network {
// processing for first-try and after backoff-timeouts:
// process jobs in the order they were added.
r#"
query = r#"
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
FROM jobs
WHERE thread=? AND desired_timestamp<=?
ORDER BY action DESC, added_timestamp
LIMIT 1;
"#
"#;
params = Box::new(move || paramsx![thread_i, t]);
} else {
// processing after call to dc_maybe_network():
// process _all_ pending jobs that failed before
// in the order of their backoff-times.
r#"
query = r#"
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
FROM jobs
WHERE thread=? AND tries>0
ORDER BY desired_timestamp, action DESC
LIMIT 1;
"#
"#;
params = Box::new(move || paramsx![thread_i]);
};
let thread_i = thread as i64;
let t = time();
let params = if !probe_network {
paramsv![thread_i, t]
} else {
paramsv![thread_i]
};
let job = loop {
let job_res = context
.sql
.query_row_optional(query, params.clone(), |row| {
let job = Job {
job_id: row.get("id")?,
action: row.get("action")?,
foreign_id: row.get("foreign_id")?,
desired_timestamp: row.get("desired_timestamp")?,
added_timestamp: row.get("added_timestamp")?,
tries: row.get("tries")?,
param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
pending_error: None,
};
Ok(job)
})
.await;
let job: Option<Job> = loop {
let p = params();
let job_res = context.sql.query_row_optional(query, p).await;
match job_res {
Ok(job) => break job,
@@ -1111,15 +1128,13 @@ LIMIT 1;
info!(context, "cleaning up job, because of {}", err);
// TODO: improve by only doing a single query
match context
.sql
.query_row(query, params.clone(), |row| row.get::<_, i32>(0))
.await
{
let p = params();
let id: Result<i32, _> = context.sql.query_value(query, p).await;
match id {
Ok(id) => {
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
.execute("DELETE FROM jobs WHERE id=?;", paramsx![id])
.await
.ok();
}
@@ -1169,7 +1184,7 @@ mod tests {
"INSERT INTO jobs
(added_timestamp, thread, action, foreign_id, param, desired_timestamp)
VALUES (?, ?, ?, ?, ?, ?);",
paramsv![
paramsx![
now,
Thread::from(Action::MoveMsg),
Action::MoveMsg,
@@ -1187,23 +1202,38 @@ mod tests {
// We want to ensure that loading jobs skips over jobs which
// fails to load from the database instead of failing to load
// all jobs.
let t = dummy_context().await;
let t = TestContext::new().await;
insert_job(&t.ctx, -1).await; // This can not be loaded into Job struct.
let jobs = load_next(&t.ctx, Thread::from(Action::MoveMsg), false).await;
let jobs = load_next(
&t.ctx,
Thread::from(Action::MoveMsg),
&InterruptInfo::new(false, None),
)
.await;
assert!(jobs.is_none());
insert_job(&t.ctx, 1).await;
let jobs = load_next(&t.ctx, Thread::from(Action::MoveMsg), false).await;
let jobs = load_next(
&t.ctx,
Thread::from(Action::MoveMsg),
&InterruptInfo::new(false, None),
)
.await;
assert!(jobs.is_some());
}
#[async_std::test]
async fn test_load_next_job_one() {
let t = dummy_context().await;
let t = TestContext::new().await;
insert_job(&t.ctx, 1).await;
let jobs = load_next(&t.ctx, Thread::from(Action::MoveMsg), false).await;
let jobs = load_next(
&t.ctx,
Thread::from(Action::MoveMsg),
&InterruptInfo::new(false, None),
)
.await;
assert!(jobs.is_some());
}
}

View File

@@ -1,19 +1,20 @@
//! Cryptographic key module
use std::collections::BTreeMap;
use std::fmt;
use std::io::Cursor;
use async_std::path::Path;
use async_trait::async_trait;
use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
use pgp::ser::Serialize;
use pgp::types::{KeyTrait, SecretKeyTrait};
use thiserror::Error;
use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::{dc_write_file, time, EmailAddress, InvalidEmailError};
use crate::dc_tools::{time, EmailAddress, InvalidEmailError};
use crate::sql;
// Re-export key types
@@ -50,8 +51,8 @@ pub type Result<T> = std::result::Result<T, Error>;
/// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world.
#[async_trait]
pub trait DcKey: Serialize + Deserializable {
type KeyType: Serialize + Deserializable;
pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
type KeyType: Serialize + Deserializable + KeyTrait + Clone;
/// Create a key from some bytes.
fn from_slice(bytes: &[u8]) -> Result<Self::KeyType> {
@@ -68,18 +69,45 @@ pub trait DcKey: Serialize + Deserializable {
Self::from_slice(&bytes)
}
/// Create a key from an ASCII-armored string.
///
/// Returns the key and a map of any headers which might have been set in
/// the ASCII-armored representation.
fn from_asc(data: &str) -> Result<(Self::KeyType, BTreeMap<String, String>)> {
let bytes = data.as_bytes();
Self::KeyType::from_armor_single(Cursor::new(bytes)).map_err(Error::Pgp)
}
/// Load the users' default key from the database.
async fn load_self(context: &Context) -> Result<Self::KeyType>;
/// Serialise the key to a base64 string.
fn to_base64(&self) -> String {
/// Serialise the key as bytes.
fn to_bytes(&self) -> Vec<u8> {
// Not using Serialize::to_bytes() to make clear *why* it is
// safe to ignore this error.
// Because we write to a Vec<u8> the io::Write impls never
// fail and we can hide this error.
let mut buf = Vec::new();
self.to_writer(&mut buf).unwrap();
base64::encode(&buf)
buf
}
/// Serialise the key to a base64 string.
fn to_base64(&self) -> String {
base64::encode(&DcKey::to_bytes(self))
}
/// Serialise the key to ASCII-armored representation.
///
/// Each header line must be terminated by `\r\n`. Only allows setting one
/// header as a simplification since that's the only way it's used so far.
// Since .to_armored_string() are actual methods on SignedPublicKey and
// SignedSecretKey we can not generically implement this.
fn to_asc(&self, header: Option<(&str, &str)>) -> String;
/// The fingerprint for the key.
fn fingerprint(&self) -> Fingerprint {
Fingerprint::new(KeyTrait::fingerprint(self)).expect("Invalid fingerprint from rpgp")
}
}
@@ -88,28 +116,43 @@ impl DcKey for SignedPublicKey {
type KeyType = SignedPublicKey;
async fn load_self(context: &Context) -> Result<Self::KeyType> {
match context
let res: std::result::Result<Vec<u8>, _> = context
.sql
.query_row(
.query_value(
r#"
SELECT public_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1;
"#,
paramsv![],
|row| row.get::<_, Vec<u8>>(0),
paramsx![],
)
.await
{
.await;
match res {
Ok(bytes) => Self::from_slice(&bytes),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
Err(sql::Error::Sqlx(sqlx::Error::RowNotFound)) => {
let keypair = generate_keypair(context).await?;
Ok(keypair.public)
}
Err(err) => Err(err.into()),
}
}
fn to_asc(&self, header: Option<(&str, &str)>) -> String {
// Not using .to_armored_string() to make clear *why* it is
// safe to ignore this error.
// Because we write to a Vec<u8> the io::Write impls never
// fail and we can hide this error.
let headers = header.map(|(key, value)| {
let mut m = BTreeMap::new();
m.insert(key.to_string(), value.to_string());
m
});
let mut buf = Vec::new();
self.to_armored_writer(&mut buf, headers.as_ref())
.unwrap_or_default();
std::string::String::from_utf8(buf).unwrap_or_default()
}
}
#[async_trait]
@@ -117,28 +160,60 @@ impl DcKey for SignedSecretKey {
type KeyType = SignedSecretKey;
async fn load_self(context: &Context) -> Result<Self::KeyType> {
match context
let res: std::result::Result<Vec<u8>, _> = context
.sql
.query_row(
.query_value(
r#"
SELECT private_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1;
"#,
paramsv![],
|row| row.get::<_, Vec<u8>>(0),
paramsx![],
)
.await
{
.await;
match res {
Ok(bytes) => Self::from_slice(&bytes),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
Err(sql::Error::Sqlx(sqlx::Error::RowNotFound)) => {
let keypair = generate_keypair(context).await?;
Ok(keypair.secret)
}
Err(err) => Err(err.into()),
}
}
fn to_asc(&self, header: Option<(&str, &str)>) -> String {
// Not using .to_armored_string() to make clear *why* it is
// safe to do these unwraps.
// Because we write to a Vec<u8> the io::Write impls never
// fail and we can hide this error. The string is always ASCII.
let headers = header.map(|(key, value)| {
let mut m = BTreeMap::new();
m.insert(key.to_string(), value.to_string());
m
});
let mut buf = Vec::new();
self.to_armored_writer(&mut buf, headers.as_ref())
.unwrap_or_default();
std::string::String::from_utf8(buf).unwrap_or_default()
}
}
/// Deltachat extension trait for secret keys.
///
/// Provides some convenience wrappers only applicable to [SignedSecretKey].
pub trait DcSecretKey {
/// Create a public key from a private one.
fn split_public_key(&self) -> Result<SignedPublicKey>;
}
impl DcSecretKey for SignedSecretKey {
fn split_public_key(&self) -> Result<SignedPublicKey> {
self.verify()?;
let unsigned_pubkey = SecretKeyTrait::public_key(self);
let signed_pubkey = unsigned_pubkey.sign(self, || "".into())?;
Ok(signed_pubkey)
}
}
async fn generate_keypair(context: &Context) -> Result<KeyPair> {
@@ -150,26 +225,25 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
let _guard = context.generating_key_mutex.lock().await;
// Check if the key appeared while we were waiting on the lock.
match context
let res: std::result::Result<(Vec<u8>, Vec<u8>), _> = context
.sql
.query_row(
r#"
SELECT public_key, private_key
FROM keypairs
WHERE addr=?1
WHERE addr=?
AND is_default=1;
"#,
paramsv![addr],
|row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, Vec<u8>>(1)?)),
paramsx![addr.to_string()],
)
.await
{
.await;
match res {
Ok((pub_bytes, sec_bytes)) => Ok(KeyPair {
addr,
public: SignedPublicKey::from_slice(&pub_bytes)?,
secret: SignedSecretKey::from_slice(&sec_bytes)?,
}),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
Err(sql::Error::Sqlx(sqlx::Error::RowNotFound)) => {
let start = std::time::Instant::now();
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await)
.unwrap_or_default();
@@ -189,193 +263,6 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
}
}
/// Cryptographic key
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Key {
Public(SignedPublicKey),
Secret(SignedSecretKey),
}
impl From<SignedPublicKey> for Key {
fn from(key: SignedPublicKey) -> Self {
Key::Public(key)
}
}
impl From<SignedSecretKey> for Key {
fn from(key: SignedSecretKey) -> Self {
Key::Secret(key)
}
}
impl std::convert::TryFrom<Key> for SignedSecretKey {
type Error = ();
fn try_from(value: Key) -> std::result::Result<Self, Self::Error> {
match value {
Key::Public(_) => Err(()),
Key::Secret(key) => Ok(key),
}
}
}
impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedSecretKey {
type Error = ();
fn try_from(value: &'a Key) -> std::result::Result<Self, Self::Error> {
match value {
Key::Public(_) => Err(()),
Key::Secret(key) => Ok(key),
}
}
}
impl std::convert::TryFrom<Key> for SignedPublicKey {
type Error = ();
fn try_from(value: Key) -> std::result::Result<Self, Self::Error> {
match value {
Key::Public(key) => Ok(key),
Key::Secret(_) => Err(()),
}
}
}
impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedPublicKey {
type Error = ();
fn try_from(value: &'a Key) -> std::result::Result<Self, Self::Error> {
match value {
Key::Public(key) => Ok(key),
Key::Secret(_) => Err(()),
}
}
}
impl Key {
pub fn is_public(&self) -> bool {
match self {
Key::Public(_) => true,
Key::Secret(_) => false,
}
}
pub fn is_secret(&self) -> bool {
!self.is_public()
}
pub fn from_slice(bytes: &[u8], key_type: KeyType) -> Result<Self> {
if bytes.is_empty() {
return Err(Error::Empty);
}
let res = match key_type {
KeyType::Public => SignedPublicKey::from_bytes(Cursor::new(bytes))?.into(),
KeyType::Private => SignedSecretKey::from_bytes(Cursor::new(bytes))?.into(),
};
Ok(res)
}
pub fn from_armored_string(
data: &str,
key_type: KeyType,
) -> Option<(Self, BTreeMap<String, String>)> {
let bytes = data.as_bytes();
let res: std::result::Result<(Key, _), _> = match key_type {
KeyType::Public => SignedPublicKey::from_armor_single(Cursor::new(bytes))
.map(|(k, h)| (Into::into(k), h)),
KeyType::Private => SignedSecretKey::from_armor_single(Cursor::new(bytes))
.map(|(k, h)| (Into::into(k), h)),
};
match res {
Ok(res) => Some(res),
Err(err) => {
eprintln!("Invalid key bytes: {:?}", err);
None
}
}
}
pub fn to_bytes(&self) -> Vec<u8> {
match self {
Key::Public(k) => k.to_bytes().unwrap_or_default(),
Key::Secret(k) => k.to_bytes().unwrap_or_default(),
}
}
pub fn verify(&self) -> bool {
match self {
Key::Public(k) => k.verify().is_ok(),
Key::Secret(k) => k.verify().is_ok(),
}
}
pub fn to_base64(&self) -> String {
let buf = self.to_bytes();
base64::encode(&buf)
}
pub fn to_armored_string(
&self,
headers: Option<&BTreeMap<String, String>>,
) -> pgp::errors::Result<String> {
match self {
Key::Public(k) => k.to_armored_string(headers),
Key::Secret(k) => k.to_armored_string(headers),
}
}
/// Each header line must be terminated by `\r\n`
pub fn to_asc(&self, header: Option<(&str, &str)>) -> String {
let headers = header.map(|(key, value)| {
let mut m = BTreeMap::new();
m.insert(key.to_string(), value.to_string());
m
});
self.to_armored_string(headers.as_ref())
.expect("failed to serialize key")
}
pub async fn write_asc_to_file(
&self,
file: impl AsRef<Path>,
context: &Context,
) -> std::io::Result<()> {
let file_content = self.to_asc(None).into_bytes();
let res = dc_write_file(context, &file, &file_content).await;
if res.is_err() {
error!(context, "Cannot write key to {}", file.as_ref().display());
}
res
}
pub fn fingerprint(&self) -> String {
match self {
Key::Public(k) => hex::encode_upper(k.fingerprint()),
Key::Secret(k) => hex::encode_upper(k.fingerprint()),
}
}
pub fn formatted_fingerprint(&self) -> String {
let rawhex = self.fingerprint();
dc_format_fingerprint(&rawhex)
}
pub fn split_key(&self) -> Option<Key> {
match self {
Key::Public(_) => None,
Key::Secret(k) => {
let pub_key = k.public_key();
pub_key.sign(k, || "".into()).map(Key::Public).ok()
}
}
}
}
/// Use of a [KeyPair] for encryption or decryption.
///
/// This is used by [store_self_keypair] to know what kind of key is
@@ -425,26 +312,20 @@ pub async fn store_self_keypair(
) -> std::result::Result<(), SaveKeyError> {
// Everything should really be one transaction, more refactoring
// is needed for that.
let public_key = keypair
.public
.to_bytes()
.map_err(|err| SaveKeyError::new("failed to serialise public key", err))?;
let secret_key = keypair
.secret
.to_bytes()
.map_err(|err| SaveKeyError::new("failed to serialise secret key", err))?;
let public_key = DcKey::to_bytes(&keypair.public);
let secret_key = DcKey::to_bytes(&keypair.secret);
context
.sql
.execute(
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
paramsv![public_key, secret_key],
paramsx![&public_key, &secret_key],
)
.await
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
if default == KeyPairUse::Default {
context
.sql
.execute("UPDATE keypairs SET is_default=0;", paramsv![])
.execute("UPDATE keypairs SET is_default=0;", paramsx![])
.await
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
}
@@ -456,7 +337,7 @@ pub async fn store_self_keypair(
let addr = keypair.addr.to_string();
let t = time();
let params = paramsv![addr, is_default, public_key, secret_key, t];
let params = paramsx![addr, is_default, public_key, secret_key, t];
context
.sql
.execute(
@@ -470,37 +351,73 @@ pub async fn store_self_keypair(
Ok(())
}
/// Make a fingerprint human-readable, in hex format.
pub fn dc_format_fingerprint(fingerprint: &str) -> String {
// split key into chunks of 4 with space, and 20 newline
let mut res = String::new();
/// A key fingerprint
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Fingerprint(Vec<u8>);
for (i, c) in fingerprint.chars().enumerate() {
if i > 0 && i % 20 == 0 {
res += "\n";
} else if i > 0 && i % 4 == 0 {
res += " ";
impl Fingerprint {
pub fn new(v: Vec<u8>) -> std::result::Result<Fingerprint, FingerprintError> {
match v.len() {
20 => Ok(Fingerprint(v)),
_ => Err(FingerprintError::WrongLength),
}
res += &c.to_string();
}
res
/// Make a hex string from the fingerprint.
///
/// Use [std::fmt::Display] or [ToString::to_string] to get a
/// human-readable formatted string.
pub fn hex(&self) -> String {
hex::encode_upper(&self.0)
}
}
/// Bring a human-readable or otherwise formatted fingerprint back to the 40-characters-uppercase-hex format.
pub fn dc_normalize_fingerprint(fp: &str) -> String {
fp.to_uppercase()
.chars()
.filter(|&c| c >= '0' && c <= '9' || c >= 'A' && c <= 'F')
.collect()
/// Make a human-readable fingerprint.
impl fmt::Display for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Split key into chunks of 4 with space and newline at 20 chars
for (i, c) in self.hex().chars().enumerate() {
if i > 0 && i % 20 == 0 {
writeln!(f)?;
} else if i > 0 && i % 4 == 0 {
write!(f, " ")?;
}
write!(f, "{}", c)?;
}
Ok(())
}
}
/// Parse a human-readable or otherwise formatted fingerprint.
impl std::str::FromStr for Fingerprint {
type Err = FingerprintError;
fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
let hex_repr: String = input
.to_uppercase()
.chars()
.filter(|&c| c >= '0' && c <= '9' || c >= 'A' && c <= 'F')
.collect();
let v: Vec<u8> = hex::decode(hex_repr)?;
let fp = Fingerprint::new(v)?;
Ok(fp)
}
}
#[derive(Debug, Error)]
pub enum FingerprintError {
#[error("Invalid hex characters")]
NotHex(#[from] hex::FromHexError),
#[error("Incorrect fingerprint lengths")]
WrongLength,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use std::convert::TryFrom;
use std::error::Error;
use async_std::sync::Arc;
use lazy_static::lazy_static;
@@ -509,16 +426,9 @@ mod tests {
static ref KEYPAIR: KeyPair = alice_keypair();
}
#[test]
fn test_normalize_fingerprint() {
let fingerprint = dc_normalize_fingerprint(" 1234 567890 \n AbcD abcdef ABCDEF ");
assert_eq!(fingerprint, "1234567890ABCDABCDEFABCDEF");
}
#[test]
fn test_from_armored_string() {
let (private_key, _) = Key::from_armored_string(
let (private_key, _) = SignedSecretKey::from_asc(
"-----BEGIN PGP PRIVATE KEY BLOCK-----
xcLYBF0fgz4BCADnRUV52V4xhSsU56ZaAn3+3oG86MZhXy4X8w14WZZDf0VJGeTh
@@ -576,63 +486,69 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
7yPJeQ==
=KZk/
-----END PGP PRIVATE KEY BLOCK-----",
KeyType::Private,
)
.expect("failed to decode"); // NOTE: if you take out the ===GU1/ part, everything passes!
let binary = private_key.to_bytes();
Key::from_slice(&binary, KeyType::Private).expect("invalid private key");
.expect("failed to decode");
let binary = DcKey::to_bytes(&private_key);
SignedSecretKey::from_slice(&binary).expect("invalid private key");
}
#[test]
fn test_format_fingerprint() {
let fingerprint = dc_format_fingerprint("1234567890ABCDABCDEFABCDEF1234567890ABCD");
fn test_asc_roundtrip() {
let key = KEYPAIR.public.clone();
let asc = key.to_asc(Some(("spam", "ham")));
let (key2, hdrs) = SignedPublicKey::from_asc(&asc).unwrap();
assert_eq!(key, key2);
assert_eq!(hdrs.len(), 1);
assert_eq!(hdrs.get("spam"), Some(&String::from("ham")));
assert_eq!(
fingerprint,
"1234 5678 90AB CDAB CDEF\nABCD EF12 3456 7890 ABCD"
);
let key = KEYPAIR.secret.clone();
let asc = key.to_asc(Some(("spam", "ham")));
let (key2, hdrs) = SignedSecretKey::from_asc(&asc).unwrap();
assert_eq!(key, key2);
assert_eq!(hdrs.len(), 1);
assert_eq!(hdrs.get("spam"), Some(&String::from("ham")));
}
#[test]
fn test_from_slice_roundtrip() {
let public_key = Key::from(KEYPAIR.public.clone());
let private_key = Key::from(KEYPAIR.secret.clone());
let public_key = KEYPAIR.public.clone();
let private_key = KEYPAIR.secret.clone();
let binary = public_key.to_bytes();
let public_key2 = Key::from_slice(&binary, KeyType::Public).expect("invalid public key");
let binary = DcKey::to_bytes(&public_key);
let public_key2 = SignedPublicKey::from_slice(&binary).expect("invalid public key");
assert_eq!(public_key, public_key2);
let binary = private_key.to_bytes();
let private_key2 = Key::from_slice(&binary, KeyType::Private).expect("invalid private key");
let binary = DcKey::to_bytes(&private_key);
let private_key2 = SignedSecretKey::from_slice(&binary).expect("invalid private key");
assert_eq!(private_key, private_key2);
}
#[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 j in 0..(4096 / 40) {
let bad_key = Key::from_slice(
&bad_data[j..j + 4096 / 2 + j],
if 0 != j & 1 {
KeyType::Public
} else {
KeyType::Private
},
);
assert!(bad_key.is_err());
let slice = &bad_data[j..j + 4096 / 2 + j];
assert!(SignedPublicKey::from_slice(slice).is_err());
assert!(SignedSecretKey::from_slice(slice).is_err());
}
}
#[test]
fn test_base64_roundtrip() {
let key = KEYPAIR.public.clone();
let base64 = key.to_base64();
let key2 = SignedPublicKey::from_base64(&base64).unwrap();
assert_eq!(key, key2);
}
#[async_std::test]
async fn test_load_self_existing() {
let alice = alice_keypair();
let t = dummy_context().await;
configure_alice_keypair(&t.ctx).await;
let t = TestContext::new().await;
t.configure_alice().await;
let pubkey = SignedPublicKey::load_self(&t.ctx).await.unwrap();
assert_eq!(alice.public, pubkey);
let seckey = SignedSecretKey::load_self(&t.ctx).await.unwrap();
@@ -640,9 +556,8 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
}
#[async_std::test]
#[ignore] // generating keys is expensive
async fn test_load_self_generate_public() {
let t = dummy_context().await;
let t = TestContext::new().await;
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
@@ -652,9 +567,8 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
}
#[async_std::test]
#[ignore] // generating keys is expensive
async fn test_load_self_generate_secret() {
let t = dummy_context().await;
let t = TestContext::new().await;
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
@@ -664,11 +578,10 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
}
#[async_std::test]
#[ignore] // generating keys is expensive
async fn test_load_self_generate_concurrent() {
use std::thread;
let t = dummy_context().await;
let t = TestContext::new().await;
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
@@ -685,44 +598,27 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
assert_eq!(res0.unwrap(), res1.unwrap());
}
#[test]
fn test_ascii_roundtrip() {
let public_key = Key::from(KEYPAIR.public.clone());
let private_key = Key::from(KEYPAIR.secret.clone());
let s = public_key.to_armored_string(None).unwrap();
let (public_key2, _) =
Key::from_armored_string(&s, KeyType::Public).expect("invalid public key");
assert_eq!(public_key, public_key2);
let s = private_key.to_armored_string(None).unwrap();
println!("{}", &s);
let (private_key2, _) =
Key::from_armored_string(&s, KeyType::Private).expect("invalid private key");
assert_eq!(private_key, private_key2);
}
#[test]
fn test_split_key() {
let private_key = Key::from(KEYPAIR.secret.clone());
let public_wrapped = private_key.split_key().unwrap();
let public = SignedPublicKey::try_from(public_wrapped).unwrap();
assert_eq!(public.primary_key, KEYPAIR.public.primary_key);
let pubkey = KEYPAIR.secret.split_public_key().unwrap();
assert_eq!(pubkey.primary_key, KEYPAIR.public.primary_key);
}
#[async_std::test]
async fn test_save_self_key_twice() {
// Saving the same key twice should result in only one row in
// the keypairs table.
let t = dummy_context().await;
let t = TestContext::new().await;
let ctx = Arc::new(t.ctx);
let ctx1 = ctx.clone();
let nrows = || async {
ctx1.sql
.query_get_value::<u32>(&ctx1, "SELECT COUNT(*) FROM keypairs;", paramsv![])
let val: i32 = ctx1
.sql
.query_value("SELECT COUNT(*) FROM keypairs;", paramsx![])
.await
.unwrap()
.unwrap();
val as usize
};
assert_eq!(nrows().await, 0);
store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default)
@@ -755,4 +651,49 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
// )
// .unwrap();
// }
#[test]
fn test_fingerprint_from_str() {
let res = Fingerprint::new(vec![
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
])
.unwrap();
let fp: Fingerprint = "0102030405060708090A0B0c0d0e0F1011121314".parse().unwrap();
assert_eq!(fp, res);
let fp: Fingerprint = "zzzz 0102 0304 0506\n0708090a0b0c0D0E0F1011121314 yyy"
.parse()
.unwrap();
assert_eq!(fp, res);
let err = "1".parse::<Fingerprint>().err().unwrap();
match err {
FingerprintError::NotHex(_) => (),
_ => panic!("Wrong error"),
}
let src_err = err.source().unwrap().downcast_ref::<hex::FromHexError>();
assert_eq!(src_err, Some(&hex::FromHexError::OddLength));
}
#[test]
fn test_fingerprint_hex() {
let fp = Fingerprint::new(vec![
1, 2, 4, 8, 16, 32, 64, 128, 255, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
])
.unwrap();
assert_eq!(fp.hex(), "0102040810204080FF0A0B0C0D0E0F1011121314");
}
#[test]
fn test_fingerprint_to_string() {
let fp = Fingerprint::new(vec![
1, 2, 4, 8, 16, 32, 64, 128, 255, 1, 2, 4, 8, 16, 32, 64, 128, 255, 19, 20,
])
.unwrap();
assert_eq!(
fp.to_string(),
"0102 0408 1020 4080 FF01\n0204 0810 2040 80FF 1314"
);
}
}

View File

@@ -1,17 +1,47 @@
//! Keyring to perform rpgp operations with.
use anyhow::Result;
use crate::constants::KeyType;
use crate::context::Context;
use crate::key::Key;
use crate::key::{self, DcKey};
#[derive(Default, Clone, Debug)]
pub struct Keyring {
keys: Vec<Key>,
/// An in-memory keyring.
///
/// Instances are usually constructed just for the rpgp operation and
/// short-lived.
#[derive(Clone, Debug, Default)]
pub struct Keyring<T>
where
T: DcKey,
{
keys: Vec<T>,
}
impl Keyring {
pub fn add(&mut self, key: Key) {
self.keys.push(key)
impl<T> Keyring<T>
where
T: DcKey<KeyType = T>,
{
/// New empty keyring.
pub fn new() -> Keyring<T> {
Keyring { keys: Vec::new() }
}
/// Create a new keyring with the the user's secret key loaded.
pub async fn new_self(context: &Context) -> Result<Keyring<T>, key::Error> {
let mut keyring: Keyring<T> = Keyring::new();
keyring.load_self(context).await?;
Ok(keyring)
}
/// Load the user's key into the keyring.
pub async fn load_self(&mut self, context: &Context) -> Result<(), key::Error> {
self.add(T::load_self(context).await?);
Ok(())
}
/// Add a key to the keyring.
pub fn add(&mut self, key: T) {
self.keys.push(key);
}
pub fn len(&self) -> usize {
@@ -22,26 +52,41 @@ impl Keyring {
self.keys.is_empty()
}
pub fn keys(&self) -> &[Key] {
/// A vector with reference to all the keys in the keyring.
pub fn keys(&self) -> &[T] {
&self.keys
}
}
pub async fn load_self_private_for_decrypting(
context: &Context,
self_addr: impl AsRef<str>,
) -> Result<Self> {
let blob: Vec<u8> = context
.sql
.query_get_value_result(
"SELECT private_key FROM keypairs ORDER BY addr=? DESC, is_default DESC;",
paramsv![self_addr.as_ref().to_string()],
)
.await?
.unwrap_or_default();
#[cfg(test)]
mod tests {
use super::*;
use crate::key::{SignedPublicKey, SignedSecretKey};
use crate::test_utils::*;
let key = async_std::task::spawn_blocking(move || Key::from_slice(&blob, KeyType::Private))
.await?;
#[test]
fn test_keyring_add_keys() {
let alice = alice_keypair();
let mut pub_ring: Keyring<SignedPublicKey> = Keyring::new();
pub_ring.add(alice.public.clone());
assert_eq!(pub_ring.keys(), [alice.public]);
Ok(Self { keys: vec![key] })
let mut sec_ring: Keyring<SignedSecretKey> = Keyring::new();
sec_ring.add(alice.secret.clone());
assert_eq!(sec_ring.keys(), [alice.secret]);
}
#[async_std::test]
async fn test_keyring_load_self() {
// new_self() implies load_self()
let t = TestContext::new().await;
t.configure_alice().await;
let alice = alice_keypair();
let pub_ring: Keyring<SignedPublicKey> = Keyring::new_self(&t.ctx).await.unwrap();
assert_eq!(pub_ring.keys(), [alice.public]);
let sec_ring: Keyring<SignedSecretKey> = Keyring::new_self(&t.ctx).await.unwrap();
assert_eq!(sec_ring.keys(), [alice.secret]);
}
}

View File

@@ -6,17 +6,9 @@
extern crate num_derive;
#[macro_use]
extern crate smallvec;
#[macro_use]
extern crate rusqlite;
extern crate strum;
#[macro_use]
extern crate strum_macros;
#[macro_use]
extern crate debug_stub_derive;
pub trait ToSql: rusqlite::ToSql + Send + Sync {}
impl<T: rusqlite::ToSql + Send + Sync> ToSql for T {}
#[macro_use]
pub mod log;

View File

@@ -1,5 +1,6 @@
//! Location handling
use async_std::prelude::*;
use bitflags::bitflags;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
@@ -37,6 +38,34 @@ impl Location {
}
}
impl<'a> sqlx::FromRow<'a, sqlx::sqlite::SqliteRow> for Location {
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
use sqlx::Row;
let msg_id = row.try_get::<i32, _>("msg_id")? as u32;
let txt: String = row.try_get("txt")?;
let marker = if msg_id != 0 && is_marker(&txt) {
Some(txt)
} else {
None
};
let loc = Location {
location_id: row.try_get::<i32, _>("id")? as u32,
latitude: row.try_get("latitude")?,
longitude: row.try_get("longitude")?,
accuracy: row.try_get("accuracy")?,
timestamp: row.try_get("timestamp")?,
independent: row.try_get::<i32, _>("independent")? as u32,
msg_id,
contact_id: row.try_get::<i32, _>("from_id")? as u32,
chat_id: row.try_get("chat_id")?,
marker,
};
Ok(loc)
}
}
#[derive(Debug, Clone, Default)]
pub struct Kml {
pub addr: Option<String>,
@@ -197,14 +226,16 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds:
if context
.sql
.execute(
"UPDATE chats \
SET locations_send_begin=?, \
locations_send_until=? \
WHERE id=?",
paramsv![
r#"
UPDATE chats
SET locations_send_begin=?,
locations_send_until=?
WHERE id=?
"#,
paramsx![
if 0 != seconds { now } else { 0 },
if 0 != seconds { now + seconds } else { 0 },
chat_id,
chat_id
],
)
.await
@@ -260,53 +291,60 @@ pub async fn is_sending_locations_to_chat(context: &Context, chat_id: ChatId) ->
.sql
.exists(
"SELECT id FROM chats WHERE (? OR id=?) AND locations_send_until>?;",
paramsv![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()],
paramsx![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()],
)
.await
.unwrap_or_default()
}
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
pub async fn set(
context: &Context,
latitude: f64,
longitude: f64,
accuracy: f64,
) -> Result<bool, Error> {
if latitude == 0.0 && longitude == 0.0 {
return true;
return Ok(true);
}
let mut continue_streaming = false;
let pool = context.sql.get_pool().await?;
let mut rows = sqlx::query_as("SELECT id FROM chats WHERE locations_send_until>?;")
.bind(time())
.fetch(&pool);
if let Ok(chats) = context
.sql
.query_map(
"SELECT id FROM chats WHERE locations_send_until>?;",
paramsv![time()],
|row| row.get::<_, i32>(0),
|chats| chats.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await
{
for chat_id in chats {
if let Err(err) = context.sql.execute(
"INSERT INTO locations \
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
paramsv![
latitude,
longitude,
accuracy,
time(),
chat_id,
DC_CONTACT_ID_SELF,
]
).await {
warn!(context, "failed to store location {:?}", err);
} else {
continue_streaming = true;
}
while let Some(row) = rows.next().await {
let (chat_id,): (i64,) = row?;
if let Err(err) = context
.sql
.execute(
"INSERT INTO locations \
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
paramsx![
latitude,
longitude,
accuracy,
time(),
chat_id,
DC_CONTACT_ID_SELF as i32
],
)
.await
{
warn!(context, "failed to store location {:?}", err);
} else {
continue_streaming = true;
}
if continue_streaming {
context.emit_event(Event::LocationChanged(Some(DC_CONTACT_ID_SELF)));
};
schedule_maybe_send_locations(context, false).await;
}
continue_streaming
if continue_streaming {
context.emit_event(Event::LocationChanged(Some(DC_CONTACT_ID_SELF)));
};
schedule_maybe_send_locations(context, false).await;
Ok(continue_streaming)
}
pub async fn get_range(
@@ -319,16 +357,21 @@ pub async fn get_range(
if timestamp_to == 0 {
timestamp_to = time() + 10;
}
context
.sql
.query_map(
"SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \
COALESCE(m.id, 0) AS msg_id, l.from_id, l.chat_id, COALESCE(m.txt, '') AS txt \
FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \
AND (? OR l.from_id=?) \
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
paramsv![
.query_rows(
r#"
SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent,
COALESCE(m.id, 0) AS msg_id, l.from_id, l.chat_id, COALESCE(m.txt, '') AS txt
FROM locations l
LEFT JOIN msgs m ON l.id=m.location_id
WHERE (? OR l.chat_id=?)
AND (? OR l.from_id=?)
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?))
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;
"#,
paramsx![
if chat_id.is_unset() { 1 } else { 0 },
chat_id,
if contact_id == 0 { 1 } else { 0 },
@@ -336,36 +379,6 @@ pub async fn get_range(
timestamp_from,
timestamp_to,
],
|row| {
let msg_id = row.get(6)?;
let txt: String = row.get(9)?;
let marker = if msg_id != 0 && is_marker(&txt) {
Some(txt)
} else {
None
};
let loc = Location {
location_id: row.get(0)?,
latitude: row.get(1)?,
longitude: row.get(2)?,
accuracy: row.get(3)?,
timestamp: row.get(4)?,
independent: row.get(5)?,
msg_id,
contact_id: row.get(7)?,
chat_id: row.get(8)?,
marker,
};
Ok(loc)
},
|locations| {
let mut ret = Vec::new();
for location in locations {
ret.push(location?);
}
Ok(ret)
},
)
.await
.unwrap_or_default()
@@ -379,7 +392,7 @@ fn is_marker(txt: &str) -> bool {
pub async fn delete_all(context: &Context) -> Result<(), Error> {
context
.sql
.execute("DELETE FROM locations;", paramsv![])
.execute("DELETE FROM locations;", paramsx![])
.await?;
context.emit_event(Event::LocationChanged(None));
Ok(())
@@ -393,16 +406,10 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
.await
.unwrap_or_default();
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
let (locations_send_begin, locations_send_until, locations_last_sent): (i64, i64, i64) = context.sql.query_row(
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
paramsv![chat_id], |row| {
let send_begin: i64 = row.get(0)?;
let send_until: i64 = row.get(1)?;
let last_sent: i64 = row.get(2)?;
Ok((send_begin, send_until, last_sent))
})
.await?;
paramsx![chat_id]
).await?;
let now = time();
let mut location_count = 0;
@@ -413,40 +420,41 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
self_addr,
);
context.sql.query_map(
"SELECT id, latitude, longitude, accuracy, timestamp \
FROM locations WHERE from_id=? \
AND timestamp>=? \
AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \
AND independent=0 \
GROUP BY timestamp \
ORDER BY timestamp;",
paramsv![DC_CONTACT_ID_SELF, locations_send_begin, locations_last_sent, DC_CONTACT_ID_SELF],
|row| {
let location_id: i32 = row.get(0)?;
let latitude: f64 = row.get(1)?;
let longitude: f64 = row.get(2)?;
let accuracy: f64 = row.get(3)?;
let timestamp = get_kml_timestamp(row.get(4)?);
let pool = context.sql.get_pool().await?;
let mut rows = sqlx::query_as(
r#"
SELECT id, latitude, longitude, accuracy, timestamp
FROM locations
WHERE from_id=?
AND timestamp>=?
AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?))
AND independent=0
GROUP BY timestamp
ORDER BY timestamp;
"#,
)
.bind(DC_CONTACT_ID_SELF as i32)
.bind(locations_send_begin)
.bind(locations_last_sent)
.bind(DC_CONTACT_ID_SELF as i32)
.fetch(&pool);
while let Some(row) = rows.next().await {
let (location_id, latitude, longitude, accuracy, timestamp): (i32, f64, f64, f64, i64) =
row?;
let timestamp = get_kml_timestamp(timestamp);
ret += &format!(
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n",
timestamp,
accuracy,
longitude,
latitude
);
location_count += 1;
last_added_location_id = location_id as u32;
}
Ok((location_id, latitude, longitude, accuracy, timestamp))
},
|rows| {
for row in rows {
let (location_id, latitude, longitude, accuracy, timestamp) = row?;
ret += &format!(
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n",
timestamp,
accuracy,
longitude,
latitude
);
location_count += 1;
last_added_location_id = location_id as u32;
}
Ok(())
}
).await?;
ret += "</Document>\n</kml>";
}
@@ -488,7 +496,7 @@ pub async fn set_kml_sent_timestamp(
.sql
.execute(
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
paramsv![timestamp, chat_id],
paramsx![timestamp, chat_id],
)
.await?;
Ok(())
@@ -503,7 +511,7 @@ pub async fn set_msg_location_id(
.sql
.execute(
"UPDATE msgs SET location_id=? WHERE id=?;",
paramsv![location_id, msg_id],
paramsx![location_id as i32, msg_id],
)
.await?;
@@ -530,48 +538,51 @@ pub async fn save(
accuracy,
..
} = location;
context
let exists: Option<i32> = context
.sql
.with_conn(move |mut conn| {
let mut stmt_test = conn
.prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
let mut stmt_insert = conn.prepare_cached(
"INSERT INTO locations\
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
VALUES (?,?,?,?,?,?,?);",
)?;
.query_value_optional(
"SELECT id FROM locations WHERE timestamp=? AND from_id=?",
paramsx![timestamp, contact_id as i32],
)
.await?;
let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?;
if independent || !exists {
stmt_insert.execute(paramsv![
if independent || exists.is_none() {
context
.sql
.execute(
r#"
INSERT INTO locations
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent)
VALUES (?,?,?,?,?,?,?);
"#,
paramsx![
timestamp,
contact_id as i32,
chat_id,
latitude,
longitude,
accuracy,
independent,
])?;
independent
],
)
.await?;
if timestamp > newest_timestamp {
// okay to drop, as we use cached prepared statements
drop(stmt_test);
drop(stmt_insert);
newest_timestamp = timestamp;
newest_location_id = crate::sql::get_rowid2(
&mut conn,
"locations",
"timestamp",
timestamp,
"from_id",
contact_id as i32,
)?;
}
}
Ok(())
})
.await?;
if timestamp > newest_timestamp {
// okay to drop, as we use cached prepared statements
newest_timestamp = timestamp;
newest_location_id = context
.sql
.get_rowid2(
"locations",
"timestamp",
timestamp,
"from_id",
contact_id as i32,
)
.await?;
}
}
}
Ok(newest_location_id)
@@ -585,85 +596,75 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
" ----------------- MAYBE_SEND_LOCATIONS -------------- ",
);
let rows = context
.sql
.query_map(
"SELECT id, locations_send_begin, locations_last_sent \
FROM chats \
WHERE locations_send_until>?;",
paramsv![now],
|row| {
let chat_id: ChatId = row.get(0)?;
let locations_send_begin: i64 = row.get(1)?;
let locations_last_sent: i64 = row.get(2)?;
continue_streaming = true;
let pool = match context.sql.get_pool().await {
Ok(pool) => pool,
Err(err) => {
return job::Status::Finished(Err(err.into()));
}
};
// be a bit tolerant as the timer may not align exactly with time(NULL)
if now - locations_last_sent < (60 - 3) {
Ok(None)
} else {
Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
}
},
|rows| {
rows.filter_map(|v| v.transpose())
.collect::<Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await;
let mut rows = sqlx::query_as(
r#"
SELECT id, locations_send_begin, locations_last_sent
FROM chats
WHERE locations_send_until>?;
"#,
)
.bind(now)
.fetch(&pool);
if rows.is_ok() {
let msgs = context
while let Some(row) = rows.next().await {
let (chat_id, locations_send_begin, locations_last_sent): (ChatId, i64, i64) = match row {
Ok(res) => res,
Err(err) => {
warn!(context, "invalid row: {}", err);
continue;
}
};
continue_streaming = true;
// be a bit tolerant as the timer may not align exactly with time(NULL)
if now - locations_last_sent < (60 - 3) {
continue;
}
let exists = context
.sql
.with_conn(move |conn| {
let rows = rows.unwrap();
let mut stmt_locations = conn.prepare_cached(
"SELECT id \
FROM locations \
WHERE from_id=? \
AND timestamp>=? \
AND timestamp>? \
AND independent=0 \
ORDER BY timestamp;",
)?;
let mut msgs = Vec::new();
for (chat_id, locations_send_begin, locations_last_sent) in &rows {
if !stmt_locations
.exists(paramsv![
DC_CONTACT_ID_SELF,
*locations_send_begin,
*locations_last_sent,
])
.unwrap_or_default()
{
// if there is no new location, there's nothing to send.
// however, maybe we want to bypass this test eg. 15 minutes
} else {
// pending locations are attached automatically to every message,
// so also to this empty text message.
// DC_CMD_LOCATION is only needed to create a nicer subject.
//
// for optimisation and to avoid flooding the sending queue,
// we could sending these messages only if we're really online.
// the easiest way to determine this, is to check for an empty message queue.
// (might not be 100%, however, as positions are sent combined later
// and dc_set_location() is typically called periodically, this is ok)
let mut msg = Message::new(Viewtype::Text);
msg.hidden = true;
msg.param.set_cmd(SystemMessage::LocationOnly);
msgs.push((*chat_id, msg));
}
}
Ok(msgs)
})
.exists(
r#"
SELECT id
FROM locations
WHERE from_id=?
AND timestamp>=?
AND timestamp>?
AND independent=0
ORDER BY timestamp;",
"#,
paramsx![
DC_CONTACT_ID_SELF as i32,
locations_send_begin,
locations_last_sent,
],
)
.await
.unwrap_or_default();
for (chat_id, mut msg) in msgs.into_iter() {
if !exists {
// if there is no new location, there's nothing to send.
// however, maybe we want to bypass this test eg. 15 minutes
} else {
// pending locations are attached automatically to every message,
// so also to this empty text message.
// DC_CMD_LOCATION is only needed to create a nicer subject.
//
// for optimisation and to avoid flooding the sending queue,
// we could sending these messages only if we're really online.
// the easiest way to determine this, is to check for an empty message queue.
// (might not be 100%, however, as positions are sent combined later
// and dc_set_location() is typically called periodically, this is ok)
let mut msg = Message::new(Viewtype::Text);
msg.hidden = true;
msg.param.set_cmd(SystemMessage::LocationOnly);
// TODO: better error handling
chat::send_msg(context, chat_id, &mut msg)
.await
@@ -674,6 +675,7 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
if continue_streaming {
schedule_maybe_send_locations(context, true).await;
}
job::Status::Finished(Ok(()))
}
@@ -687,13 +689,12 @@ pub(crate) async fn job_maybe_send_locations_ended(
let chat_id = ChatId::new(job.foreign_id);
let (send_begin, send_until) = job_try!(
let (send_begin, send_until): (i64, i64) = job_try!(
context
.sql
.query_row(
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
paramsv![chat_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
paramsx![chat_id],
)
.await
);
@@ -705,8 +706,8 @@ pub(crate) async fn job_maybe_send_locations_ended(
if !(send_begin == 0 && send_until == 0) {
// not streaming, device-message already sent
job_try!(context.sql.execute(
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
paramsv![chat_id],
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
paramsx![chat_id],
).await);
let stock_str = context
@@ -722,11 +723,11 @@ pub(crate) async fn job_maybe_send_locations_ended(
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::dummy_context;
use crate::test_utils::TestContext;
#[async_std::test]
async fn test_kml_parse() {
let context = dummy_context().await;
let context = TestContext::new().await;
let xml =
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"user@example.org\">\n<Placemark><Timestamp><when>2019-03-06T21:09:57Z</when></Timestamp><Point><coordinates accuracy=\"32.000000\">9.423110,53.790302</coordinates></Point></Placemark>\n<PlaceMARK>\n<Timestamp><WHEN > \n\t2018-12-13T22:11:12Z\t</WHEN></Timestamp><Point><coordinates aCCuracy=\"2.500000\"> 19.423110 \t , \n 63.790302\n </coordinates></Point></PlaceMARK>\n</Document>\n</kml>";

View File

@@ -56,63 +56,54 @@ impl LoginParam {
let key = format!("{}addr", prefix);
let addr = sql
.get_raw_config(context, key)
.get_raw_config(key)
.await
.unwrap_or_default()
.trim()
.to_string();
let key = format!("{}mail_server", prefix);
let mail_server = sql.get_raw_config(context, key).await.unwrap_or_default();
let mail_server = sql.get_raw_config(key).await.unwrap_or_default();
let key = format!("{}mail_port", prefix);
let mail_port = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let mail_port = sql.get_raw_config_int(key).await.unwrap_or_default();
let key = format!("{}mail_user", prefix);
let mail_user = sql.get_raw_config(context, key).await.unwrap_or_default();
let mail_user = sql.get_raw_config(key).await.unwrap_or_default();
let key = format!("{}mail_pw", prefix);
let mail_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
let mail_pw = sql.get_raw_config(key).await.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 {
if let Some(certificate_checks) = sql.get_raw_config_int(key).await {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
} else {
Default::default()
};
let key = format!("{}send_server", prefix);
let send_server = sql.get_raw_config(context, key).await.unwrap_or_default();
let send_server = sql.get_raw_config(key).await.unwrap_or_default();
let key = format!("{}send_port", prefix);
let send_port = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let send_port = sql.get_raw_config_int(key).await.unwrap_or_default();
let key = format!("{}send_user", prefix);
let send_user = sql.get_raw_config(context, key).await.unwrap_or_default();
let send_user = sql.get_raw_config(key).await.unwrap_or_default();
let key = format!("{}send_pw", prefix);
let send_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
let send_pw = sql.get_raw_config(key).await.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 {
if let Some(certificate_checks) = sql.get_raw_config_int(key).await {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
} else {
Default::default()
};
let key = format!("{}server_flags", prefix);
let server_flags = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let server_flags = sql.get_raw_config_int(key).await.unwrap_or_default();
LoginParam {
addr,
@@ -130,10 +121,6 @@ impl LoginParam {
}
}
pub fn addr_str(&self) -> &str {
self.addr.as_str()
}
/// Save this loginparam to the database.
pub async fn save_to_database(
&self,
@@ -277,21 +264,15 @@ fn get_readable_flags(flags: i32) -> String {
res
}
pub fn dc_build_tls(certificate_checks: CertificateChecks) -> async_native_tls::TlsConnector {
pub fn dc_build_tls(strict_tls: bool) -> async_native_tls::TlsConnector {
let tls_builder = async_native_tls::TlsConnector::new();
match certificate_checks {
CertificateChecks::Automatic => {
// Same as AcceptInvalidCertificates for now.
// TODO: use provider database when it becomes available
tls_builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true)
}
CertificateChecks::Strict => tls_builder,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => tls_builder
if strict_tls {
tls_builder
} else {
tls_builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true),
.danger_accept_invalid_certs(true)
}
}

View File

@@ -1,4 +1,6 @@
use deltachat_derive::{FromSql, ToSql};
use deltachat_derive::*;
use crate::key::Fingerprint;
/// An object containing a set of values.
/// The meaning of the values is defined by the function returning the object.
@@ -14,13 +16,13 @@ pub struct Lot {
pub(crate) timestamp: i64,
pub(crate) state: LotState,
pub(crate) id: u32,
pub(crate) fingerprint: Option<String>,
pub(crate) fingerprint: Option<Fingerprint>,
pub(crate) invitenumber: Option<String>,
pub(crate) auth: Option<String>,
}
#[repr(u8)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
pub enum Meaning {
None = 0,
Text1Draft = 1,
@@ -65,7 +67,7 @@ impl Lot {
}
#[repr(i32)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
pub enum LotState {
// Default
Undefined = 0,

View File

@@ -1,7 +1,8 @@
//! # Messages and their identifiers
use async_std::path::{Path, PathBuf};
use deltachat_derive::{FromSql, ToSql};
use async_std::prelude::*;
use deltachat_derive::*;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
@@ -14,7 +15,7 @@ use crate::error::{ensure, Error};
use crate::events::Event;
use crate::job::{self, Action};
use crate::lot::{Lot, LotState, Meaning};
use crate::mimeparser::SystemMessage;
use crate::mimeparser::{FailureReport, SystemMessage};
use crate::param::*;
use crate::pgp::*;
use crate::stock::StockMessage;
@@ -33,7 +34,20 @@ const SUMMARY_CHARACTERS: usize = 160;
/// This type can represent both the special as well as normal
/// messages.
#[derive(
Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
Debug,
Copy,
Clone,
Default,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
Serialize,
Deserialize,
ToPrimitive,
FromPrimitive,
Sqlx,
)]
pub struct MsgId(u32);
@@ -92,7 +106,7 @@ impl MsgId {
.sql
.execute(
"UPDATE msgs SET chat_id=?, txt='', txt_raw='' WHERE id=?",
paramsv![chat_id, self],
paramsx![chat_id, self],
)
.await?;
@@ -105,11 +119,11 @@ impl MsgId {
// sure they are not left while the message is deleted.
context
.sql
.execute("DELETE FROM msgs_mdns WHERE msg_id=?;", paramsv![self])
.execute("DELETE FROM msgs_mdns WHERE msg_id=?;", paramsx![self])
.await?;
context
.sql
.execute("DELETE FROM msgs WHERE id=?;", paramsv![self])
.execute("DELETE FROM msgs WHERE id=?;", paramsx![self])
.await?;
Ok(())
}
@@ -123,10 +137,12 @@ impl MsgId {
context
.sql
.execute(
"UPDATE msgs \
SET server_folder='', server_uid=0 \
WHERE id=?",
paramsv![self],
r#"
UPDATE msgs
SET server_folder='', server_uid=0
WHERE id=?
"#,
paramsx![self],
)
.await?;
Ok(())
@@ -156,41 +172,6 @@ impl std::fmt::Display for MsgId {
}
}
/// Allow converting [MsgId] to an SQLite type.
///
/// This allows you to directly store [MsgId] into the database.
///
/// # Errors
///
/// This **does** ensure that no special message IDs are written into
/// the database and the conversion will fail if this is not the case.
impl rusqlite::types::ToSql for MsgId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
if self.0 <= DC_MSG_ID_LAST_SPECIAL {
return Err(rusqlite::Error::ToSqlConversionFailure(Box::new(
InvalidMsgId,
)));
}
let val = rusqlite::types::Value::Integer(self.0 as i64);
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
/// Allow converting an SQLite integer directly into [MsgId].
impl rusqlite::types::FromSql for MsgId {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
// Would be nice if we could use match here, but alas.
i64::column_result(value).and_then(|val| {
if 0 <= val && val <= std::u32::MAX as i64 {
Ok(MsgId::new(val as u32))
} else {
Err(rusqlite::types::FromSqlError::OutOfRange(val))
}
})
}
}
/// Message ID was invalid.
///
/// This usually occurs when trying to use a message ID of
@@ -201,16 +182,7 @@ impl rusqlite::types::FromSql for MsgId {
pub struct InvalidMsgId;
#[derive(
Debug,
Copy,
Clone,
PartialEq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
Debug, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive, Serialize, Deserialize, Sqlx,
)]
#[repr(u8)]
pub(crate) enum MessengerMessage {
@@ -255,9 +227,62 @@ pub struct Message {
pub(crate) starred: bool,
pub(crate) chat_blocked: Blocked,
pub(crate) location_id: u32,
pub(crate) error: String,
pub(crate) param: Params,
}
impl<'a> sqlx::FromRow<'a, sqlx::sqlite::SqliteRow> for Message {
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
use sqlx::Row;
let id = row.try_get("id")?;
let text;
if let Some(buf) = row.try_get_unchecked::<Option<&[u8]>, _>("txt")? {
if let Ok(t) = String::from_utf8(buf.to_vec()) {
text = t;
} else {
eprintln!(
"dc_msg_load_from_db: could not get text column as non-lossy utf8 id {}",
id
);
text = String::from_utf8_lossy(buf).into_owned();
}
} else {
text = "".to_string();
}
Ok(Message {
id,
rfc724_mid: row.try_get::<String, _>("rfc724mid")?,
in_reply_to: row.try_get::<Option<String>, _>("mime_in_reply_to")?,
server_folder: row.try_get::<Option<String>, _>("server_folder")?,
server_uid: row.try_get_unchecked::<i64, _>("server_uid")? as u32,
chat_id: row.try_get("chat_id")?,
from_id: row.try_get::<i32, _>("from_id")? as u32,
to_id: row.try_get::<i32, _>("to_id")? as u32,
timestamp_sort: row.try_get_unchecked("timestamp")?,
timestamp_sent: row.try_get_unchecked("timestamp_sent")?,
timestamp_rcvd: row.try_get_unchecked("timestamp_rcvd")?,
viewtype: row.try_get("type")?,
state: row.try_get("state")?,
is_dc_message: row.try_get("msgrmsg")?,
text: Some(text),
param: row
.try_get::<String, _>("param")?
.parse()
.unwrap_or_default(),
starred: row.try_get::<i32, _>("starred")? > 0,
hidden: row.try_get::<i32, _>("hidden")? > 0,
location_id: row.try_get_unchecked::<i64, _>("location")? as u32,
chat_blocked: row
.try_get::<Option<Blocked>, _>("blocked")?
.unwrap_or_default(),
error: row.try_get("error")?,
})
}
}
impl Message {
pub fn new(viewtype: Viewtype) -> Self {
let mut msg = Message::default();
@@ -271,7 +296,7 @@ impl Message {
!id.is_special(),
"Can not load special message IDs from DB."
);
let msg = context
let msg: Message = context
.sql
.query_row(
concat!(
@@ -289,6 +314,7 @@ impl Message {
" m.timestamp_rcvd AS timestamp_rcvd,",
" m.type AS type,",
" m.state AS state,",
" m.error AS error,",
" m.msgrmsg AS msgrmsg,",
" m.txt AS txt,",
" m.param AS param,",
@@ -299,55 +325,7 @@ impl Message {
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
" WHERE m.id=?;"
),
paramsv![id],
|row| {
let mut msg = Message::default();
// msg.id = row.get::<_, AnyMsgId>("id")?;
msg.id = row.get("id")?;
msg.rfc724_mid = row.get::<_, String>("rfc724mid")?;
msg.in_reply_to = row.get::<_, Option<String>>("mime_in_reply_to")?;
msg.server_folder = row.get::<_, Option<String>>("server_folder")?;
msg.server_uid = row.get("server_uid")?;
msg.chat_id = row.get("chat_id")?;
msg.from_id = row.get("from_id")?;
msg.to_id = row.get("to_id")?;
msg.timestamp_sort = row.get("timestamp")?;
msg.timestamp_sent = row.get("timestamp_sent")?;
msg.timestamp_rcvd = row.get("timestamp_rcvd")?;
msg.viewtype = row.get("type")?;
msg.state = row.get("state")?;
msg.is_dc_message = row.get("msgrmsg")?;
let text;
if let rusqlite::types::ValueRef::Text(buf) = row.get_raw("txt") {
if let Ok(t) = String::from_utf8(buf.to_vec()) {
text = t;
} else {
warn!(
context,
concat!(
"dc_msg_load_from_db: could not get ",
"text column as non-lossy utf8 id {}"
),
id
);
text = String::from_utf8_lossy(buf).into_owned();
}
} else {
text = "".to_string();
}
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
.get::<_, Option<Blocked>>("blocked")?
.unwrap_or_default();
Ok(msg)
},
paramsx![id],
)
.await?;
@@ -390,7 +368,7 @@ impl Message {
}
if !self.id.is_unset() {
self.save_param_to_disk(context).await;
self.update_param(context).await;
}
}
}
@@ -643,15 +621,15 @@ impl Message {
if duration > 0 {
self.param.set_int(Param::Duration, duration);
}
self.save_param_to_disk(context).await;
self.update_param(context).await;
}
pub async fn save_param_to_disk(&mut self, context: &Context) -> bool {
pub async fn update_param(&mut self, context: &Context) -> bool {
context
.sql
.execute(
"UPDATE msgs SET param=? WHERE id=?;",
paramsv![self.param.to_string(), self.id],
paramsx![self.param.to_string(), self.id],
)
.await
.is_ok()
@@ -666,10 +644,9 @@ impl Message {
Eq,
FromPrimitive,
ToPrimitive,
ToSql,
FromSql,
Serialize,
Deserialize,
sqlx::Type,
)]
#[repr(i32)]
pub enum MessageState {
@@ -763,9 +740,10 @@ impl From<MessageState> for LotState {
impl MessageState {
pub fn can_fail(self) -> bool {
match self {
MessageState::OutPreparing | MessageState::OutPending | MessageState::OutDelivered => {
true
}
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,
}
}
@@ -840,29 +818,16 @@ impl Lot {
}
}
pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String, Error> {
let mut ret = String::new();
let msg = Message::load_from_db(context, msg_id).await;
if msg.is_err() {
return ret;
}
let msg = msg.unwrap_or_default();
let msg = Message::load_from_db(context, msg_id).await?;
let rawtxt: Option<String> = context
.sql
.query_get_value(
context,
"SELECT txt_raw FROM msgs WHERE id=?;",
paramsv![msg_id],
)
.await;
.query_value("SELECT txt_raw FROM msgs WHERE id=?;", paramsx![msg_id])
.await?;
if rawtxt.is_none() {
ret += &format!("Cannot load message {}.", msg_id);
return ret;
}
let rawtxt = rawtxt.unwrap_or_default();
let rawtxt = dc_truncate(rawtxt.trim(), 100_000);
@@ -871,8 +836,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
let name = Contact::load_from_db(context, msg.from_id)
.await
.map(|contact| contact.get_name_n_addr())
.unwrap_or_default();
.map(|contact| contact.get_name_n_addr())?;
ret += &format!(" by {}", name);
ret += "\n";
@@ -889,35 +853,28 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
if msg.from_id == DC_CONTACT_ID_INFO || msg.to_id == DC_CONTACT_ID_INFO {
// device-internal message, no further details needed
return ret;
return Ok(ret);
}
if let Ok(rows) = context
.sql
.query_map(
"SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;",
paramsv![msg_id],
|row| {
let contact_id: i32 = row.get(0)?;
let ts: i64 = row.get(1)?;
Ok((contact_id, ts))
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await
{
for (contact_id, ts) in rows {
let fts = dc_timestamp_to_str(ts);
ret += &format!("Read: {}", fts);
let pool = context.sql.get_pool().await?;
let name = Contact::load_from_db(context, contact_id as u32)
.await
.map(|contact| contact.get_name_n_addr())
.unwrap_or_default();
let mut rows =
sqlx::query_as("SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;")
.bind(msg_id)
.fetch(&pool);
ret += &format!(" by {}", name);
ret += "\n";
}
while let Some(row) = rows.next().await {
let (contact_id, ts): (i32, i32) = row?;
let fts = dc_timestamp_to_str(ts as i64);
ret += &format!("Read: {}", fts);
let name = Contact::load_from_db(context, contact_id as u32)
.await
.map(|contact| contact.get_name_n_addr())?;
ret += &format!(" by {}", name);
ret += "\n";
}
ret += &format!("State: {}", msg.state);
@@ -937,8 +894,9 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
}
ret += "\n";
if let Some(err) = msg.param.get(Param::Error) {
ret += &format!("Error: {}", err)
if !msg.error.is_empty() {
ret += &format!("Error: {}", msg.error);
}
if let Some(path) = msg.get_file(context) {
@@ -973,7 +931,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
}
}
ret
Ok(ret)
}
pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
@@ -1001,12 +959,12 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Option<String> {
context
.sql
.query_get_value(
context,
.query_value(
"SELECT mime_headers FROM msgs WHERE id=?;",
paramsv![msg_id],
paramsx![msg_id],
)
.await
.ok()
}
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
@@ -1045,7 +1003,7 @@ async fn delete_poi_location(context: &Context, location_id: u32) -> bool {
.sql
.execute(
"DELETE FROM locations WHERE independent = 1 AND id=?;",
paramsv![location_id as i32],
paramsx![location_id as i32],
)
.await
.is_ok()
@@ -1056,56 +1014,40 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> bool {
return false;
}
let msgs = context
.sql
.with_conn(move |conn| {
let mut stmt = conn.prepare_cached(concat!(
"SELECT",
" m.state AS state,",
" c.blocked AS blocked",
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
" WHERE m.id=? AND m.chat_id>9"
))?;
let mut msgs = Vec::with_capacity(msg_ids.len());
for id in msg_ids.into_iter() {
let query_res = stmt.query_row(paramsv![id], |row| {
Ok((
row.get::<_, MessageState>("state")?,
row.get::<_, Option<Blocked>>("blocked")?
.unwrap_or_default(),
))
});
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));
}
Ok(msgs)
})
.await
.unwrap_or_default();
let mut send_event = false;
for id in msg_ids.into_iter() {
let query_res: Result<Option<(MessageState, Option<Blocked>)>, _> = context
.sql
.query_row_optional(
r#"
SELECT
m.state
c.blocked
FROM msgs m LEFT JOIN chats c ON c.id = m.chat_id
WHERE m.id = ? AND m.chat_id > 9
"#,
paramsx![id],
)
.await;
for (id, curr_state, curr_blocked) in msgs.into_iter() {
if curr_blocked == Blocked::Not {
if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
update_msg_state(context, id, MessageState::InSeen).await;
info!(context, "Seen message {}.", id);
if let Ok(Some((state, blocked))) = query_res {
let blocked = blocked.unwrap_or_default();
if blocked == Blocked::Not {
if state == MessageState::InFresh || state == MessageState::InNoticed {
update_msg_state(context, id, MessageState::InSeen).await;
info!(context, "Seen message {}.", id);
job::add(
context,
job::Job::new(Action::MarkseenMsgOnImap, id.to_u32(), Params::new(), 0),
)
.await;
job::add(
context,
job::Job::new(Action::MarkseenMsgOnImap, id.to_u32(), Params::new(), 0),
)
.await;
send_event = true;
}
} else if state == MessageState::InFresh {
update_msg_state(context, id, MessageState::InNoticed).await;
send_event = true;
}
} else if curr_state == MessageState::InFresh {
update_msg_state(context, id, MessageState::InNoticed).await;
send_event = true;
}
}
@@ -1124,7 +1066,7 @@ pub async fn update_msg_state(context: &Context, msg_id: MsgId, state: MessageSt
.sql
.execute(
"UPDATE msgs SET state=? WHERE id=?;",
paramsv![state, msg_id],
paramsx![state, msg_id],
)
.await
.is_ok()
@@ -1134,17 +1076,22 @@ pub async fn star_msgs(context: &Context, msg_ids: Vec<MsgId>, star: bool) -> bo
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()
for msg_id in msg_ids.into_iter() {
if context
.sql
.execute(
"UPDATE msgs SET starred=? WHERE id=?;",
paramsx![star as i32, msg_id],
)
.await
.is_err()
{
return false;
}
}
true
}
/// Returns a summary test.
@@ -1235,12 +1182,9 @@ pub async fn exists(context: &Context, msg_id: MsgId) -> bool {
let chat_id: Option<ChatId> = context
.sql
.query_get_value(
context,
"SELECT chat_id FROM msgs WHERE id=?;",
paramsv![msg_id],
)
.await;
.query_value("SELECT chat_id FROM msgs WHERE id=?;", paramsx![msg_id])
.await
.ok();
if let Some(chat_id) = chat_id {
!chat_id.is_trash()
@@ -1251,33 +1195,38 @@ pub async fn exists(context: &Context, msg_id: MsgId) -> bool {
pub async fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option<impl AsRef<str>>) {
if let Ok(mut msg) = Message::load_from_db(context, msg_id).await {
let error = error.map(|e| e.as_ref().to_string()).unwrap_or_default();
if msg.state.can_fail() {
msg.state = MessageState::OutFailed;
}
if let Some(error) = error {
msg.param.set(Param::Error, error.as_ref());
warn!(context, "Message failed: {}", error.as_ref());
warn!(context, "{} failed: {}", msg_id, error);
} else {
warn!(
context,
"{} seems to have failed ({}), but state is {}", msg_id, error, msg.state
)
}
if context
match context
.sql
.execute(
"UPDATE msgs SET state=?, param=? WHERE id=?;",
paramsv![msg.state, msg.param.to_string(), msg_id],
"UPDATE msgs SET state=?, error=? WHERE id=?;",
paramsx![msg.state, error, msg_id],
)
.await
.is_ok()
{
context.emit_event(Event::MsgFailed {
Ok(_) => context.emit_event(Event::MsgFailed {
chat_id: msg.chat_id,
msg_id,
});
}),
Err(e) => {
warn!(context, "{:?}", e);
}
}
}
}
/// returns Some if an event should be send
pub async fn mdn_from_ext(
pub async fn handle_mdn(
context: &Context,
from_id: u32,
rfc724_mid: &str,
@@ -1287,28 +1236,19 @@ pub async fn mdn_from_ext(
return None;
}
let res = context
let res: Result<(MsgId, ChatId, Chattype, MessageState), _> = context
.sql
.query_row(
concat!(
"SELECT",
" m.id AS msg_id,",
" c.id AS chat_id,",
" c.type AS type,",
" m.state AS state",
" FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id",
" WHERE rfc724_mid=? AND from_id=1",
" ORDER BY m.id;"
),
paramsv![rfc724_mid],
|row| {
Ok((
row.get::<_, MsgId>("msg_id")?,
row.get::<_, ChatId>("chat_id")?,
row.get::<_, Chattype>("type")?,
row.get::<_, MessageState>("state")?,
))
},
r#"
SELECT
m.id AS msg_id,
c.id AS chat_id,
c.type AS type,
m.state AS state
FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id
WHERE rfc724_mid=? AND from_id=1
ORDER BY m.id;"#,
paramsx![rfc724_mid],
)
.await;
if let Err(ref err) = res {
@@ -1318,15 +1258,15 @@ pub async fn mdn_from_ext(
if let Ok((msg_id, chat_id, chat_type, msg_state)) = res {
let mut read_by_all = false;
// if already marked as MDNS_RCVD msgstate_can_fail() returns false.
// however, it is important, that ret_msg_id is set above as this
// will allow the caller eg. to move the message away
if msg_state.can_fail() {
if msg_state == MessageState::OutPreparing
|| msg_state == MessageState::OutPending
|| msg_state == MessageState::OutDelivered
{
let mdn_already_in_table = context
.sql
.exists(
"SELECT contact_id FROM msgs_mdns WHERE msg_id=? AND contact_id=?;",
paramsv![msg_id, from_id as i32,],
paramsx![msg_id, from_id as i32,],
)
.await
.unwrap_or_default();
@@ -1334,7 +1274,7 @@ pub async fn mdn_from_ext(
if !mdn_already_in_table {
context.sql.execute(
"INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);",
paramsv![msg_id, from_id as i32, timestamp_sent],
paramsx![msg_id, from_id as i32, timestamp_sent],
)
.await
.unwrap_or_default(); // TODO: better error handling
@@ -1346,15 +1286,16 @@ pub async fn mdn_from_ext(
read_by_all = true;
} else {
// send event about new state
let ist_cnt = context
let ist_cnt: i32 = context
.sql
.query_get_value::<isize>(
context,
.query_value(
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?;",
paramsv![msg_id],
paramsx![msg_id],
)
.await
.unwrap_or_default() as usize;
.unwrap_or_default();
let ist_cnt = ist_cnt as usize;
/*
Groupsize: Min. MDNs
@@ -1384,16 +1325,73 @@ pub async fn mdn_from_ext(
None
}
/// Marks a message as failed after an ndn (non-delivery-notification) arrived.
/// Where appropriate, also adds an info message telling the user which of the recipients of a group message failed.
pub(crate) async fn handle_ndn(
context: &Context,
failed: &FailureReport,
error: Option<impl AsRef<str>>,
) {
if failed.rfc724_mid.is_empty() {
return;
}
let res: Result<(MsgId, ChatId, Chattype), _> = context
.sql
.query_row(
r#"
SELECT
m.id AS msg_id,
c.id AS chat_id,
c.type AS type
FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id
WHERE rfc724_mid=? AND from_id=1
"#,
paramsx![&failed.rfc724_mid],
)
.await;
if let Err(ref err) = res {
info!(context, "Failed to select NDN {:?}", err);
}
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 let Some(failed_recipient) = &failed.failed_recipient {
let contact_id =
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown).await;
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
// Tell the user which of the recipients failed if we know that (because in a group, this might otherwise be unclear)
chat::add_info_msg(
context,
chat_id,
context
.stock_string_repl_str(
StockMessage::FailedSendingTo,
contact.get_display_name(),
)
.await,
)
.await;
context.emit_event(Event::ChatModified(chat_id));
}
}
}
}
}
/// The number of messages assigned to real chat (!=deaddrop, !=trash)
pub async fn get_real_msg_cnt(context: &Context) -> i32 {
match context
.sql
.query_row(
"SELECT COUNT(*) \
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \
WHERE m.id>9 AND m.chat_id>9 AND c.blocked=0;",
paramsv![],
|row| row.get(0),
.query_value(
r#"
SELECT COUNT(*)
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
WHERE m.id>9 AND m.chat_id>9 AND c.blocked=0;
"#,
paramsx![],
)
.await
{
@@ -1406,17 +1404,17 @@ pub async fn get_real_msg_cnt(context: &Context) -> i32 {
}
pub async fn get_deaddrop_msg_cnt(context: &Context) -> usize {
match context
let res: Result<i32, _> = context
.sql
.query_row(
"SELECT COUNT(*) \
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \
WHERE c.blocked=2;",
paramsv![],
|row| row.get::<_, isize>(0),
.query_value(
r#"
SELECT COUNT(*)
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
WHERE c.blocked=2;"#,
paramsx![],
)
.await
{
.await;
match res {
Ok(res) => res as usize,
Err(err) => {
error!(context, "dc_get_deaddrop_msg_cnt() failed. {}", err);
@@ -1436,37 +1434,39 @@ pub async fn estimate_deletion_cnt(
.0;
let threshold_timestamp = time() - seconds;
let cnt: isize = if from_server {
let cnt: i32 = if from_server {
context
.sql
.query_row(
"SELECT COUNT(*)
.query_value(
r#"SELECT COUNT(*)
FROM msgs m
WHERE m.id > ?
AND timestamp < ?
AND chat_id != ?
AND server_uid != 0;",
paramsv![DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id],
|row| row.get(0),
AND server_uid != 0;"#,
paramsx![
DC_MSG_ID_LAST_SPECIAL as i32,
threshold_timestamp,
self_chat_id
],
)
.await?
} else {
context
.sql
.query_row(
"SELECT COUNT(*)
.query_value(
r#"SELECT COUNT(*)
FROM msgs m
WHERE m.id > ?
AND timestamp < ?
AND chat_id != ?
AND chat_id != ? AND hidden = 0;",
paramsv![
DC_MSG_ID_LAST_SPECIAL,
AND chat_id != ? AND hidden = 0;"#,
paramsx![
DC_MSG_ID_LAST_SPECIAL as i32,
threshold_timestamp,
self_chat_id,
ChatId::new(DC_CHAT_ID_TRASH)
],
|row| row.get(0),
)
.await?
};
@@ -1481,10 +1481,9 @@ pub async fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> i32 {
// check the number of messages with the same rfc724_mid
match context
.sql
.query_row(
.query_value(
"SELECT COUNT(*) FROM msgs WHERE rfc724_mid=? AND NOT server_uid = 0",
paramsv![rfc724_mid],
|row| row.get(0),
paramsx![rfc724_mid],
)
.await
{
@@ -1505,22 +1504,15 @@ pub(crate) async fn rfc724_mid_exists(
return Ok(None);
}
let res = context
let res: Option<(Option<String>, i32, MsgId)> = context
.sql
.query_row_optional(
"SELECT server_folder, server_uid, id FROM msgs WHERE rfc724_mid=?",
paramsv![rfc724_mid],
|row| {
let server_folder = row.get::<_, Option<String>>(0)?.unwrap_or_default();
let server_uid = row.get(1)?;
let msg_id: MsgId = row.get(2)?;
Ok((server_folder, server_uid, msg_id))
},
paramsx![rfc724_mid],
)
.await?;
Ok(res)
Ok(res.map(|(a, b, c)| (a.unwrap_or_default(), b as u32, c)))
}
pub async fn update_server_uid(
@@ -1532,9 +1524,8 @@ pub async fn update_server_uid(
match context
.sql
.execute(
"UPDATE msgs SET server_folder=?, server_uid=? \
WHERE rfc724_mid=?",
paramsv![server_folder.as_ref(), server_uid, rfc724_mid],
"UPDATE msgs SET server_folder=?, server_uid=? WHERE rfc724_mid=?",
paramsx![server_folder.as_ref(), server_uid as i32, rfc724_mid],
)
.await
{
@@ -1572,7 +1563,7 @@ mod tests {
async fn test_prepare_message_and_send() {
use crate::config::Config;
let d = test::dummy_context().await;
let d = test::TestContext::new().await;
let ctx = &d.ctx;
let contact = Contact::create(ctx, "", "dest@example.com")
@@ -1596,7 +1587,7 @@ mod tests {
#[async_std::test]
async fn test_get_summarytext_by_raw() {
let d = test::dummy_context().await;
let d = test::TestContext::new().await;
let ctx = &d.ctx;
let some_text = Some("bla bla".to_string());

View File

@@ -1,3 +1,4 @@
use async_std::prelude::*;
use chrono::TimeZone;
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
@@ -87,30 +88,26 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
if chat.is_self_talk() {
recipients.push((from_displayname.to_string(), from_addr.to_string()));
} else {
context
.sql
.query_map(
"SELECT c.authname, c.addr \
FROM chats_contacts cc \
LEFT JOIN contacts c ON cc.contact_id=c.id \
WHERE cc.chat_id=? AND cc.contact_id>9;",
paramsv![msg.chat_id],
|row| {
let authname: String = row.get(0)?;
let addr: String = row.get(1)?;
Ok((authname, addr))
},
|rows| {
for row in rows {
let (authname, addr) = row?;
if !recipients_contain_addr(&recipients, &addr) {
recipients.push((authname, addr));
}
}
Ok(())
},
)
.await?;
let pool = context.sql.get_pool().await?;
let mut rows = sqlx::query_as(
r#"
SELECT c.authname, c.addr
FROM chats_contacts cc
LEFT JOIN contacts c ON cc.contact_id=c.id
WHERE cc.chat_id=? AND cc.contact_id>9;
"#,
)
.bind(msg.chat_id)
.fetch(&pool);
while let Some(row) = rows.next().await {
let (authname, addr): (String, String) = row?;
if !recipients_contain_addr(&recipients, &addr) {
recipients.push((authname, addr));
}
}
let command = msg.param.get_cmd();
@@ -125,18 +122,15 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
.sql
.query_row(
"SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?",
paramsv![msg.id],
|row| {
let in_reply_to: String = row.get(0)?;
let references: String = row.get(1)?;
Ok((
render_rfc724_mid_list(&in_reply_to),
render_rfc724_mid_list(&references),
))
},
paramsx![msg.id],
)
.await?;
.await
.map(|(in_reply_to, references): (String, String)| {
(
render_rfc724_mid_list(&in_reply_to),
render_rfc724_mid_list(&references),
)
})?;
let default_str = context
.stock_str(StockMessage::StatusLine)
@@ -211,7 +205,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
Ok(res)
}
async fn peerstates_for_recipients(&self) -> Result<Vec<(Option<Peerstate<'_>>, &str)>, Error> {
async fn peerstates_for_recipients(&self) -> Result<Vec<(Option<Peerstate>, &str)>, Error> {
let self_addr = self
.context
.get_config(Config::ConfiguredAddr)
@@ -225,7 +219,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
.filter(|(_, addr)| addr != &self_addr)
{
res.push((
Peerstate::from_addr(self.context, addr).await,
Peerstate::from_addr(self.context, addr).await.ok(),
addr.as_str(),
));
}
@@ -351,16 +345,47 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
};
format!("{}{}", re, chat.name)
} else {
let raw = message::get_summarytext_by_raw(
self.msg.viewtype,
self.msg.text.as_ref(),
&self.msg.param,
32,
self.context,
)
.await;
let raw_subject = raw.lines().next().unwrap_or_default();
format!("Chat: {}", raw_subject)
match chat.param.get(Param::LastSubject) {
Some(last_subject) => {
let subject_start = if last_subject.starts_with("Chat:") {
0
} else {
// "Antw:" is the longest abbreviation in
// https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations#Abbreviations_in_other_languages,
// so look at the first _5_ characters:
match last_subject.chars().take(5).position(|c| c == ':') {
Some(prefix_end) => prefix_end + 1,
None => 0,
}
};
format!(
"Re: {}",
last_subject
.chars()
.skip(subject_start)
.collect::<String>()
.trim()
)
}
None => {
let self_name = match self.context.get_config(Config::Displayname).await
{
Some(name) => name,
None => self
.context
.get_config(Config::Addr)
.await
.unwrap_or_default(),
};
self.context
.stock_string_repl_str(
StockMessage::SubjectForNewContact,
self_name,
)
.await
}
}
}
}
Loaded::MDN { .. } => self
@@ -409,6 +434,8 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
to.push(from.clone());
}
unprotected_headers.push(Header::new("MIME-Version".into(), "1.0".into()));
if !self.references.is_empty() {
unprotected_headers.push(Header::new("References".into(), self.references.clone()));
}
@@ -1174,6 +1201,10 @@ pub fn needs_encoding(to_check: impl AsRef<str>) -> bool {
#[cfg(test)]
mod tests {
use super::*;
use crate::chatlist::Chatlist;
use crate::dc_receive_imf::dc_receive_imf;
use crate::mimeparser::*;
use crate::test_utils::TestContext;
#[test]
fn test_render_email_address() {
@@ -1181,6 +1212,9 @@ mod tests {
let addr = "x@y.org";
assert!(!display_name.is_ascii());
assert!(!display_name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == ' '));
let s = format!(
"{}",
@@ -1192,6 +1226,25 @@ mod tests {
assert_eq!(s, "=?utf-8?q?=C3=A4_space?= <x@y.org>");
}
#[test]
fn test_render_email_address_noescape() {
let display_name = "a space";
let addr = "x@y.org";
assert!(display_name.is_ascii());
assert!(display_name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == ' '));
let s = format!(
"{}",
Address::new_mailbox_with_name(display_name.to_string(), addr.to_string())
);
// Addresses should not be unnecessarily be encoded, see https://github.com/deltachat/deltachat-core-rust/issues/1575:
assert_eq!(s, "a space <x@y.org>");
}
#[test]
fn test_render_rfc724_mid() {
assert_eq!(
@@ -1234,4 +1287,193 @@ mod tests {
assert!(needs_encoding(" "));
assert!(needs_encoding("foo bar"));
}
#[async_std::test]
async fn test_subject() {
// 1.: Receive a mail from an MUA or Delta Chat
assert_eq!(
msg_to_subject_str(
b"From: Bob <bob@example.com>\n\
To: alice@example.com\n\
Subject: Antw: Chat: hello\n\
Message-ID: <2222@example.com>\n\
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n"
)
.await,
"Re: Chat: hello"
);
assert_eq!(
msg_to_subject_str(
b"From: Bob <bob@example.com>\n\
To: alice@example.com\n\
Subject: Infos: 42\n\
Message-ID: <2222@example.com>\n\
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n"
)
.await,
"Re: Infos: 42"
);
// 2. Receive a message from Delta Chat when we did not send any messages before
assert_eq!(
msg_to_subject_str(
b"From: Charlie <charlie@example.com>\n\
To: alice@example.com\n\
Subject: Chat: hello\n\
Chat-Version: 1.0\n\
Message-ID: <2223@example.com>\n\
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n"
)
.await,
"Re: Chat: hello"
);
// 3. Send the first message to a new contact
let t = TestContext::new_alice().await;
assert_eq!(first_subject_str(t).await, "Message from alice@example.com");
let t = TestContext::new_alice().await;
t.ctx
.set_config(Config::Displayname, Some("Alice"))
.await
.unwrap();
assert_eq!(first_subject_str(t).await, "Message from Alice");
// 4. Receive messages with unicode characters and make sure that we do not panic (we do not care about the result)
msg_to_subject_str(
"From: Charlie <charlie@example.com>\n\
To: alice@example.com\n\
Subject: äääää\n\
Chat-Version: 1.0\n\
Message-ID: <2893@example.com>\n\
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n"
.as_bytes(),
)
.await;
msg_to_subject_str(
"From: Charlie <charlie@example.com>\n\
To: alice@example.com\n\
Subject: aäääää\n\
Chat-Version: 1.0\n\
Message-ID: <2893@example.com>\n\
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n"
.as_bytes(),
)
.await;
}
async fn first_subject_str(t: TestContext) -> String {
let contact_id =
Contact::add_or_lookup(&t.ctx, "Dave", "dave@example.com", Origin::ManuallyCreated)
.await
.unwrap()
.0;
let chat_id = chat::create_by_contact_id(&t.ctx, contact_id)
.await
.unwrap();
let mut new_msg = Message::new(Viewtype::Text);
new_msg.set_text(Some("Hi".to_string()));
new_msg.chat_id = chat_id;
chat::prepare_msg(&t.ctx, chat_id, &mut new_msg)
.await
.unwrap();
let mf = MimeFactory::from_msg(&t.ctx, &new_msg, false)
.await
.unwrap();
mf.subject_str().await
}
async fn msg_to_subject_str(imf_raw: &[u8]) -> String {
let t = TestContext::new_alice().await;
let new_msg = incoming_msg_to_reply_msg(imf_raw, &t.ctx).await;
let mf = MimeFactory::from_msg(&t.ctx, &new_msg, false)
.await
.unwrap();
mf.subject_str().await
}
// Creates a mimefactory for a message that replies "Hi" to the incoming message in `imf_raw`.
async fn incoming_msg_to_reply_msg(imf_raw: &[u8], context: &Context) -> Message {
context
.set_config(Config::ShowEmails, Some("2"))
.await
.unwrap();
dc_receive_imf(context, imf_raw, "INBOX", 1, false)
.await
.unwrap();
let chats = Chatlist::try_load(context, 0, None, None).await.unwrap();
let chat_id = chat::create_by_msg_id(context, chats.get_msg_id(0).unwrap())
.await
.unwrap();
let mut new_msg = Message::new(Viewtype::Text);
new_msg.set_text(Some("Hi".to_string()));
new_msg.chat_id = chat_id;
chat::prepare_msg(context, chat_id, &mut new_msg)
.await
.unwrap();
new_msg
}
#[async_std::test]
// This test could still be extended
async fn test_render_reply() {
let t = TestContext::new_alice().await;
let context = &t.ctx;
let msg = incoming_msg_to_reply_msg(
b"From: Charlie <charlie@example.com>\n\
To: alice@example.com\n\
Subject: Chat: hello\n\
Chat-Version: 1.0\n\
Message-ID: <2223@example.com>\n\
Date: Sun, 22 Mar 2020 22:37:56 +0000\n\
\n\
hello\n",
context,
)
.await;
let mimefactory = MimeFactory::from_msg(&t.ctx, &msg, false).await.unwrap();
let recipients = mimefactory.recipients();
assert_eq!(recipients, vec!["charlie@example.com"]);
let rendered_msg = mimefactory.render().await.unwrap();
let mail = mailparse::parse_mail(&rendered_msg.message).unwrap();
assert_eq!(
mail.headers
.iter()
.find(|h| h.get_key() == "MIME-Version")
.unwrap()
.get_value(),
"1.0"
);
let _mime_msg = MimeMessage::from_bytes(context, &rendered_msg.message)
.await
.unwrap();
}
}

View File

@@ -2,7 +2,8 @@ use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::pin::Pin;
use deltachat_derive::{FromSql, ToSql};
use deltachat_derive::*;
use lazy_static::lazy_static;
use lettre_email::mime::{self, Mime};
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
@@ -17,6 +18,7 @@ use crate::e2ee;
use crate::error::{bail, Result};
use crate::events::Event;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::Fingerprint;
use crate::location;
use crate::message;
use crate::param::*;
@@ -44,7 +46,7 @@ pub struct MimeMessage {
pub from: Vec<SingleInfo>,
pub chat_disposition_notification_to: Option<SingleInfo>,
pub decrypting_failed: bool,
pub signatures: HashSet<String>,
pub signatures: HashSet<Fingerprint>,
pub gossipped_addr: HashSet<String>,
pub is_forwarded: bool,
pub is_system_message: SystemMessage,
@@ -52,7 +54,8 @@ pub struct MimeMessage {
pub message_kml: Option<location::Kml>,
pub(crate) user_avatar: Option<AvatarAction>,
pub(crate) group_avatar: Option<AvatarAction>,
pub(crate) reports: Vec<Report>,
pub(crate) mdn_reports: Vec<Report>,
pub(crate) failure_report: Option<FailureReport>,
}
#[derive(Debug, PartialEq)]
@@ -61,7 +64,7 @@ pub(crate) enum AvatarAction {
Change(String),
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
#[repr(i32)]
pub enum SystemMessage {
Unknown = 0,
@@ -175,14 +178,16 @@ impl MimeMessage {
signatures,
gossipped_addr,
is_forwarded: false,
reports: Vec::new(),
mdn_reports: Vec::new(),
is_system_message: SystemMessage::Unknown,
location_kml: None,
message_kml: None,
user_avatar: None,
group_avatar: None,
failure_report: None,
};
parser.parse_mime_recursive(context, &mail).await?;
parser.heuristically_parse_ndn(context).await;
parser.parse_headers(context)?;
Ok(parser)
@@ -252,7 +257,8 @@ impl MimeMessage {
self.parts[0].msg = "".to_string();
// swap new with old
std::mem::replace(&mut self.parts[0], filepart);
self.parts.push(filepart); // push to the end
let _ = self.parts.swap_remove(0); // drops first element, replacing it with the last one in O(1)
}
}
}
@@ -352,7 +358,7 @@ impl MimeMessage {
// just have send a message in the subject with an empty body.
// Besides, we want to show something in case our incoming-processing
// failed to properly handle an incoming message.
if self.parts.is_empty() && self.reports.is_empty() {
if self.parts.is_empty() && self.mdn_reports.is_empty() {
let mut part = Part::default();
part.typ = Viewtype::Text;
@@ -526,6 +532,7 @@ impl MimeMessage {
part.typ = Viewtype::Text;
part.msg_raw = Some(txt.clone());
part.msg = txt;
part.error = "Decryption failed".to_string();
self.parts.push(part);
@@ -548,10 +555,10 @@ impl MimeMessage {
(mime::MULTIPART, "report") => {
/* RFC 6522: the first part is for humans, the second for machines */
if mail.subparts.len() >= 2 {
if let Some(report_type) = mail.ctype.params.get("report-type") {
if report_type == "disposition-notification" {
match mail.ctype.params.get("report-type").map(|s| s as &str) {
Some("disposition-notification") => {
if let Some(report) = self.process_report(context, mail)? {
self.reports.push(report);
self.mdn_reports.push(report);
}
// Add MDN part so we can track it, avoid
@@ -563,9 +570,21 @@ impl MimeMessage {
self.parts.push(part);
any_part_added = true;
} else {
/* eg. `report-type=delivery-status`;
maybe we should show them as a little error icon */
}
// Some providers, e.g. Tiscali, forget to set the report-type. So, if it's None, assume that it might be delivery-status
Some("delivery-status") | None => {
if let Some(report) = self.process_delivery_status(context, mail)? {
self.failure_report = Some(report);
}
// Add all parts (we need another part, preferrably text/plain, to show as an error message)
for cur_data in mail.subparts.iter() {
if self.parse_mime_recursive(context, cur_data).await? {
any_part_added = true;
}
}
}
Some(_) => {
if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(context, first).await?;
}
@@ -838,24 +857,118 @@ impl MimeMessage {
Ok(None)
}
/// Handle reports (only MDNs for now)
pub async fn handle_reports(&self, context: &Context, from_id: u32, sent_timestamp: i64) {
if self.reports.is_empty() {
return;
fn process_delivery_status(
&self,
context: &Context,
report: &mailparse::ParsedMail<'_>,
) -> Result<Option<FailureReport>> {
// parse as mailheaders
if let Some(original_msg) = report
.subparts
.iter()
.find(|p| p.ctype.mimetype.contains("rfc822") || p.ctype.mimetype == "message/global")
{
let report_body = original_msg.get_body_raw()?;
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
if let Some(original_message_id) = report_fields
.get_header_value(HeaderDef::MessageId)
.and_then(|v| parse_message_id(&v).ok())
{
let mut to_list = get_all_addresses_from_header(&report.headers, |header_key| {
header_key == "x-failed-recipients"
});
let to = if to_list.len() == 1 {
Some(to_list.pop().unwrap())
} else {
None // We do not know which recipient failed
};
return Ok(Some(FailureReport {
rfc724_mid: original_message_id,
failed_recipient: to.map(|s| s.addr),
}));
}
warn!(
context,
"ignoring unknown ndn-notification, Message-Id: {:?}",
report_fields.get_header_value(HeaderDef::MessageId)
);
}
for report in &self.reports {
Ok(None)
}
/// 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).
async fn heuristically_parse_ndn(&mut self, context: &Context) -> Option<()> {
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")
} else {
false
};
if maybe_ndn && self.failure_report.is_none() {
lazy_static! {
static ref RE: regex::Regex = regex::Regex::new(r"Message-ID:(.*)").unwrap();
}
for captures in self
.parts
.iter()
.filter_map(|part| part.msg_raw.as_ref())
.flat_map(|part| part.lines())
.filter_map(|line| RE.captures(line))
{
if let Ok(original_message_id) = parse_message_id(&captures[1]) {
if let Ok(Some(_)) =
message::rfc724_mid_exists(context, &original_message_id).await
{
self.failure_report = Some(FailureReport {
rfc724_mid: original_message_id,
failed_recipient: None,
})
}
}
}
}
None // Always return None, we just return anything so that we can use the '?' operator.
}
/// Handle reports
/// (MDNs = Message Disposition Notification, the message was read
/// and NDNs = Non delivery notification, the message could not be delivered)
pub async fn handle_reports(
&self,
context: &Context,
from_id: u32,
sent_timestamp: i64,
parts: &[Part],
) {
for report in &self.mdn_reports {
for original_message_id in
std::iter::once(&report.original_message_id).chain(&report.additional_message_ids)
{
if let Some((chat_id, msg_id)) =
message::mdn_from_ext(context, from_id, original_message_id, sent_timestamp)
.await
message::handle_mdn(context, from_id, original_message_id, sent_timestamp).await
{
context.emit_event(Event::MsgRead { chat_id, msg_id });
}
}
}
if let Some(failure_report) = &self.failure_report {
let error = parts.iter().find(|p| p.typ == Viewtype::Text).map(|p| {
let msg = &p.msg;
match msg.find("\n--- ") {
Some(footer_start) => &msg[..footer_start],
None => msg,
}
.trim()
});
message::handle_ndn(context, failure_report, error).await
}
}
}
@@ -876,12 +989,12 @@ async fn update_gossip_peerstates(
.iter()
.any(|info| info.addr == header.addr.to_lowercase())
{
let mut peerstate = Peerstate::from_addr(context, &header.addr).await;
let mut peerstate = Peerstate::from_addr(context, &header.addr).await.ok();
if let Some(ref mut peerstate) = peerstate {
peerstate.apply_gossip(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else {
let p = Peerstate::from_gossip(context, header, message_time);
let p = Peerstate::from_gossip(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
@@ -912,6 +1025,12 @@ pub(crate) struct Report {
additional_message_ids: Vec<String>,
}
#[derive(Debug)]
pub(crate) struct FailureReport {
pub rfc724_mid: String,
pub failed_recipient: Option<String>,
}
pub(crate) fn parse_message_ids(ids: &str) -> Result<Vec<String>> {
// take care with mailparse::msgidparse() that is pretty untolerant eg. wrt missing `<` or `>`
let mut msgids = Vec::new();
@@ -955,6 +1074,7 @@ pub struct Part {
pub bytes: usize,
pub param: Params,
org_filename: Option<String>,
pub error: String,
}
/// return mimetype and viewtype for a parsed mail
@@ -1117,7 +1237,7 @@ mod tests {
#[async_std::test]
async fn test_dc_mimeparser_crash() {
let context = dummy_context().await;
let context = TestContext::new().await;
let raw = include_bytes!("../test-data/message/issue_523.txt");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
@@ -1129,7 +1249,7 @@ mod tests {
#[async_std::test]
async fn test_get_rfc724_mid_exists() {
let context = dummy_context().await;
let context = TestContext::new().await;
let raw = include_bytes!("../test-data/message/mail_with_message_id.txt");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
@@ -1143,7 +1263,7 @@ mod tests {
#[async_std::test]
async fn test_get_rfc724_mid_not_exists() {
let context = dummy_context().await;
let context = TestContext::new().await;
let raw = include_bytes!("../test-data/message/issue_523.txt");
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..])
.await
@@ -1201,7 +1321,7 @@ mod tests {
#[async_std::test]
async fn test_parse_first_addr() {
let context = dummy_context().await;
let context = TestContext::new().await;
let raw = b"From: hello@one.org, world@two.org\n\
Chat-Disposition-Notification-To: wrong\n\
Content-Type: text/plain\n\
@@ -1222,7 +1342,7 @@ mod tests {
#[async_std::test]
async fn test_mimeparser_with_context() {
let context = dummy_context().await;
let context = TestContext::new().await;
let raw = b"From: hello\n\
Content-Type: multipart/mixed; boundary=\"==break==\";\n\
Subject: outer-subject\n\
@@ -1272,7 +1392,7 @@ mod tests {
#[async_std::test]
async fn test_mimeparser_with_avatars() {
let t = dummy_context().await;
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/mail_attach_txt.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap();
@@ -1315,7 +1435,7 @@ mod tests {
#[async_std::test]
async fn test_mimeparser_message_kml() {
let context = dummy_context().await;
let context = TestContext::new().await;
let raw = b"Chat-Version: 1.0\n\
From: foo <foo@example.org>\n\
To: bar <bar@example.org>\n\
@@ -1360,7 +1480,7 @@ Content-Disposition: attachment; filename=\"message.kml\"\n\
#[async_std::test]
async fn test_parse_mdn() {
let context = dummy_context().await;
let context = TestContext::new().await;
let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
Chat-Version: 1.0\n\
@@ -1401,7 +1521,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
);
assert_eq!(message.parts.len(), 1);
assert_eq!(message.reports.len(), 1);
assert_eq!(message.mdn_reports.len(), 1);
}
/// Test parsing multiple MDNs combined in a single message.
@@ -1410,7 +1530,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
/// multipart MIME messages.
#[async_std::test]
async fn test_parse_multiple_mdns() {
let context = dummy_context().await;
let context = TestContext::new().await;
let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
Chat-Version: 1.0\n\
@@ -1481,12 +1601,12 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\
);
assert_eq!(message.parts.len(), 2);
assert_eq!(message.reports.len(), 2);
assert_eq!(message.mdn_reports.len(), 2);
}
#[async_std::test]
async fn test_parse_mdn_with_additional_message_ids() {
let context = dummy_context().await;
let context = TestContext::new().await;
let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
Chat-Version: 1.0\n\
@@ -1528,17 +1648,20 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
);
assert_eq!(message.parts.len(), 1);
assert_eq!(message.reports.len(), 1);
assert_eq!(message.reports[0].original_message_id, "foo@example.org");
assert_eq!(message.mdn_reports.len(), 1);
assert_eq!(
&message.reports[0].additional_message_ids,
message.mdn_reports[0].original_message_id,
"foo@example.org"
);
assert_eq!(
&message.mdn_reports[0].additional_message_ids,
&["foo@example.com", "foo@example.net"]
);
}
#[async_std::test]
async fn test_parse_inline_attachment() {
let context = dummy_context().await;
let context = TestContext::new().await;
let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC)
From: sender@example.com
To: receiver@example.com
@@ -1578,7 +1701,7 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
#[async_std::test]
async fn parse_inline_image() {
let context = dummy_context().await;
let context = TestContext::new().await;
let raw = br#"Message-ID: <foobar@example.org>
From: foo <foo@example.org>
Subject: example
@@ -1624,7 +1747,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
#[async_std::test]
async fn parse_thunderbird_html_embedded_image() {
let context = dummy_context().await;
let context = TestContext::new().await;
let raw = br#"To: Alice <alice@example.org>
From: Bob <bob@example.org>
Subject: Test subject
@@ -1697,7 +1820,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
// Outlook specifies filename in the "name" attribute of Content-Type
#[async_std::test]
async fn parse_outlook_html_embedded_image() {
let context = dummy_context().await;
let context = TestContext::new().await;
let raw = br##"From: Anonymous <anonymous@example.org>
To: Anonymous <anonymous@example.org>
Subject: Delta Chat is great stuff!

View File

@@ -1,12 +1,16 @@
//! OAuth 2 module
use regex::Regex;
use std::collections::HashMap;
use async_std_resolver::{config, resolver};
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use serde::Deserialize;
use crate::context::Context;
use crate::dc_tools::*;
use crate::provider;
use crate::provider::Oauth2Authorizer;
const OAUTH2_GMAIL: Oauth2 = Oauth2 {
// see https://developers.google.com/identity/protocols/OAuth2InstalledApp
@@ -15,6 +19,7 @@ const OAUTH2_GMAIL: Oauth2 = Oauth2 {
init_token: "https://accounts.google.com/o/oauth2/token?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&code=$CODE&grant_type=authorization_code",
refresh_token: "https://accounts.google.com/o/oauth2/token?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&refresh_token=$REFRESH_TOKEN&grant_type=refresh_token",
get_userinfo: Some("https://www.googleapis.com/oauth2/v1/userinfo?alt=json&access_token=$ACCESS_TOKEN"),
mx_pattern: Some(r"^aspmx\.l\.google\.com\.$"),
};
const OAUTH2_YANDEX: Oauth2 = Oauth2 {
@@ -24,8 +29,11 @@ const OAUTH2_YANDEX: Oauth2 = Oauth2 {
init_token: "https://oauth.yandex.com/token?grant_type=authorization_code&code=$CODE&client_id=$CLIENT_ID&client_secret=58b8c6e94cf44fbe952da8511955dacf",
refresh_token: "https://oauth.yandex.com/token?grant_type=refresh_token&refresh_token=$REFRESH_TOKEN&client_id=$CLIENT_ID&client_secret=58b8c6e94cf44fbe952da8511955dacf",
get_userinfo: None,
mx_pattern: None,
};
const OAUTH2_PROVIDERS: [Oauth2; 1] = [OAUTH2_GMAIL];
#[derive(Debug, Clone, PartialEq, Eq)]
struct Oauth2 {
client_id: &'static str,
@@ -33,6 +41,7 @@ struct Oauth2 {
init_token: &'static str,
refresh_token: &'static str,
get_userinfo: Option<&'static str>,
mx_pattern: Option<&'static str>,
}
/// OAuth 2 Access Token Response
@@ -53,7 +62,7 @@ pub async fn dc_get_oauth2_url(
addr: impl AsRef<str>,
redirect_uri: impl AsRef<str>,
) -> Option<String> {
if let Some(oauth2) = Oauth2::from_address(addr) {
if let Some(oauth2) = Oauth2::from_address(addr).await {
if context
.sql
.set_raw_config(
@@ -81,15 +90,12 @@ pub async fn dc_get_oauth2_access_token(
code: impl AsRef<str>,
regenerate: bool,
) -> Option<String> {
if let Some(oauth2) = Oauth2::from_address(addr) {
if let Some(oauth2) = Oauth2::from_address(addr).await {
let lock = context.oauth2_mutex.lock().await;
// read generated token
if !regenerate && !is_expired(context).await {
let access_token = context
.sql
.get_raw_config(context, "oauth2_access_token")
.await;
let access_token = context.sql.get_raw_config("oauth2_access_token").await;
if access_token.is_some() {
// success
return access_token;
@@ -97,13 +103,10 @@ pub async fn dc_get_oauth2_access_token(
}
// generate new token: build & call auth url
let refresh_token = context
.sql
.get_raw_config(context, "oauth2_refresh_token")
.await;
let refresh_token = context.sql.get_raw_config("oauth2_refresh_token").await;
let refresh_token_for = context
.sql
.get_raw_config(context, "oauth2_refresh_token_for")
.get_raw_config("oauth2_refresh_token_for")
.await
.unwrap_or_else(|| "unset".into());
@@ -113,7 +116,7 @@ pub async fn dc_get_oauth2_access_token(
(
context
.sql
.get_raw_config(context, "oauth2_pending_redirect_uri")
.get_raw_config("oauth2_pending_redirect_uri")
.await
.unwrap_or_else(|| "unset".into()),
oauth2.init_token,
@@ -127,7 +130,7 @@ pub async fn dc_get_oauth2_access_token(
(
context
.sql
.get_raw_config(context, "oauth2_redirect_uri")
.get_raw_config("oauth2_redirect_uri")
.await
.unwrap_or_else(|| "unset".into()),
oauth2.refresh_token,
@@ -239,7 +242,7 @@ pub async fn dc_get_oauth2_addr(
addr: impl AsRef<str>,
code: impl AsRef<str>,
) -> Option<String> {
let oauth2 = Oauth2::from_address(addr.as_ref())?;
let oauth2 = Oauth2::from_address(addr.as_ref()).await?;
oauth2.get_userinfo?;
if let Some(access_token) =
@@ -263,23 +266,56 @@ pub async fn dc_get_oauth2_addr(
}
impl Oauth2 {
fn from_address(addr: impl AsRef<str>) -> Option<Self> {
async fn from_address(addr: impl AsRef<str>) -> Option<Self> {
let addr_normalized = normalize_addr(addr.as_ref());
if let Some(domain) = addr_normalized
.find('@')
.map(|index| addr_normalized.split_at(index + 1).1)
{
match domain {
"gmail.com" | "googlemail.com" => Some(OAUTH2_GMAIL),
"yandex.com" | "yandex.by" | "yandex.kz" | "yandex.ru" | "yandex.ua" | "ya.ru"
| "narod.ru" => Some(OAUTH2_YANDEX),
_ => None,
if let Some(provider) = provider::get_provider_info(&addr_normalized) {
match &provider.oauth2_authorizer {
Some(Oauth2Authorizer::Gmail) => Some(OAUTH2_GMAIL),
Some(Oauth2Authorizer::Yandex) => Some(OAUTH2_YANDEX),
None => None, // provider known to not support oauth2, no mx-lookup required
}
} else {
Oauth2::lookup_mx(domain).await
}
} else {
None
}
}
async fn lookup_mx(domain: impl AsRef<str>) -> Option<Self> {
if let Ok(resolver) = resolver(
config::ResolverConfig::default(),
config::ResolverOpts::default(),
)
.await
{
for provider in OAUTH2_PROVIDERS.iter() {
if let Some(pattern) = provider.mx_pattern {
let re = Regex::new(pattern).unwrap();
let mut fqdn: String = String::from(domain.as_ref());
if !fqdn.ends_with('.') {
fqdn.push_str(".");
}
if let Ok(res) = resolver.mx_lookup(fqdn).await {
for rr in res.iter() {
if re.is_match(&rr.exchange().to_lowercase().to_utf8()) {
return Some(provider.clone());
}
}
}
}
}
}
None
}
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 = replace_in_uri(&userinfo_url, "$ACCESS_TOKEN", access_token);
@@ -318,7 +354,7 @@ impl Oauth2 {
async fn is_expired(context: &Context) -> bool {
let expire_timestamp = context
.sql
.get_raw_config_int64(context, "oauth2_timestamp_expires")
.get_raw_config_int64("oauth2_timestamp_expires")
.await
.unwrap_or_default();
@@ -362,25 +398,39 @@ mod tests {
);
}
#[test]
fn test_oauth_from_address() {
assert_eq!(Oauth2::from_address("hello@gmail.com"), Some(OAUTH2_GMAIL));
#[async_std::test]
async fn test_oauth_from_address() {
assert_eq!(
Oauth2::from_address("hello@googlemail.com"),
Oauth2::from_address("hello@gmail.com").await,
Some(OAUTH2_GMAIL)
);
assert_eq!(
Oauth2::from_address("hello@yandex.com"),
Oauth2::from_address("hello@googlemail.com").await,
Some(OAUTH2_GMAIL)
);
assert_eq!(
Oauth2::from_address("hello@yandex.com").await,
Some(OAUTH2_YANDEX)
);
assert_eq!(
Oauth2::from_address("hello@yandex.ru").await,
Some(OAUTH2_YANDEX)
);
assert_eq!(Oauth2::from_address("hello@yandex.ru"), Some(OAUTH2_YANDEX));
assert_eq!(Oauth2::from_address("hello@web.de"), None);
assert_eq!(Oauth2::from_address("hello@web.de").await, None);
}
#[async_std::test]
async fn test_oauth_from_mx() {
assert_eq!(
Oauth2::from_address("hello@google.com").await,
Some(OAUTH2_GMAIL)
);
}
#[async_std::test]
async fn test_dc_get_oauth2_addr() {
let ctx = dummy_context().await;
let ctx = TestContext::new().await;
let addr = "dignifiedquire@gmail.com";
let code = "fail";
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code).await;
@@ -390,7 +440,7 @@ mod tests {
#[async_std::test]
async fn test_dc_get_oauth2_url() {
let ctx = dummy_context().await;
let ctx = TestContext::new().await;
let addr = "dignifiedquire@gmail.com";
let redirect_uri = "chat.delta:/com.b44t.messenger";
let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri).await;
@@ -400,7 +450,7 @@ mod tests {
#[async_std::test]
async fn test_dc_get_oauth2_token() {
let ctx = dummy_context().await;
let ctx = TestContext::new().await;
let addr = "dignifiedquire@gmail.com";
let code = "fail";
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false).await;

View File

@@ -65,9 +65,6 @@ pub enum Param {
/// For Messages
Arg4 = b'H',
/// For Messages
Error = b'L',
/// For Messages
AttachGroupImage = b'A',
@@ -103,6 +100,9 @@ pub enum Param {
/// For Chats
Selftalk = b'K',
/// For Chats: So that on sending a new message we can sent the subject to "Re: <last subject>"
LastSubject = b't',
/// For Chats
Devicetalk = b'D',
@@ -414,7 +414,7 @@ mod tests {
#[async_std::test]
async fn test_params_file_fs_path() {
let t = dummy_context().await;
let t = TestContext::new().await;
if let ParamsFile::FsPath(p) = ParamsFile::from_param(&t.ctx, "/foo/bar/baz").unwrap() {
assert_eq!(p, Path::new("/foo/bar/baz"));
} else {
@@ -424,7 +424,7 @@ mod tests {
#[async_std::test]
async fn test_params_file_blob() {
let t = dummy_context().await;
let t = TestContext::new().await;
if let ParamsFile::Blob(b) = ParamsFile::from_param(&t.ctx, "$BLOBDIR/foo").unwrap() {
assert_eq!(b.as_name(), "$BLOBDIR/foo");
} else {
@@ -435,7 +435,7 @@ mod tests {
// Tests for Params::get_file(), Params::get_path() and Params::get_blob().
#[async_std::test]
async fn test_params_get_fileparam() {
let t = dummy_context().await;
let t = TestContext::new().await;
let fname = t.dir.path().join("foo");
let mut p = Params::new();
p.set(Param::File, fname.to_str().unwrap());

View File

@@ -1,14 +1,12 @@
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module
use std::collections::HashSet;
use std::convert::TryFrom;
use std::fmt;
use num_traits::FromPrimitive;
use std::collections::HashSet;
use anyhow::Result;
use crate::aheader::*;
use crate::constants::*;
use crate::context::Context;
use crate::key::{Key, SignedPublicKey};
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::sql::Sql;
#[derive(Debug)]
@@ -26,60 +24,66 @@ pub enum PeerstateVerifiedStatus {
}
/// Peerstate represents the state of an Autocrypt peer.
pub struct Peerstate<'a> {
pub context: &'a Context,
#[derive(Debug, PartialEq, Eq)]
pub struct Peerstate {
pub addr: String,
pub last_seen: i64,
pub last_seen_autocrypt: i64,
pub prefer_encrypt: EncryptPreference,
pub public_key: Option<Key>,
pub public_key_fingerprint: Option<String>,
pub gossip_key: Option<Key>,
pub public_key: Option<SignedPublicKey>,
pub public_key_fingerprint: Option<Fingerprint>,
pub gossip_key: Option<SignedPublicKey>,
pub gossip_timestamp: i64,
pub gossip_key_fingerprint: Option<String>,
pub verified_key: Option<Key>,
pub verified_key_fingerprint: Option<String>,
pub gossip_key_fingerprint: Option<Fingerprint>,
pub verified_key: Option<SignedPublicKey>,
pub verified_key_fingerprint: Option<Fingerprint>,
pub to_save: Option<ToSave>,
pub degrade_event: Option<DegradeEvent>,
}
impl<'a> PartialEq for Peerstate<'a> {
fn eq(&self, other: &Peerstate) -> bool {
self.addr == other.addr
&& self.last_seen == other.last_seen
&& self.last_seen_autocrypt == other.last_seen_autocrypt
&& self.prefer_encrypt == other.prefer_encrypt
&& self.public_key == other.public_key
&& self.public_key_fingerprint == other.public_key_fingerprint
&& self.gossip_key == other.gossip_key
&& self.gossip_timestamp == other.gossip_timestamp
&& self.gossip_key_fingerprint == other.gossip_key_fingerprint
&& self.verified_key == other.verified_key
&& self.verified_key_fingerprint == other.verified_key_fingerprint
&& self.to_save == other.to_save
&& self.degrade_event == other.degrade_event
}
}
impl<'a> sqlx::FromRow<'a, sqlx::sqlite::SqliteRow> for Peerstate {
fn from_row(row: &sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
use sqlx::Row;
impl<'a> Eq for Peerstate<'a> {}
let mut res = Self::new(row.try_get("addr")?);
impl<'a> fmt::Debug for Peerstate<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Peerstate")
.field("addr", &self.addr)
.field("last_seen", &self.last_seen)
.field("last_seen_autocrypt", &self.last_seen_autocrypt)
.field("prefer_encrypt", &self.prefer_encrypt)
.field("public_key", &self.public_key)
.field("public_key_fingerprint", &self.public_key_fingerprint)
.field("gossip_key", &self.gossip_key)
.field("gossip_timestamp", &self.gossip_timestamp)
.field("gossip_key_fingerprint", &self.gossip_key_fingerprint)
.field("verified_key", &self.verified_key)
.field("verified_key_fingerprint", &self.verified_key_fingerprint)
.field("to_save", &self.to_save)
.field("degrade_event", &self.degrade_event)
.finish()
res.last_seen = row.try_get_unchecked("last_seen")?;
res.last_seen_autocrypt = row.try_get_unchecked("last_seen_autocrypt")?;
res.prefer_encrypt = row.try_get("prefer_encrypted")?;
res.gossip_timestamp = row.try_get_unchecked("gossip_timestamp")?;
res.public_key_fingerprint = row
.try_get::<Option<String>, _>("public_key_fingerprint")?
.map(|fp| fp.parse::<Fingerprint>())
.transpose()
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
res.gossip_key_fingerprint = row
.try_get::<Option<String>, _>("gossip_key_fingerprint")?
.map(|fp| fp.parse::<Fingerprint>())
.transpose()
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
res.verified_key_fingerprint = row
.try_get::<Option<String>, _>("verified_key_fingerprint")?
.map(|fp| fp.parse::<Fingerprint>())
.transpose()
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
res.public_key = row
.try_get::<Option<&[u8]>, _>("public_key")?
.map(|blob| SignedPublicKey::from_slice(blob))
.transpose()
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
res.gossip_key = row
.try_get::<Option<&[u8]>, _>("gossip_key")?
.map(|blob| SignedPublicKey::from_slice(blob))
.transpose()
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
res.verified_key = row
.try_get::<Option<&[u8]>, _>("verified_key")?
.map(|blob| SignedPublicKey::from_slice(blob))
.transpose()
.map_err(|err| sqlx::Error::Decode(Box::new(err)))?;
Ok(res)
}
}
@@ -100,10 +104,9 @@ pub enum DegradeEvent {
FingerprintChanged = 0x02,
}
impl<'a> Peerstate<'a> {
pub fn new(context: &'a Context, addr: String) -> Self {
impl Peerstate {
pub fn new(addr: String) -> Self {
Peerstate {
context,
addr,
last_seen: 0,
last_seen_autocrypt: 0,
@@ -120,120 +123,77 @@ impl<'a> Peerstate<'a> {
}
}
pub fn from_header(context: &'a Context, header: &Aheader, message_time: i64) -> Self {
let mut res = Self::new(context, header.addr.clone());
pub fn from_header(header: &Aheader, message_time: i64) -> Self {
let mut res = Self::new(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(Key::from(header.public_key.clone()));
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());
pub fn from_gossip(gossip_header: &Aheader, message_time: i64) -> Self {
let mut res = Self::new(gossip_header.addr.clone());
res.gossip_timestamp = message_time;
res.to_save = Some(ToSave::All);
res.gossip_key = Some(Key::from(gossip_header.public_key.clone()));
res.gossip_key = Some(gossip_header.public_key.clone());
res.recalc_fingerprint();
res
}
pub async fn from_addr(context: &'a Context, addr: &str) -> Option<Peerstate<'a>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, verified_key, verified_key_fingerprint FROM acpeerstates WHERE addr=? COLLATE NOCASE;";
Self::from_stmt(context, query, paramsv![addr]).await
pub async fn from_addr(context: &Context, addr: &str) -> Result<Peerstate> {
let query = r#"
SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key,
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint,
verified_key, verified_key_fingerprint
FROM acpeerstates
WHERE addr=? COLLATE NOCASE;
"#;
Self::from_stmt(context, query, paramsx![addr]).await
}
pub async fn from_fingerprint(
context: &'a Context,
_sql: &Sql,
fingerprint: &str,
) -> Option<Peerstate<'a>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint \
FROM acpeerstates \
WHERE public_key_fingerprint=? COLLATE NOCASE \
OR gossip_key_fingerprint=? COLLATE NOCASE \
ORDER BY public_key_fingerprint=? DESC;";
context: &Context,
fingerprint: &Fingerprint,
) -> Result<Peerstate> {
let query = r#"
SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key,
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint,
verified_key, verified_key_fingerprint
FROM acpeerstates
WHERE public_key_fingerprint=? COLLATE NOCASE
OR gossip_key_fingerprint=? COLLATE NOCASE
ORDER BY public_key_fingerprint=? DESC;
"#;
let fingerprint = fingerprint.hex();
Self::from_stmt(
context,
query,
paramsv![fingerprint, fingerprint, fingerprint],
paramsx![&fingerprint, &fingerprint, &fingerprint],
)
.await
}
async fn from_stmt(
context: &'a Context,
query: &str,
params: Vec<&dyn crate::ToSql>,
) -> Option<Peerstate<'a>> {
context
.sql
.query_row(query, params, |row| {
/* all the above queries start with this: SELECT
addr, last_seen, last_seen_autocrypt, prefer_encrypted,
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)?);
async fn from_stmt<'a, P: sqlx::IntoArguments<'a, sqlx::sqlite::Sqlite> + 'a>(
context: &Context,
query: &'a str,
params: P,
) -> Result<Peerstate> {
/* all the above queries start with this: SELECT
addr, last_seen, last_seen_autocrypt, prefer_encrypted,
public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
gossip_key_fingerprint, verified_key, verified_key_fingerprint
*/
let peerstate = context.sql.query_row(query, params).await?;
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(7)?;
if res
.public_key_fingerprint
.as_ref()
.map(|s| s.is_empty())
.unwrap_or_default()
{
res.public_key_fingerprint = None;
}
res.gossip_key_fingerprint = row.get(8)?;
if res
.gossip_key_fingerprint
.as_ref()
.map(|s| s.is_empty())
.unwrap_or_default()
{
res.gossip_key_fingerprint = None;
}
res.verified_key_fingerprint = row.get(10)?;
if res
.verified_key_fingerprint
.as_ref()
.map(|s| s.is_empty())
.unwrap_or_default()
{
res.verified_key_fingerprint = None;
}
res.public_key = row
.get(4)
.ok()
.and_then(|blob: Vec<u8>| Key::from_slice(&blob, KeyType::Public).ok());
res.gossip_key = row
.get(6)
.ok()
.and_then(|blob: Vec<u8>| Key::from_slice(&blob, KeyType::Public).ok());
res.verified_key = row
.get(9)
.ok()
.and_then(|blob: Vec<u8>| Key::from_slice(&blob, KeyType::Public).ok());
Ok(res)
})
.await
.ok()
Ok(peerstate)
}
pub fn recalc_fingerprint(&mut self) {
@@ -300,8 +260,8 @@ impl<'a> Peerstate<'a> {
self.to_save = Some(ToSave::All)
}
if self.public_key.as_ref() != Some(&Key::from(header.public_key.clone())) {
self.public_key = Some(Key::from(header.public_key.clone()));
if self.public_key.as_ref() != Some(&header.public_key) {
self.public_key = Some(header.public_key.clone());
self.recalc_fingerprint();
self.to_save = Some(ToSave::All);
}
@@ -316,9 +276,8 @@ impl<'a> Peerstate<'a> {
if message_time > self.gossip_timestamp {
self.gossip_timestamp = message_time;
self.to_save = Some(ToSave::Timestamps);
let hdr_key = Key::from(gossip_header.public_key.clone());
if self.gossip_key.as_ref() != Some(&hdr_key) {
self.gossip_key = Some(hdr_key);
if self.gossip_key.as_ref() != Some(&gossip_header.public_key) {
self.gossip_key = Some(gossip_header.public_key.clone());
self.recalc_fingerprint();
self.to_save = Some(ToSave::All)
}
@@ -346,11 +305,9 @@ impl<'a> Peerstate<'a> {
pub fn render_gossip_header(&self, min_verified: PeerstateVerifiedStatus) -> Option<String> {
if let Some(key) = self.peek_key(min_verified) {
// TODO: avoid cloning
let public_key = SignedPublicKey::try_from(key.clone()).ok()?;
let header = Aheader::new(
self.addr.clone(),
public_key,
key.clone(), // TODO: avoid cloning
// Autocrypt 1.1.0 specification says that
// `prefer-encrypt` attribute SHOULD NOT be included,
// but we include it anyway to propagate encryption
@@ -367,7 +324,7 @@ impl<'a> Peerstate<'a> {
}
}
pub fn take_key(mut self, min_verified: PeerstateVerifiedStatus) -> Option<Key> {
pub fn take_key(mut self, min_verified: PeerstateVerifiedStatus) -> Option<SignedPublicKey> {
match min_verified {
PeerstateVerifiedStatus::BidirectVerified => self.verified_key.take(),
PeerstateVerifiedStatus::Unverified => {
@@ -376,7 +333,7 @@ impl<'a> Peerstate<'a> {
}
}
pub fn peek_key(&self, min_verified: PeerstateVerifiedStatus) -> Option<&Key> {
pub fn peek_key(&self, min_verified: PeerstateVerifiedStatus) -> Option<&SignedPublicKey> {
match min_verified {
PeerstateVerifiedStatus::BidirectVerified => self.verified_key.as_ref(),
PeerstateVerifiedStatus::Unverified => self
@@ -389,7 +346,7 @@ impl<'a> Peerstate<'a> {
pub fn set_verified(
&mut self,
which_key: PeerstateKeyType,
fingerprint: &str,
fingerprint: &Fingerprint,
verified: PeerstateVerifiedStatus,
) -> bool {
if verified == PeerstateVerifiedStatus::BidirectVerified {
@@ -428,41 +385,42 @@ impl<'a> Peerstate<'a> {
if create {
sql.execute(
"INSERT INTO acpeerstates (addr) VALUES(?);",
paramsv![self.addr],
paramsx![&self.addr],
)
.await?;
}
if self.to_save == Some(ToSave::All) || create {
sql.execute(
"UPDATE acpeerstates \
SET last_seen=?, last_seen_autocrypt=?, prefer_encrypted=?, \
public_key=?, gossip_timestamp=?, gossip_key=?, public_key_fingerprint=?, gossip_key_fingerprint=?, \
verified_key=?, verified_key_fingerprint=? \
WHERE addr=?;",
paramsv![
r#"
UPDATE acpeerstates
SET last_seen=?, last_seen_autocrypt=?, prefer_encrypted=?,
public_key=?, gossip_timestamp=?, gossip_key=?, public_key_fingerprint=?, gossip_key_fingerprint=?,
verified_key=?, verified_key_fingerprint=?
WHERE addr=?;
"#,
paramsx![
self.last_seen,
self.last_seen_autocrypt,
self.prefer_encrypt as i64,
self.public_key.as_ref().map(|k| k.to_bytes()),
self.gossip_timestamp,
self.gossip_key.as_ref().map(|k| k.to_bytes()),
self.public_key_fingerprint,
self.gossip_key_fingerprint,
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verified_key.as_ref().map(|k| k.to_bytes()),
self.verified_key_fingerprint,
self.addr,
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
&self.addr
],
).await?;
} else if self.to_save == Some(ToSave::Timestamps) {
sql.execute(
"UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \
WHERE addr=?;",
paramsv![
"UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? WHERE addr=?;",
paramsx![
self.last_seen,
self.last_seen_autocrypt,
self.gossip_timestamp,
self.addr
&self.addr
],
)
.await?;
@@ -471,15 +429,12 @@ impl<'a> Peerstate<'a> {
Ok(())
}
pub fn has_verified_key(&self, fingerprints: &HashSet<String>) -> bool {
if self.verified_key.is_some() && self.verified_key_fingerprint.is_some() {
let vkc = self.verified_key_fingerprint.as_ref().unwrap();
if fingerprints.contains(vkc) {
return true;
}
pub fn has_verified_key(&self, fingerprints: &HashSet<Fingerprint>) -> bool {
if let Some(vkc) = &self.verified_key_fingerprint {
fingerprints.contains(vkc) && self.verified_key.is_some()
} else {
false
}
false
}
}
@@ -488,17 +443,15 @@ 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() {
let ctx = crate::test_utils::dummy_context().await;
let ctx = crate::test_utils::TestContext::new().await;
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from(alice_keypair().public);
let pub_key = alice_keypair().public;
let mut peerstate = Peerstate {
context: &ctx.ctx,
addr: addr.into(),
last_seen: 10,
last_seen_autocrypt: 11,
@@ -526,21 +479,19 @@ mod tests {
// clear to_save, as that is not persissted
peerstate.to_save = None;
assert_eq!(peerstate, peerstate_new);
let peerstate_new2 =
Peerstate::from_fingerprint(&ctx.ctx, &ctx.ctx.sql, &pub_key.fingerprint())
.await
.expect("failed to load peerstate from db");
let peerstate_new2 = Peerstate::from_fingerprint(&ctx.ctx, &pub_key.fingerprint())
.await
.expect("failed to load peerstate from db");
assert_eq!(peerstate, peerstate_new2);
}
#[async_std::test]
async fn test_peerstate_double_create() {
let ctx = crate::test_utils::dummy_context().await;
let ctx = crate::test_utils::TestContext::new().await;
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from(alice_keypair().public);
let pub_key = alice_keypair().public;
let peerstate = Peerstate {
context: &ctx.ctx,
addr: addr.into(),
last_seen: 10,
last_seen_autocrypt: 11,
@@ -568,13 +519,12 @@ mod tests {
#[async_std::test]
async fn test_peerstate_with_empty_gossip_key_save_to_db() {
let ctx = crate::test_utils::dummy_context().await;
let ctx = crate::test_utils::TestContext::new().await;
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from(alice_keypair().public);
let pub_key = alice_keypair().public;
let mut peerstate = Peerstate {
context: &ctx.ctx,
addr: addr.into(),
last_seen: 10,
last_seen_autocrypt: 11,
@@ -603,11 +553,4 @@ mod tests {
peerstate.to_save = None;
assert_eq!(peerstate, peerstate_new);
}
// TODO: don't copy this from stress.rs
#[allow(dead_code)]
struct TestContext {
ctx: Context,
dir: TempDir,
}
}

View File

@@ -1,7 +1,6 @@
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp)
use std::collections::{BTreeMap, HashSet};
use std::convert::TryInto;
use std::io;
use std::io::Cursor;
@@ -19,8 +18,8 @@ use rand::{thread_rng, CryptoRng, Rng};
use crate::constants::KeyGenType;
use crate::dc_tools::EmailAddress;
use crate::error::{bail, ensure, format_err, Result};
use crate::key::*;
use crate::keyring::*;
use crate::key::{DcKey, Fingerprint};
use crate::keyring::Keyring;
pub const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
pub const HEADER_SETUPCODE: &str = "passphrase-begin";
@@ -240,8 +239,8 @@ fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<SignedPublicKeyOrSu
/// and signs it using `private_key_for_signing`.
pub async fn pk_encrypt(
plain: &[u8],
public_keys_for_encryption: Keyring,
private_key_for_signing: Option<Key>,
public_keys_for_encryption: Keyring<SignedPublicKey>,
private_key_for_signing: Option<SignedSecretKey>,
) -> Result<String> {
let lit_msg = Message::new_literal_bytes("", plain);
@@ -249,18 +248,14 @@ pub async fn pk_encrypt(
let pkeys: Vec<SignedPublicKeyOrSubkey> = public_keys_for_encryption
.keys()
.iter()
.filter_map(|key| key.try_into().ok().and_then(select_pk_for_encryption))
.filter_map(|key| select_pk_for_encryption(key))
.collect();
let pkeys_refs: Vec<&SignedPublicKeyOrSubkey> = pkeys.iter().collect();
let mut rng = thread_rng();
// TODO: measure time
let encrypted_msg = if let Some(ref private_key) = private_key_for_signing {
let skey: &SignedSecretKey = private_key
.try_into()
.map_err(|_| format_err!("Invalid private key"))?;
let encrypted_msg = if let Some(ref skey) = private_key_for_signing {
lit_msg
.sign(skey, || "".into(), Default::default())
.and_then(|msg| msg.compress(CompressionAlgorithm::ZLIB))
@@ -277,61 +272,67 @@ pub async fn pk_encrypt(
.await
}
/// Decrypts the message with keys from the private key keyring.
///
/// Receiver private keys are provided in
/// `private_keys_for_decryption`.
///
/// If `ret_signature_fingerprints` is not `None`, stores fingerprints
/// of all keys from the `public_keys_for_validation` keyring that
/// have valid signatures there.
#[allow(clippy::implicit_hasher)]
pub async fn pk_decrypt(
ctext: Vec<u8>,
private_keys_for_decryption: Keyring,
public_keys_for_validation: Keyring,
ret_signature_fingerprints: Option<&mut HashSet<String>>,
private_keys_for_decryption: Keyring<SignedSecretKey>,
public_keys_for_validation: Keyring<SignedPublicKey>,
ret_signature_fingerprints: Option<&mut HashSet<Fingerprint>>,
) -> Result<Vec<u8>> {
let msgs = async_std::task::spawn_blocking(move || {
let cursor = Cursor::new(ctext);
let (msg, _) = Message::from_armor_single(cursor)?;
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption
.keys()
.iter()
.filter_map(|key| key.try_into().ok())
.collect();
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.keys().iter().collect();
let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?;
decryptor.collect::<pgp::errors::Result<Vec<_>>>()
})
.await?;
ensure!(!msgs.is_empty(), "No valid messages found");
if let Some(msg) = msgs.into_iter().next() {
// get_content() will decompress the message if needed,
// but this avoids decompressing it again to check signatures
let msg = msg.decompress()?;
let content = match msgs[0].get_content()? {
Some(content) => content,
None => bail!("Decrypted message is empty"),
};
let content = match msg.get_content()? {
Some(content) => content,
None => bail!("The decrypted message is empty"),
};
if let Some(ret_signature_fingerprints) = ret_signature_fingerprints {
if !public_keys_for_validation.is_empty() {
let fingerprints = async_std::task::spawn_blocking(move || {
let dec_msg = &msgs[0];
if let Some(ret_signature_fingerprints) = ret_signature_fingerprints {
if !public_keys_for_validation.is_empty() {
let fingerprints = async_std::task::spawn_blocking(move || {
let pkeys = public_keys_for_validation.keys();
let pkeys = public_keys_for_validation
.keys()
.iter()
.filter_map(|key| -> Option<&SignedPublicKey> { key.try_into().ok() });
let mut fingerprints = Vec::new();
for pkey in pkeys {
if dec_msg.verify(&pkey.primary_key).is_ok() {
let fp = hex::encode_upper(pkey.fingerprint());
fingerprints.push(fp);
let mut fingerprints: Vec<Fingerprint> = Vec::new();
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
for pkey in pkeys {
if signed_msg.verify(&pkey.primary_key).is_ok() {
let fp = DcKey::fingerprint(pkey);
fingerprints.push(fp);
}
}
}
}
fingerprints
})
.await;
fingerprints
})
.await;
ret_signature_fingerprints.extend(fingerprints);
ret_signature_fingerprints.extend(fingerprints);
}
}
Ok(content)
} else {
bail!("No valid messages found");
}
Ok(content)
}
/// Symmetric encryption.
@@ -424,10 +425,10 @@ mod tests {
/// [Key] objects to use in tests.
struct TestKeys {
alice_secret: Key,
alice_public: Key,
bob_secret: Key,
bob_public: Key,
alice_secret: SignedSecretKey,
alice_public: SignedPublicKey,
bob_secret: SignedSecretKey,
bob_public: SignedPublicKey,
}
impl TestKeys {
@@ -435,10 +436,10 @@ mod tests {
let alice = alice_keypair();
let bob = bob_keypair();
TestKeys {
alice_secret: Key::from(alice.secret.clone()),
alice_public: Key::from(alice.public.clone()),
bob_secret: Key::from(bob.secret.clone()),
bob_public: Key::from(bob.public.clone()),
alice_secret: alice.secret.clone(),
alice_public: alice.public.clone(),
bob_secret: bob.secret.clone(),
bob_public: bob.public.clone(),
}
}
}
@@ -452,7 +453,7 @@ mod tests {
/// A cyphertext encrypted to Alice & Bob, signed by Alice.
static ref CTEXT_SIGNED: String = {
let mut keyring = Keyring::default();
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()
@@ -460,7 +461,7 @@ mod tests {
/// A cyphertext encrypted to Alice & Bob, not signed.
static ref CTEXT_UNSIGNED: String = {
let mut keyring = Keyring::default();
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()
@@ -482,11 +483,11 @@ mod tests {
#[async_std::test]
async fn test_decrypt_singed() {
// Check decrypting as Alice
let mut decrypt_keyring = Keyring::default();
let mut decrypt_keyring: Keyring<SignedSecretKey> = Keyring::new();
decrypt_keyring.add(KEYS.alice_secret.clone());
let mut sig_check_keyring = Keyring::default();
let mut sig_check_keyring: Keyring<SignedPublicKey> = Keyring::new();
sig_check_keyring.add(KEYS.alice_public.clone());
let mut valid_signatures: HashSet<String> = Default::default();
let mut valid_signatures: HashSet<Fingerprint> = Default::default();
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes().to_vec(),
decrypt_keyring,
@@ -500,11 +501,11 @@ mod tests {
assert_eq!(valid_signatures.len(), 1);
// Check decrypting as Bob
let mut decrypt_keyring = Keyring::default();
let mut decrypt_keyring = Keyring::new();
decrypt_keyring.add(KEYS.bob_secret.clone());
let mut sig_check_keyring = Keyring::default();
let mut sig_check_keyring = Keyring::new();
sig_check_keyring.add(KEYS.alice_public.clone());
let mut valid_signatures: HashSet<String> = Default::default();
let mut valid_signatures: HashSet<Fingerprint> = Default::default();
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes().to_vec(),
decrypt_keyring,
@@ -520,10 +521,10 @@ mod tests {
#[async_std::test]
async fn test_decrypt_no_sig_check() {
let mut keyring = Keyring::default();
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_secret.clone());
let empty_keyring = Keyring::default();
let mut valid_signatures: HashSet<String> = Default::default();
let empty_keyring = Keyring::new();
let mut valid_signatures: HashSet<Fingerprint> = Default::default();
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes().to_vec(),
keyring,
@@ -539,11 +540,11 @@ mod tests {
#[async_std::test]
async fn test_decrypt_signed_no_key() {
// The validation does not have the public key of the signer.
let mut decrypt_keyring = Keyring::default();
let mut decrypt_keyring = Keyring::new();
decrypt_keyring.add(KEYS.bob_secret.clone());
let mut sig_check_keyring = Keyring::default();
let mut sig_check_keyring = Keyring::new();
sig_check_keyring.add(KEYS.bob_public.clone());
let mut valid_signatures: HashSet<String> = Default::default();
let mut valid_signatures: HashSet<Fingerprint> = Default::default();
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes().to_vec(),
decrypt_keyring,
@@ -558,11 +559,10 @@ mod tests {
#[async_std::test]
async fn test_decrypt_unsigned() {
let mut decrypt_keyring = Keyring::default();
let mut decrypt_keyring = Keyring::new();
decrypt_keyring.add(KEYS.bob_secret.clone());
let sig_check_keyring = Keyring::default();
decrypt_keyring.add(KEYS.alice_public.clone());
let mut valid_signatures: HashSet<String> = Default::default();
let sig_check_keyring = Keyring::new();
let mut valid_signatures: HashSet<Fingerprint> = Default::default();
let plain = pk_decrypt(
CTEXT_UNSIGNED.as_bytes().to_vec(),
decrypt_keyring,
@@ -578,9 +578,9 @@ mod tests {
#[async_std::test]
async fn test_decrypt_signed_no_sigret() {
// Check decrypting signed cyphertext without providing the HashSet for signatures.
let mut decrypt_keyring = Keyring::default();
let mut decrypt_keyring = Keyring::new();
decrypt_keyring.add(KEYS.bob_secret.clone());
let mut sig_check_keyring = Keyring::default();
let mut sig_check_keyring = Keyring::new();
sig_check_keyring.add(KEYS.alice_public.clone());
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes().to_vec(),

View File

@@ -19,6 +19,8 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "newyear.aktivix.org", port: 25, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// aol.md: aol.com
@@ -30,6 +32,23 @@ lazy_static::lazy_static! {
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// arcor.de.md: arcor.de
static ref P_ARCOR_DE: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/arcor-de",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.arcor.de", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "mail.arcor.de", port: 465, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// autistici.org.md: autistici.org
@@ -43,6 +62,8 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: SSL, hostname: "smtp.autistici.org", port: 465, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// bluewin.ch.md: bluewin.ch
@@ -56,16 +77,81 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: SSL, hostname: "smtpauths.bluewin.ch", port: 465, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// chello.at.md: chello.at
static ref P_CHELLO_AT: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/chello-at",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "mail.mymagenta.at", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "mail.mymagenta.at", port: 465, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// comcast.md: xfinity.com, comcast.net
// - skipping provider with status OK and no special things to do
static ref P_COMCAST: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/comcast",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// dismail.de.md: dismail.de
// - skipping provider with status OK and no special things to do
static ref P_DISMAIL_DE: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/dismail-de",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// disroot.md: disroot.org
// - skipping provider with status OK and no special things to do
static ref P_DISROOT: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/disroot",
server: vec![
],
config_defaults: None,
strict_tls: true,
oauth2_authorizer: None,
};
// dubby.org.md: dubby.org
static ref P_DUBBY_ORG: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/dubby-org",
server: vec![
],
config_defaults: Some(vec![
ConfigDefault { key: Config::BccSelf, value: "1" },
ConfigDefault { key: Config::SentboxWatch, value: "0" },
ConfigDefault { key: Config::MvboxWatch, value: "0" },
ConfigDefault { key: Config::MvboxMove, value: "0" },
]),
strict_tls: true,
oauth2_authorizer: None,
};
// example.com.md: example.com, example.org
static ref P_EXAMPLE_COM: Provider = Provider {
@@ -78,6 +164,8 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.example.com", port: 1337, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// fastmail.md: fastmail.com
@@ -89,6 +177,26 @@ lazy_static::lazy_static! {
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// five.chat.md: five.chat
static ref P_FIVE_CHAT: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/five-chat",
server: vec![
],
config_defaults: Some(vec![
ConfigDefault { key: Config::BccSelf, value: "1" },
ConfigDefault { key: Config::SentboxWatch, value: "0" },
ConfigDefault { key: Config::MvboxWatch, value: "0" },
ConfigDefault { key: Config::MvboxMove, value: "0" },
]),
strict_tls: true,
oauth2_authorizer: None,
};
// freenet.de.md: freenet.de
@@ -102,6 +210,8 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "mx.freenet.de", port: 587, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// gmail.md: gmail.com, googlemail.com
@@ -115,6 +225,8 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: SSL, hostname: "smtp.gmail.com", port: 465, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: true,
oauth2_authorizer: Some(Oauth2Authorizer::Gmail),
};
// gmx.net.md: gmx.net, gmx.de, gmx.at, gmx.ch, gmx.org, gmx.eu, gmx.info, gmx.biz, gmx.com
@@ -129,10 +241,35 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "mail.gmx.net", port: 587, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// hey.com.md: hey.com
static ref P_HEY_COM: Provider = Provider {
status: Status::BROKEN,
before_login_hint: "hey.com does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to hey.com.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/hey-com",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// i.ua.md: i.ua
// - skipping provider with status OK and no special things to do
static ref P_I_UA: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/i-ua",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// icloud.md: icloud.com, me.com, mac.com
static ref P_ICLOUD: Provider = Provider {
@@ -145,19 +282,61 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.mail.me.com", port: 587, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// kolst.com.md: kolst.com
// - skipping provider with status OK and no special things to do
static ref P_KOLST_COM: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/kolst-com",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// kontent.com.md: kontent.com
// - skipping provider with status OK and no special things to do
static ref P_KONTENT_COM: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/kontent-com",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// mail.ru.md: mail.ru, inbox.ru, bk.ru, list.ru
// - skipping provider with status OK and no special things to do
static ref P_MAIL_RU: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/mail-ru",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// mailbox.org.md: mailbox.org, secure.mailbox.org
// - skipping provider with status OK and no special things to do
static ref P_MAILBOX_ORG: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/mailbox-org",
server: vec![
],
config_defaults: None,
strict_tls: true,
oauth2_authorizer: None,
};
// nauta.cu.md: nauta.cu
static ref P_NAUTA_CU: Provider = Provider {
@@ -178,6 +357,8 @@ lazy_static::lazy_static! {
ConfigDefault { key: Config::E2eeEnabled, value: "0" },
ConfigDefault { key: Config::MediaQuality, value: "1" },
]),
strict_tls: false,
oauth2_authorizer: None,
};
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com
@@ -191,9 +372,11 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp-mail.outlook.com", port: 587, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// posteo.md: posteo.de
// posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us
static ref P_POSTEO: Provider = Provider {
status: Status::OK,
before_login_hint: "",
@@ -204,6 +387,8 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "posteo.de", port: 587, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: true,
oauth2_authorizer: None,
};
// protonmail.md: protonmail.com, protonmail.ch
@@ -215,16 +400,48 @@ lazy_static::lazy_static! {
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// riseup.net.md: riseup.net
// - skipping provider with status OK and no special things to do
static ref P_RISEUP_NET: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/riseup-net",
server: vec![
],
config_defaults: None,
strict_tls: true,
oauth2_authorizer: None,
};
// rogers.com.md: rogers.com
// - skipping provider with status OK and no special things to do
static ref P_ROGERS_COM: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/rogers-com",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// systemli.org.md: systemli.org
// - skipping provider with status OK and no special things to do
static ref P_SYSTEMLI_ORG: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/systemli-org",
server: vec![
],
config_defaults: None,
strict_tls: true,
oauth2_authorizer: None,
};
// t-online.md: t-online.de, magenta.de
static ref P_T_ONLINE: Provider = Provider {
@@ -235,6 +452,8 @@ lazy_static::lazy_static! {
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// testrun.md: testrun.org
@@ -248,7 +467,14 @@ lazy_static::lazy_static! {
Server { protocol: IMAP, socket: STARTTLS, hostname: "testrun.org", port: 143, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "testrun.org", port: 587, username_pattern: EMAIL },
],
config_defaults: None,
config_defaults: Some(vec![
ConfigDefault { key: Config::BccSelf, value: "1" },
ConfigDefault { key: Config::SentboxWatch, value: "0" },
ConfigDefault { key: Config::MvboxWatch, value: "0" },
ConfigDefault { key: Config::MvboxMove, value: "0" },
]),
strict_tls: true,
oauth2_authorizer: None,
};
// tiscali.it.md: tiscali.it
@@ -262,13 +488,35 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: SSL, hostname: "smtp.tiscali.it", port: 465, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// ukr.net.md: ukr.net
// - skipping provider with status OK and no special things to do
static ref P_UKR_NET: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/ukr-net",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// vfemail.md: vfemail.net
// - skipping provider with status OK and no special things to do
static ref P_VFEMAIL: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/vfemail",
server: vec![
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// web.de.md: web.de, email.de, flirt.ms, hallo.ms, kuss.ms, love.ms, magic.ms, singles.ms, cool.ms, kanzler.ms, okay.ms, party.ms, pop.ms, stars.ms, techno.ms, clever.ms, deutschland.ms, genial.ms, ich.ms, online.ms, smart.ms, wichtig.ms, action.ms, fussball.ms, joker.ms, planet.ms, power.ms
static ref P_WEB_DE: Provider = Provider {
@@ -282,12 +530,14 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.web.de", port: 587, username_pattern: EMAILLOCALPART },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// yahoo.md: yahoo.com, yahoo.de, yahoo.it, yahoo.fr, yahoo.es, yahoo.se, yahoo.co.uk, yahoo.co.nz, yahoo.com.au, yahoo.com.ar, yahoo.com.br, yahoo.com.mx, ymail.com, rocketmail.com, yahoodns.net
static ref P_YAHOO: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "To use Delta Chat with your Yahoo email address you have to allow \"less secure apps\" in the Yahoo webinterface.",
before_login_hint: "To use Delta Chat with your Yahoo email address you have to create an \"App-Password\" in the account security screen.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/yahoo",
server: vec![
@@ -295,9 +545,11 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: SSL, hostname: "smtp.mail.yahoo.com", port: 465, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
// yandex.ru.md: yandex.ru, yandex.com
// yandex.ru.md: yandex.com, yandex.by, yandex.kz, yandex.ru, yandex.ua, ya.ru, narod.ru
static ref P_YANDEX_RU: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "For Yandex accounts, you have to set IMAP protocol option turned on.",
@@ -306,6 +558,8 @@ lazy_static::lazy_static! {
server: vec![
],
config_defaults: None,
strict_tls: true,
oauth2_authorizer: Some(Oauth2Authorizer::Yandex),
};
// ziggo.nl.md: ziggo.nl
@@ -319,16 +573,26 @@ lazy_static::lazy_static! {
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.ziggo.nl", port: 587, username_pattern: EMAIL },
],
config_defaults: None,
strict_tls: false,
oauth2_authorizer: None,
};
pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [
("aktivix.org", &*P_AKTIVIX_ORG),
("aol.com", &*P_AOL),
("arcor.de", &*P_ARCOR_DE),
("autistici.org", &*P_AUTISTICI_ORG),
("bluewin.ch", &*P_BLUEWIN_CH),
("chello.at", &*P_CHELLO_AT),
("xfinity.com", &*P_COMCAST),
("comcast.net", &*P_COMCAST),
("dismail.de", &*P_DISMAIL_DE),
("disroot.org", &*P_DISROOT),
("dubby.org", &*P_DUBBY_ORG),
("example.com", &*P_EXAMPLE_COM),
("example.org", &*P_EXAMPLE_COM),
("fastmail.com", &*P_FASTMAIL),
("five.chat", &*P_FIVE_CHAT),
("freenet.de", &*P_FREENET_DE),
("gmail.com", &*P_GMAIL),
("googlemail.com", &*P_GMAIL),
@@ -341,9 +605,19 @@ lazy_static::lazy_static! {
("gmx.info", &*P_GMX_NET),
("gmx.biz", &*P_GMX_NET),
("gmx.com", &*P_GMX_NET),
("hey.com", &*P_HEY_COM),
("i.ua", &*P_I_UA),
("icloud.com", &*P_ICLOUD),
("me.com", &*P_ICLOUD),
("mac.com", &*P_ICLOUD),
("kolst.com", &*P_KOLST_COM),
("kontent.com", &*P_KONTENT_COM),
("mail.ru", &*P_MAIL_RU),
("inbox.ru", &*P_MAIL_RU),
("bk.ru", &*P_MAIL_RU),
("list.ru", &*P_MAIL_RU),
("mailbox.org", &*P_MAILBOX_ORG),
("secure.mailbox.org", &*P_MAILBOX_ORG),
("nauta.cu", &*P_NAUTA_CU),
("hotmail.com", &*P_OUTLOOK_COM),
("outlook.com", &*P_OUTLOOK_COM),
@@ -351,12 +625,65 @@ lazy_static::lazy_static! {
("outlook.com.tr", &*P_OUTLOOK_COM),
("live.com", &*P_OUTLOOK_COM),
("posteo.de", &*P_POSTEO),
("posteo.af", &*P_POSTEO),
("posteo.at", &*P_POSTEO),
("posteo.be", &*P_POSTEO),
("posteo.ch", &*P_POSTEO),
("posteo.cl", &*P_POSTEO),
("posteo.co", &*P_POSTEO),
("posteo.co.uk", &*P_POSTEO),
("posteo.com.br", &*P_POSTEO),
("posteo.cr", &*P_POSTEO),
("posteo.cz", &*P_POSTEO),
("posteo.dk", &*P_POSTEO),
("posteo.ee", &*P_POSTEO),
("posteo.es", &*P_POSTEO),
("posteo.eu", &*P_POSTEO),
("posteo.fi", &*P_POSTEO),
("posteo.gl", &*P_POSTEO),
("posteo.gr", &*P_POSTEO),
("posteo.hn", &*P_POSTEO),
("posteo.hr", &*P_POSTEO),
("posteo.hu", &*P_POSTEO),
("posteo.ie", &*P_POSTEO),
("posteo.in", &*P_POSTEO),
("posteo.is", &*P_POSTEO),
("posteo.jp", &*P_POSTEO),
("posteo.la", &*P_POSTEO),
("posteo.li", &*P_POSTEO),
("posteo.lt", &*P_POSTEO),
("posteo.lu", &*P_POSTEO),
("posteo.me", &*P_POSTEO),
("posteo.mx", &*P_POSTEO),
("posteo.my", &*P_POSTEO),
("posteo.net", &*P_POSTEO),
("posteo.nl", &*P_POSTEO),
("posteo.no", &*P_POSTEO),
("posteo.nz", &*P_POSTEO),
("posteo.org", &*P_POSTEO),
("posteo.pe", &*P_POSTEO),
("posteo.pl", &*P_POSTEO),
("posteo.pm", &*P_POSTEO),
("posteo.pt", &*P_POSTEO),
("posteo.ro", &*P_POSTEO),
("posteo.ru", &*P_POSTEO),
("posteo.se", &*P_POSTEO),
("posteo.sg", &*P_POSTEO),
("posteo.si", &*P_POSTEO),
("posteo.tn", &*P_POSTEO),
("posteo.uk", &*P_POSTEO),
("posteo.us", &*P_POSTEO),
("protonmail.com", &*P_PROTONMAIL),
("protonmail.ch", &*P_PROTONMAIL),
("riseup.net", &*P_RISEUP_NET),
("rogers.com", &*P_ROGERS_COM),
("systemli.org", &*P_SYSTEMLI_ORG),
("t-online.de", &*P_T_ONLINE),
("magenta.de", &*P_T_ONLINE),
("testrun.org", &*P_TESTRUN),
("tiscali.it", &*P_TISCALI_IT),
("ukr.net", &*P_UKR_NET),
("vfemail.net", &*P_VFEMAIL),
("web.de", &*P_WEB_DE),
("email.de", &*P_WEB_DE),
("flirt.ms", &*P_WEB_DE),
@@ -399,8 +726,13 @@ lazy_static::lazy_static! {
("ymail.com", &*P_YAHOO),
("rocketmail.com", &*P_YAHOO),
("yahoodns.net", &*P_YAHOO),
("yandex.ru", &*P_YANDEX_RU),
("yandex.com", &*P_YANDEX_RU),
("yandex.by", &*P_YANDEX_RU),
("yandex.kz", &*P_YANDEX_RU),
("yandex.ru", &*P_YANDEX_RU),
("yandex.ua", &*P_YANDEX_RU),
("ya.ru", &*P_YANDEX_RU),
("narod.ru", &*P_YANDEX_RU),
("ziggo.nl", &*P_ZIGGO_NL),
].iter().copied().collect();
}

View File

@@ -35,6 +35,13 @@ pub enum UsernamePattern {
EMAILLOCALPART = 2,
}
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum Oauth2Authorizer {
Yandex = 1,
Gmail = 2,
}
#[derive(Debug)]
pub struct Server {
pub protocol: Protocol,
@@ -72,6 +79,8 @@ pub struct Provider {
pub overview_page: &'static str,
pub server: Vec<Server>,
pub config_defaults: Option<Vec<ConfigDefault>>,
pub strict_tls: bool,
pub oauth2_authorizer: Option<Oauth2Authorizer>,
}
impl Provider {

View File

@@ -100,6 +100,12 @@ def process_data(data, file):
config_defaults = process_config_defaults(data)
strict_tls = data.get("strict_tls", False)
strict_tls = "true" if strict_tls else "false"
oauth2 = data.get("oauth2", "")
oauth2 = "Some(Oauth2Authorizer::" + camel(oauth2) + ")" if oauth2 != "" else "None"
provider = ""
before_login_hint = cleanstr(data.get("before_login_hint", ""))
after_login_hint = cleanstr(data.get("after_login_hint", ""))
@@ -111,6 +117,8 @@ def process_data(data, file):
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")
@@ -121,11 +129,11 @@ def process_data(data, file):
# finally, add the provider
global out_all, out_domains
out_all += " // " + file[file.rindex("/")+1:] + ": " + comment.strip(", ") + "\n"
if status == "OK" and before_login_hint == "" and after_login_hint == "" and server == "" and config_defaults == "None":
out_all += " // - skipping provider with status OK and no special things to do\n\n"
else:
out_all += provider
out_domains += domains
# 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
out_all += provider
out_domains += domains
def process_file(file):

View File

@@ -10,8 +10,7 @@ use crate::constants::Blocked;
use crate::contact::*;
use crate::context::Context;
use crate::error::{bail, ensure, format_err, Error};
use crate::key::dc_format_fingerprint;
use crate::key::dc_normalize_fingerprint;
use crate::key::Fingerprint;
use crate::lot::{Lot, LotState};
use crate::param::*;
use crate::peerstate::*;
@@ -80,6 +79,14 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
Some(pair) => pair,
None => (payload, ""),
};
let fingerprint: Fingerprint = match fingerprint.parse() {
Ok(fp) => fp,
Err(err) => {
return Error::new(err)
.context("Failed to parse fingerprint in QR code")
.into()
}
};
// replace & with \n to match expected param format
let fragment = fragment.replace('&', "\n");
@@ -128,20 +135,13 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
None
};
let fingerprint = dc_normalize_fingerprint(fingerprint);
// ensure valid fingerprint
if fingerprint.len() != 40 {
return format_err!("Bad fingerprint length in QR code").into();
}
let mut lot = Lot::new();
// retrieve known state for this fingerprint
let peerstate = Peerstate::from_fingerprint(context, &context.sql, &fingerprint).await;
let peerstate = Peerstate::from_fingerprint(context, &fingerprint).await;
if invitenumber.is_none() || auth.is_none() {
if let Some(peerstate) = peerstate {
if let Ok(peerstate) = peerstate {
lot.state = LotState::QrFprOk;
lot.id = Contact::add_or_lookup(
@@ -161,7 +161,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
chat::add_info_msg(context, id, format!("{} verified.", peerstate.addr)).await;
} else {
lot.state = LotState::QrFprWithoutAddr;
lot.text1 = Some(dc_format_fingerprint(&fingerprint));
lot.text1 = Some(fingerprint.to_string());
}
} else if let Some(addr) = addr {
if grpid.is_some() && grpname.is_some() {
@@ -383,11 +383,11 @@ fn normalize_address(addr: &str) -> Result<String, Error> {
mod tests {
use super::*;
use crate::test_utils::dummy_context;
use crate::test_utils::TestContext;
#[async_std::test]
async fn test_decode_http() {
let ctx = dummy_context().await;
let ctx = TestContext::new().await;
let res = check_qr(&ctx.ctx, "http://www.hello.com").await;
@@ -399,7 +399,7 @@ mod tests {
#[async_std::test]
async fn test_decode_https() {
let ctx = dummy_context().await;
let ctx = TestContext::new().await;
let res = check_qr(&ctx.ctx, "https://www.hello.com").await;
@@ -411,7 +411,7 @@ mod tests {
#[async_std::test]
async fn test_decode_text() {
let ctx = dummy_context().await;
let ctx = TestContext::new().await;
let res = check_qr(&ctx.ctx, "I am so cool").await;
@@ -423,7 +423,7 @@ mod tests {
#[async_std::test]
async fn test_decode_vcard() {
let ctx = dummy_context().await;
let ctx = TestContext::new().await;
let res = check_qr(
&ctx.ctx,
@@ -441,7 +441,7 @@ mod tests {
#[async_std::test]
async fn test_decode_matmsg() {
let ctx = dummy_context().await;
let ctx = TestContext::new().await;
let res = check_qr(
&ctx.ctx,
@@ -459,7 +459,7 @@ mod tests {
#[async_std::test]
async fn test_decode_mailto() {
let ctx = dummy_context().await;
let ctx = TestContext::new().await;
let res = check_qr(
&ctx.ctx,
@@ -485,7 +485,7 @@ mod tests {
#[async_std::test]
async fn test_decode_smtp() {
let ctx = dummy_context().await;
let ctx = TestContext::new().await;
let res = check_qr(&ctx.ctx, "SMTP:stress@test.local:subjecthello:bodyworld").await;
@@ -499,7 +499,7 @@ mod tests {
#[async_std::test]
async fn test_decode_openpgp_group() {
let ctx = dummy_context().await;
let ctx = TestContext::new().await;
let res = check_qr(
&ctx.ctx,
@@ -528,7 +528,7 @@ mod tests {
#[async_std::test]
async fn test_decode_openpgp_secure_join() {
let ctx = dummy_context().await;
let ctx = TestContext::new().await;
let res = check_qr(
&ctx.ctx,
@@ -556,7 +556,7 @@ mod tests {
#[async_std::test]
async fn test_decode_openpgp_without_addr() {
let ctx = dummy_context().await;
let ctx = TestContext::new().await;
let res = check_qr(
&ctx.ctx,
@@ -591,7 +591,7 @@ mod tests {
#[async_std::test]
async fn test_decode_account() {
let ctx = dummy_context().await;
let ctx = TestContext::new().await;
let res = check_qr(
&ctx.ctx,
@@ -613,7 +613,7 @@ mod tests {
#[async_std::test]
async fn test_decode_account_bad_scheme() {
let ctx = dummy_context().await;
let ctx = TestContext::new().await;
let res = check_qr(
&ctx.ctx,
"DCACCOUNT:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",

View File

@@ -1,3 +1,5 @@
#![warn(clippy::indexing_slicing)]
use async_std::prelude::*;
use async_std::sync::{channel, Receiver, Sender};
use async_std::task;
@@ -5,7 +7,7 @@ use async_std::task;
use crate::context::Context;
use crate::imap::Imap;
use crate::job::{self, Thread};
use crate::smtp::Smtp;
use crate::{config::Config, message::MsgId, smtp::Smtp};
pub(crate) struct StopToken;
@@ -32,36 +34,12 @@ impl Context {
self.scheduler.read().await.maybe_network().await;
}
pub(crate) async fn interrupt_inbox(&self, probe_network: bool) {
self.scheduler
.read()
.await
.interrupt_inbox(probe_network)
.await;
pub(crate) async fn interrupt_inbox(&self, info: InterruptInfo) {
self.scheduler.read().await.interrupt_inbox(info).await;
}
pub(crate) async fn interrupt_sentbox(&self, probe_network: bool) {
self.scheduler
.read()
.await
.interrupt_sentbox(probe_network)
.await;
}
pub(crate) async fn interrupt_mvbox(&self, probe_network: bool) {
self.scheduler
.read()
.await
.interrupt_mvbox(probe_network)
.await;
}
pub(crate) async fn interrupt_smtp(&self, probe_network: bool) {
self.scheduler
.read()
.await
.interrupt_smtp(probe_network)
.await;
pub(crate) async fn interrupt_smtp(&self, info: InterruptInfo) {
self.scheduler.read().await.interrupt_smtp(info).await;
}
}
@@ -79,20 +57,16 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
let fut = async move {
started.send(()).await;
let ctx = ctx1;
if let Err(err) = connection.connect_configured(&ctx).await {
error!(ctx, "{}", err);
return;
}
// track number of continously executed jobs
let mut jobs_loaded = 0;
let mut probe_network = false;
let mut info = InterruptInfo::default();
loop {
match job::load_next(&ctx, Thread::Imap, probe_network).await {
match job::load_next(&ctx, Thread::Imap, &info).await {
Some(job) if jobs_loaded <= 20 => {
jobs_loaded += 1;
job::perform_job(&ctx, job::Connection::Inbox(&mut connection), job).await;
probe_network = false;
info = Default::default();
}
Some(job) => {
// Let the fetch run, but return back to the job afterwards.
@@ -102,7 +76,11 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
}
None => {
jobs_loaded = 0;
probe_network = fetch_idle(&ctx, &mut connection).await;
info = if ctx.get_config_bool(Config::InboxWatch).await {
fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await
} else {
connection.fake_idle(&ctx, None).await
};
}
}
}
@@ -119,15 +97,18 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
}
async fn fetch(ctx: &Context, connection: &mut Imap) {
match get_watch_folder(&ctx, "configured_inbox_folder").await {
match ctx.get_config(Config::ConfiguredInboxFolder).await {
Some(watch_folder) => {
if let Err(err) = connection.connect_configured(&ctx).await {
error!(ctx, "{}", err);
return;
}
// fetch
connection
.fetch(&ctx, &watch_folder)
.await
.unwrap_or_else(|err| {
error!(ctx, "{}", err);
});
if let Err(err) = connection.fetch(&ctx, &watch_folder).await {
connection.trigger_reconnect();
warn!(ctx, "{}", err);
}
}
None => {
warn!(ctx, "Can not fetch inbox folder, not set");
@@ -136,16 +117,20 @@ async fn fetch(ctx: &Context, connection: &mut Imap) {
}
}
async fn fetch_idle(ctx: &Context, connection: &mut Imap) -> bool {
match get_watch_folder(&ctx, "configured_inbox_folder").await {
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> InterruptInfo {
match ctx.get_config(folder).await {
Some(watch_folder) => {
// connect and fake idle if unable to connect
if let Err(err) = connection.connect_configured(&ctx).await {
error!(ctx, "imap connection failed: {}", err);
return connection.fake_idle(&ctx, None).await;
}
// fetch
connection
.fetch(&ctx, &watch_folder)
.await
.unwrap_or_else(|err| {
error!(ctx, "{}", err);
});
if let Err(err) = connection.fetch(&ctx, &watch_folder).await {
connection.trigger_reconnect();
warn!(ctx, "{}", err);
}
// idle
if connection.can_idle() {
@@ -153,15 +138,16 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap) -> bool {
.idle(&ctx, Some(watch_folder))
.await
.unwrap_or_else(|err| {
error!(ctx, "{}", err);
false
connection.trigger_reconnect();
warn!(ctx, "{}", err);
InterruptInfo::new(false, None)
})
} else {
connection.fake_idle(&ctx, Some(watch_folder)).await
}
}
None => {
warn!(ctx, "Can not watch inbox folder, not set");
warn!(ctx, "Can not watch {} folder, not set", folder);
connection.fake_idle(&ctx, None).await
}
}
@@ -171,7 +157,7 @@ async fn simple_imap_loop(
ctx: Context,
started: Sender<()>,
inbox_handlers: ImapConnectionHandlers,
folder: impl AsRef<str>,
folder: Config,
) {
use futures::future::FutureExt;
@@ -187,44 +173,9 @@ async fn simple_imap_loop(
let fut = async move {
started.send(()).await;
let ctx = ctx1;
if let Err(err) = connection.connect_configured(&ctx).await {
error!(ctx, "{}", err);
return;
}
loop {
match get_watch_folder(&ctx, folder.as_ref()).await {
Some(watch_folder) => {
// fetch
connection
.fetch(&ctx, &watch_folder)
.await
.unwrap_or_else(|err| {
error!(ctx, "{}", err);
});
// idle
if connection.can_idle() {
connection
.idle(&ctx, Some(watch_folder))
.await
.unwrap_or_else(|err| {
error!(ctx, "{}", err);
false
});
} else {
connection.fake_idle(&ctx, Some(watch_folder)).await;
}
}
None => {
warn!(
&ctx,
"No watch folder found for {}, skipping",
folder.as_ref()
);
connection.fake_idle(&ctx, None).await;
}
}
fetch_idle(&ctx, &mut connection, folder).await;
}
};
@@ -254,18 +205,18 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
started.send(()).await;
let ctx = ctx1;
let mut probe_network = false;
let mut interrupt_info = Default::default();
loop {
match job::load_next(&ctx, Thread::Smtp, probe_network).await {
match job::load_next(&ctx, Thread::Smtp, &interrupt_info).await {
Some(job) => {
info!(ctx, "executing smtp job");
job::perform_job(&ctx, job::Connection::Smtp(&mut connection), job).await;
probe_network = false;
interrupt_info = Default::default();
}
None => {
// Fake Idle
info!(ctx, "smtp fake idle - started");
probe_network = idle_interrupt_receiver.recv().await.unwrap_or_default();
interrupt_info = idle_interrupt_receiver.recv().await.unwrap_or_default();
info!(ctx, "smtp fake idle - interrupted")
}
}
@@ -290,61 +241,64 @@ impl Scheduler {
let (smtp, smtp_handlers) = SmtpConnectionState::new();
let (inbox, inbox_handlers) = ImapConnectionState::new();
let (inbox_start_send, inbox_start_recv) = channel(1);
let (mvbox_start_send, mvbox_start_recv) = channel(1);
let mut mvbox_handle = None;
let (sentbox_start_send, sentbox_start_recv) = channel(1);
let mut sentbox_handle = None;
let (smtp_start_send, smtp_start_recv) = channel(1);
let ctx1 = ctx.clone();
let inbox_handle = Some(task::spawn(async move {
inbox_loop(ctx1, inbox_start_send, inbox_handlers).await
}));
if ctx.get_config_bool(Config::MvboxWatch).await {
let ctx1 = ctx.clone();
mvbox_handle = Some(task::spawn(async move {
simple_imap_loop(
ctx1,
mvbox_start_send,
mvbox_handlers,
Config::ConfiguredMvboxFolder,
)
.await
}));
} else {
mvbox_start_send.send(()).await;
}
if ctx.get_config_bool(Config::SentboxWatch).await {
let ctx1 = ctx.clone();
sentbox_handle = Some(task::spawn(async move {
simple_imap_loop(
ctx1,
sentbox_start_send,
sentbox_handlers,
Config::ConfiguredSentboxFolder,
)
.await
}));
} else {
sentbox_start_send.send(()).await;
}
let ctx1 = ctx.clone();
let smtp_handle = Some(task::spawn(async move {
smtp_loop(ctx1, smtp_start_send, smtp_handlers).await
}));
*self = Scheduler::Running {
inbox,
mvbox,
sentbox,
smtp,
inbox_handle: None,
mvbox_handle: None,
sentbox_handle: None,
smtp_handle: None,
inbox_handle,
mvbox_handle,
sentbox_handle,
smtp_handle,
};
let (inbox_start_send, inbox_start_recv) = channel(1);
if let Scheduler::Running { inbox_handle, .. } = self {
let ctx1 = ctx.clone();
*inbox_handle = Some(task::spawn(async move {
inbox_loop(ctx1, inbox_start_send, inbox_handlers).await
}));
}
let (mvbox_start_send, mvbox_start_recv) = channel(1);
if let Scheduler::Running { mvbox_handle, .. } = self {
let ctx1 = ctx.clone();
*mvbox_handle = Some(task::spawn(async move {
simple_imap_loop(
ctx1,
mvbox_start_send,
mvbox_handlers,
"configured_mvbox_folder",
)
.await
}));
}
let (sentbox_start_send, sentbox_start_recv) = channel(1);
if let Scheduler::Running { sentbox_handle, .. } = self {
let ctx1 = ctx.clone();
*sentbox_handle = Some(task::spawn(async move {
simple_imap_loop(
ctx1,
sentbox_start_send,
sentbox_handlers,
"configured_sentbox_folder",
)
.await
}));
}
let (smtp_start_send, smtp_start_recv) = channel(1);
if let Scheduler::Running { smtp_handle, .. } = self {
let ctx1 = ctx.clone();
*smtp_handle = Some(task::spawn(async move {
smtp_loop(ctx1, smtp_start_send, smtp_handlers).await
}));
}
// wait for all loops to be started
if let Err(err) = inbox_start_recv
.recv()
@@ -364,34 +318,34 @@ impl Scheduler {
return;
}
self.interrupt_inbox(true)
.join(self.interrupt_mvbox(true))
.join(self.interrupt_sentbox(true))
.join(self.interrupt_smtp(true))
self.interrupt_inbox(InterruptInfo::new(true, None))
.join(self.interrupt_mvbox(InterruptInfo::new(true, None)))
.join(self.interrupt_sentbox(InterruptInfo::new(true, None)))
.join(self.interrupt_smtp(InterruptInfo::new(true, None)))
.await;
}
async fn interrupt_inbox(&self, probe_network: bool) {
async fn interrupt_inbox(&self, info: InterruptInfo) {
if let Scheduler::Running { ref inbox, .. } = self {
inbox.interrupt(probe_network).await;
inbox.interrupt(info).await;
}
}
async fn interrupt_mvbox(&self, probe_network: bool) {
async fn interrupt_mvbox(&self, info: InterruptInfo) {
if let Scheduler::Running { ref mvbox, .. } = self {
mvbox.interrupt(probe_network).await;
mvbox.interrupt(info).await;
}
}
async fn interrupt_sentbox(&self, probe_network: bool) {
async fn interrupt_sentbox(&self, info: InterruptInfo) {
if let Scheduler::Running { ref sentbox, .. } = self {
sentbox.interrupt(probe_network).await;
sentbox.interrupt(info).await;
}
}
async fn interrupt_smtp(&self, probe_network: bool) {
async fn interrupt_smtp(&self, info: InterruptInfo) {
if let Scheduler::Running { ref smtp, .. } = self {
smtp.interrupt(probe_network).await;
smtp.interrupt(info).await;
}
}
@@ -433,10 +387,18 @@ impl Scheduler {
smtp_handle,
..
} => {
inbox_handle.take().expect("inbox not started").await;
mvbox_handle.take().expect("mvbox not started").await;
sentbox_handle.take().expect("sentbox not started").await;
smtp_handle.take().expect("smtp not started").await;
if let Some(handle) = inbox_handle.take() {
handle.await;
}
if let Some(handle) = mvbox_handle.take() {
handle.await;
}
if let Some(handle) = sentbox_handle.take() {
handle.await;
}
if let Some(handle) = smtp_handle.take() {
handle.await;
}
*self = Scheduler::Stopped;
}
@@ -460,7 +422,7 @@ struct ConnectionState {
/// Channel to interrupt the whole connection.
stop_sender: Sender<()>,
/// Channel to interrupt idle.
idle_interrupt_sender: Sender<bool>,
idle_interrupt_sender: Sender<InterruptInfo>,
}
impl ConnectionState {
@@ -472,9 +434,9 @@ impl ConnectionState {
self.shutdown_receiver.recv().await.ok();
}
async fn interrupt(&self, probe_network: bool) {
async fn interrupt(&self, info: InterruptInfo) {
// Use try_send to avoid blocking on interrupts.
self.idle_interrupt_sender.try_send(probe_network).ok();
self.idle_interrupt_sender.try_send(info).ok();
}
}
@@ -508,8 +470,8 @@ impl SmtpConnectionState {
}
/// Interrupt any form of idle.
async fn interrupt(&self, probe_network: bool) {
self.state.interrupt(probe_network).await;
async fn interrupt(&self, info: InterruptInfo) {
self.state.interrupt(info).await;
}
/// Shutdown this connection completely.
@@ -518,12 +480,11 @@ impl SmtpConnectionState {
}
}
#[derive(Debug)]
struct SmtpConnectionHandlers {
connection: Smtp,
stop_receiver: Receiver<()>,
shutdown_sender: Sender<()>,
idle_interrupt_receiver: Receiver<bool>,
idle_interrupt_receiver: Receiver<InterruptInfo>,
}
#[derive(Debug)]
@@ -556,8 +517,8 @@ impl ImapConnectionState {
}
/// Interrupt any form of idle.
async fn interrupt(&self, probe_network: bool) {
self.state.interrupt(probe_network).await;
async fn interrupt(&self, info: InterruptInfo) {
self.state.interrupt(info).await;
}
/// Shutdown this connection completely.
@@ -573,20 +534,17 @@ struct ImapConnectionHandlers {
shutdown_sender: Sender<()>,
}
async fn get_watch_folder(context: &Context, config_name: impl AsRef<str>) -> Option<String> {
match context
.sql
.get_raw_config(context, config_name.as_ref())
.await
{
Some(name) => Some(name),
None => {
if config_name.as_ref() == "configured_inbox_folder" {
// initialized with old version, so has not set configured_inbox_folder
Some("INBOX".to_string())
} else {
None
}
#[derive(Default, Debug)]
pub struct InterruptInfo {
pub probe_network: bool,
pub msg_id: Option<MsgId>,
}
impl InterruptInfo {
pub fn new(probe_network: bool, msg_id: Option<MsgId>) -> Self {
Self {
probe_network,
msg_id,
}
}
}

View File

@@ -14,7 +14,7 @@ use crate::e2ee::*;
use crate::error::{bail, Error};
use crate::events::Event;
use crate::headerdef::HeaderDef;
use crate::key::{dc_normalize_fingerprint, DcKey, Key, SignedPublicKey};
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::lot::LotState;
use crate::message::Message;
use crate::mimeparser::*;
@@ -73,8 +73,6 @@ pub async fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> O
==== Step 1 in "Setup verified contact" protocol ====
=======================================================*/
let fingerprint: String;
ensure_secret_key_exists(context).await.ok();
// invitenumber will be used to allow starting the handshake,
@@ -95,7 +93,7 @@ pub async fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> O
.await
.unwrap_or_default();
fingerprint = match get_self_fingerprint(context).await {
let fingerprint: Fingerprint = match get_self_fingerprint(context).await {
Some(fp) => fp,
None => {
return None;
@@ -116,7 +114,7 @@ pub async fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> O
Some(format!(
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
fingerprint,
fingerprint.hex(),
self_addr_urlencoded,
&group_name_urlencoded,
&chat.grpid,
@@ -131,7 +129,11 @@ pub async fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> O
// parameters used: a=n=i=s=
Some(format!(
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
fingerprint, self_addr_urlencoded, self_name_urlencoded, &invitenumber, &auth,
fingerprint.hex(),
self_addr_urlencoded,
self_name_urlencoded,
&invitenumber,
&auth,
))
};
@@ -140,9 +142,9 @@ pub async fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> O
qr
}
async fn get_self_fingerprint(context: &Context) -> Option<String> {
async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
match SignedPublicKey::load_self(context).await {
Ok(key) => Some(Key::from(key).fingerprint()),
Ok(key) => Some(key.fingerprint()),
Err(_) => {
warn!(context, "get_self_fingerprint(): failed to load key");
None
@@ -150,81 +152,69 @@ async fn get_self_fingerprint(context: &Context) -> Option<String> {
}
}
async fn cleanup(
context: &Context,
contact_chat_id: ChatId,
ongoing_allocated: bool,
join_vg: bool,
) -> ChatId {
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.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 {
if context.alloc_ongoing().await.is_err() {
return cleanup(&context, ChatId::new(0), false, false).await;
if let Err(err) = context.alloc_ongoing().await {
error!(context, "SecureJoin Error: {}", err);
return ChatId::new(0);
}
securejoin(context, qr).await
match securejoin(context, qr).await {
Ok(id) => id,
Err(err) => {
error!(context, "SecureJoin Error: {}", err);
ChatId::new(0)
}
}
}
async fn securejoin(context: &Context, qr: &str) -> ChatId {
/*========================================================
==== Bob - the joiner's side =====
==== Step 2 in "Setup verified contact" protocol =====
========================================================*/
/// Bob - the joiner's side
/// Step 2 in "Setup verified contact" protocol
async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, anyhow::Error> {
struct DropGuard<'a> {
context: &'a Context,
}
let mut contact_chat_id = ChatId::new(0);
let mut join_vg: bool = false;
impl Drop for DropGuard<'_> {
fn drop(&mut self) {
async_std::task::block_on(async {
let mut bob = self.context.bob.write().await;
bob.expects = 0;
bob.qr_scan = None;
self.context.free_ongoing().await;
});
}
}
info!(context, "Requesting secure-join ...",);
let _guard = DropGuard { context: &context };
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;
bail!("Unknown QR code.");
}
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;
bail!("Unknown contact.");
}
};
if context.shall_stop_ongoing().await {
return cleanup(&context, contact_chat_id, true, join_vg).await;
bail!("Interrupted");
}
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(
let fp_equals_sender = fingerprint_equals_sender(
context,
context
.bob
@@ -238,21 +228,22 @@ async fn securejoin(context: &Context, qr: &str) -> ChatId {
.unwrap(),
contact_chat_id,
)
.await
{
.await?;
if fp_equals_sender {
// 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;
joiner_progress!(
context,
chat_id_2_contact_id(context, contact_chat_id).await,
chat_id_2_contact_id(context, contact_chat_id).await?,
400
);
let own_fingerprint = get_self_fingerprint(context).await.unwrap_or_default();
let own_fingerprint = get_self_fingerprint(context).await;
// Bob -> Alice
if let Err(err) = send_handshake_msg(
send_handshake_msg(
context,
contact_chat_id,
if join_vg {
@@ -261,23 +252,19 @@ async fn securejoin(context: &Context, qr: &str) -> ChatId {
"vc-request-with-auth"
},
get_qr_attr!(context, auth).to_string(),
Some(own_fingerprint),
own_fingerprint,
if join_vg {
get_qr_attr!(context, text2).to_string()
} else {
"".to_string()
},
)
.await
{
error!(context, "failed to send handshake message: {}", err);
return cleanup(&context, contact_chat_id, true, join_vg).await;
}
.await?;
} else {
context.bob.write().await.expects = DC_VC_AUTH_REQUIRED;
// Bob -> Alice
if let Err(err) = send_handshake_msg(
send_handshake_msg(
context,
contact_chat_id,
if join_vg { "vg-request" } else { "vc-request" },
@@ -285,11 +272,7 @@ async fn securejoin(context: &Context, qr: &str) -> ChatId {
None,
"",
)
.await
{
error!(context, "failed to send handshake message: {}", err);
return cleanup(&context, contact_chat_id, true, join_vg).await;
}
.await?;
}
if join_vg {
@@ -297,12 +280,23 @@ 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
let bob = context.bob.read().await;
if bob.status == DC_BOB_SUCCESS {
let id = chat::get_chat_id_by_grpid(
context,
bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(),
)
.await?
.0;
Ok(id)
} else {
bail!("Failed to join");
}
} 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)
}
}
@@ -311,7 +305,7 @@ async fn send_handshake_msg(
contact_chat_id: ChatId,
step: &str,
param2: impl AsRef<str>,
fingerprint: Option<String>,
fingerprint: Option<Fingerprint>,
grpid: impl AsRef<str>,
) -> Result<(), HandshakeError> {
let mut msg = Message::default();
@@ -328,7 +322,7 @@ async fn send_handshake_msg(
msg.param.set(Param::Arg2, param2);
}
if let Some(fp) = fingerprint {
msg.param.set(Param::Arg3, fp);
msg.param.set(Param::Arg3, fp.hex());
}
if !grpid.as_ref().is_empty() {
msg.param.set(Param::Arg4, grpid.as_ref());
@@ -349,36 +343,37 @@ async fn send_handshake_msg(
Ok(())
}
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 {
let contacts = chat::get_chat_contacts(context, contact_chat_id).await;
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> Result<u32, Error> {
let contacts = chat::get_chat_contacts(context, contact_chat_id).await?;
if contacts.len() == 1 {
contacts[0]
Ok(contacts[0])
} else {
0
Ok(0)
}
}
async fn fingerprint_equals_sender(
context: &Context,
fingerprint: impl AsRef<str>,
fingerprint: &Fingerprint,
contact_chat_id: ChatId,
) -> bool {
let contacts = chat::get_chat_contacts(context, contact_chat_id).await;
) -> Result<bool, Error> {
let contacts = chat::get_chat_contacts(context, contact_chat_id).await?;
if contacts.len() == 1 {
if let Ok(contact) = Contact::load_from_db(context, contacts[0]).await {
if let Some(peerstate) = Peerstate::from_addr(context, contact.get_addr()).await {
let fingerprint_normalized = dc_normalize_fingerprint(fingerprint.as_ref());
if let Ok(peerstate) = Peerstate::from_addr(context, contact.get_addr()).await {
if peerstate.public_key_fingerprint.is_some()
&& &fingerprint_normalized == peerstate.public_key_fingerprint.as_ref().unwrap()
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
{
return true;
return Ok(true);
}
}
}
}
false
Ok(false)
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum HandshakeError {
#[error("Can not be called with special contact ID")]
@@ -397,6 +392,10 @@ pub(crate) enum HandshakeError {
NoSelfAddr,
#[error("Failed to send message")]
MsgSendFailed(#[source] Error),
#[error("Failed to parse fingerprint")]
BadFingerprint(#[from] crate::key::FingerprintError),
#[error("{0}")]
Other(#[from] Error),
}
/// What to do with a Secure-Join handshake message after it was handled.
@@ -516,10 +515,11 @@ pub(crate) async fn handle_securejoin_handshake(
// no error, just aborted somehow or a mail from another handshake
return Ok(HandshakeMessage::Ignore);
}
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
let scanned_fingerprint_of_alice: Fingerprint =
get_qr_attr!(context, fingerprint).clone();
let auth = get_qr_attr!(context, auth).to_string();
if !encrypted_and_signed(context, mime_message, &scanned_fingerprint_of_alice) {
if !encrypted_and_signed(context, mime_message, Some(&scanned_fingerprint_of_alice)) {
could_not_establish_secure_connection(
context,
contact_chat_id,
@@ -529,20 +529,20 @@ pub(crate) async fn handle_securejoin_handshake(
"Not encrypted."
},
)
.await;
.await?;
context.bob.write().await.status = 0; // secure-join failed
context.stop_ongoing().await;
return Ok(HandshakeMessage::Ignore);
}
if !fingerprint_equals_sender(context, &scanned_fingerprint_of_alice, contact_chat_id)
.await
.await?
{
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint mismatch on joiner-side.",
)
.await;
.await?;
context.bob.write().await.status = 0; // secure-join failed
context.stop_ongoing().await;
return Ok(HandshakeMessage::Ignore);
@@ -576,34 +576,35 @@ pub(crate) async fn handle_securejoin_handshake(
==========================================================*/
// verify that Secure-Join-Fingerprint:-header matches the fingerprint of Bob
let fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint) {
Some(fp) => fp,
let fingerprint: Fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint)
{
Some(fp) => fp.parse()?,
None => {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint not provided.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
};
if !encrypted_and_signed(context, mime_message, &fingerprint) {
if !encrypted_and_signed(context, mime_message, Some(&fingerprint)) {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Auth not encrypted.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await {
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await? {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint mismatch on inviter-side.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
info!(context, "Fingerprint verified.",);
@@ -616,27 +617,27 @@ pub(crate) async fn handle_securejoin_handshake(
contact_chat_id,
"Auth not provided.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
};
if !token::exists(context, token::Namespace::Auth, &auth_0).await {
could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.")
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
if mark_peer_as_verified(context, fingerprint).await.is_err() {
if mark_peer_as_verified(context, &fingerprint).await.is_err() {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint mismatch on inviter-side.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await;
info!(context, "Auth verified.",);
secure_connection_established(context, contact_chat_id).await;
secure_connection_established(context, contact_chat_id).await?;
emit_event!(context, Event::ContactsChanged(Some(contact_id)));
inviter_progress!(context, contact_id, 600);
if join_vg {
@@ -673,7 +674,7 @@ pub(crate) async fn handle_securejoin_handshake(
contact_chat_id,
"vc-contact-confirm",
"",
Some(fingerprint.clone()),
Some(fingerprint),
"",
)
.await?;
@@ -709,7 +710,8 @@ pub(crate) async fn handle_securejoin_handshake(
);
return Ok(abort_retval);
}
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
let scanned_fingerprint_of_alice: Fingerprint =
get_qr_attr!(context, fingerprint).clone();
let vg_expect_encrypted = if join_vg {
let group_id = get_qr_attr!(context, text2).to_string();
@@ -731,14 +733,14 @@ pub(crate) async fn handle_securejoin_handshake(
true
};
if vg_expect_encrypted
&& !encrypted_and_signed(context, mime_message, &scanned_fingerprint_of_alice)
&& !encrypted_and_signed(context, mime_message, Some(&scanned_fingerprint_of_alice))
{
could_not_establish_secure_connection(
context,
contact_chat_id,
"Contact confirm message not encrypted.",
)
.await;
.await?;
context.bob.write().await.status = 0;
return Ok(abort_retval);
}
@@ -752,7 +754,7 @@ pub(crate) async fn handle_securejoin_handshake(
contact_chat_id,
"Fingerprint mismatch on joiner-side.",
)
.await;
.await?;
return Ok(abort_retval);
}
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinJoined).await;
@@ -770,7 +772,7 @@ pub(crate) async fn handle_securejoin_handshake(
info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group).");
return Ok(abort_retval);
}
secure_connection_established(context, contact_chat_id).await;
secure_connection_established(context, contact_chat_id).await?;
context.bob.write().await.expects = 0;
// Bob -> Alice
@@ -888,35 +890,36 @@ pub(crate) async fn observe_securejoin_on_other_device(
if !encrypted_and_signed(
context,
mime_message,
get_self_fingerprint(context).await.unwrap_or_default(),
get_self_fingerprint(context).await.as_ref(),
) {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Message not encrypted correctly.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
let fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint) {
Some(fp) => fp,
let fingerprint: Fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint)
{
Some(fp) => fp.parse()?,
None => {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint not provided, please update Delta Chat on all your devices.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
};
if mark_peer_as_verified(context, fingerprint).await.is_err() {
if mark_peer_as_verified(context, &fingerprint).await.is_err() {
could_not_establish_secure_connection(
context,
contact_chat_id,
format!("Fingerprint mismatch on observing {}.", step).as_ref(),
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
Ok(if step.as_str() == "vg-member-added" {
@@ -929,8 +932,11 @@ pub(crate) async fn observe_securejoin_on_other_device(
}
}
async fn secure_connection_established(context: &Context, contact_chat_id: ChatId) {
let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id).await;
async fn secure_connection_established(
context: &Context,
contact_chat_id: ChatId,
) -> Result<(), HandshakeError> {
let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id).await?;
let contact = Contact::get_by_id(context, contact_id).await;
let addr = if let Ok(ref contact) = contact {
@@ -943,14 +949,16 @@ async fn secure_connection_established(context: &Context, contact_chat_id: ChatI
.await;
chat::add_info_msg(context, contact_chat_id, msg).await;
emit_event!(context, Event::ChatModified(contact_chat_id));
Ok(())
}
async fn could_not_establish_secure_connection(
context: &Context,
contact_chat_id: ChatId,
details: &str,
) {
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await;
) -> Result<(), HandshakeError> {
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?;
let contact = Contact::get_by_id(context, contact_id).await;
let msg = context
.stock_string_repl_str(
@@ -965,18 +973,14 @@ async fn could_not_establish_secure_connection(
chat::add_info_msg(context, contact_chat_id, &msg).await;
error!(context, "{} ({})", &msg, details);
Ok(())
}
async fn mark_peer_as_verified(
context: &Context,
fingerprint: impl AsRef<str>,
) -> Result<(), Error> {
if let Some(ref mut peerstate) =
Peerstate::from_fingerprint(context, &context.sql, fingerprint.as_ref()).await
{
async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) -> Result<(), Error> {
if let Ok(ref mut peerstate) = Peerstate::from_fingerprint(context, fingerprint).await {
if peerstate.set_verified(
PeerstateKeyType::PublicKey,
fingerprint.as_ref(),
fingerprint,
PeerstateVerifiedStatus::BidirectVerified,
) {
peerstate.prefer_encrypt = EncryptPreference::Mutual;
@@ -990,7 +994,7 @@ async fn mark_peer_as_verified(
}
bail!(
"could not mark peer as verified for fingerprint {}",
fingerprint.as_ref()
fingerprint.hex()
);
}
@@ -1001,7 +1005,7 @@ async fn mark_peer_as_verified(
fn encrypted_and_signed(
context: &Context,
mimeparser: &MimeMessage,
expected_fingerprint: impl AsRef<str>,
expected_fingerprint: Option<&Fingerprint>,
) -> bool {
if !mimeparser.was_encrypted() {
warn!(context, "Message not encrypted.",);
@@ -1009,17 +1013,17 @@ fn encrypted_and_signed(
} else if mimeparser.signatures.is_empty() {
warn!(context, "Message not signed.",);
false
} else if expected_fingerprint.as_ref().is_empty() {
warn!(context, "Fingerprint for comparison missing.",);
} else if expected_fingerprint.is_none() {
warn!(context, "Fingerprint for comparison missing.");
false
} else if !mimeparser
.signatures
.contains(expected_fingerprint.as_ref())
.contains(expected_fingerprint.unwrap())
{
warn!(
context,
"Message does not match expected fingerprint {}.",
expected_fingerprint.as_ref(),
expected_fingerprint.unwrap(),
);
false
} else {
@@ -1027,31 +1031,21 @@ fn encrypted_and_signed(
}
}
pub async fn handle_degrade_event(
context: &Context,
peerstate: &Peerstate<'_>,
) -> Result<(), Error> {
pub async fn handle_degrade_event(context: &Context, peerstate: &Peerstate) -> Result<(), Error> {
// - we do not issue an warning for DC_DE_ENCRYPTION_PAUSED as this is quite normal
// - currently, we do not issue an extra warning for DC_DE_VERIFICATION_LOST - this always comes
// together with DC_DE_FINGERPRINT_CHANGED which is logged, the idea is not to bother
// with things they cannot fix, so the user is just kicked from the verified group
// (and he will know this and can fix this)
if Some(DegradeEvent::FingerprintChanged) == peerstate.degrade_event {
let contact_id: i32 = match context
let contact_id: i32 = context
.sql
.query_get_value(
context,
.query_value(
"SELECT id FROM contacts WHERE addr=?;",
paramsv![peerstate.addr],
paramsx![&peerstate.addr],
)
.await
{
None => bail!(
"contact with peerstate.addr {:?} not found",
&peerstate.addr
),
Some(contact_id) => contact_id,
};
.await?;
if contact_id > 0 {
let (contact_chat_id, _) =
chat::create_or_lookup_by_contact_id(context, contact_id as u32, Blocked::Deaddrop)

View File

@@ -1,5 +1,7 @@
//! # SMTP transport module
#![forbid(clippy::indexing_slicing)]
pub mod send;
use std::time::{Duration, Instant};
@@ -10,8 +12,9 @@ use async_smtp::*;
use crate::constants::*;
use crate::context::Context;
use crate::events::Event;
use crate::login_param::{dc_build_tls, LoginParam};
use crate::login_param::{dc_build_tls, CertificateChecks, LoginParam};
use crate::oauth2::*;
use crate::provider::get_provider_info;
use crate::stock::StockMessage;
/// SMTP write and read timeout in seconds.
@@ -44,9 +47,8 @@ pub enum Error {
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Default, DebugStub)]
pub struct Smtp {
#[debug_stub(some = "SmtpTransport")]
#[derive(Default)]
pub(crate) struct Smtp {
transport: Option<smtp::SmtpTransport>,
/// Email address we are sending from.
@@ -113,7 +115,14 @@ impl Smtp {
let domain = &lp.send_server;
let port = lp.send_port as u16;
let tls_config = dc_build_tls(lp.smtp_certificate_checks);
let provider = get_provider_info(&lp.addr);
let strict_tls = match lp.smtp_certificate_checks {
CertificateChecks::Automatic => provider.map_or(false, |provider| provider.strict_tls),
CertificateChecks::Strict => true,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => false,
};
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) {
@@ -172,7 +181,7 @@ impl Smtp {
.stock_string_repl_str2(
StockMessage::ServerResponse,
format!("SMTP {}:{}", domain, port),
err.to_string(),
format!("{}, ({:?})", err.to_string(), err),
)
.await;

1323
src/sql.rs

File diff suppressed because it is too large Load Diff

13
src/sql/macros.rs Normal file
View File

@@ -0,0 +1,13 @@
#[macro_export]
macro_rules! paramsx {
() => {
sqlx::sqlite::SqliteArguments::default()
};
($($param:expr),+ $(,)?) => {{
use sqlx::Arguments;
let mut args = sqlx::sqlite::SqliteArguments::default();
$(args.add($param);)+
args
}};
}

378
src/sql/migrations.rs Normal file
View File

@@ -0,0 +1,378 @@
use super::{Error, Result, Sql};
use crate::constants::ShowEmails;
use crate::context::Context;
/// Executes all migrations required to get from the passed in `dbversion` to the latest.
pub async fn run(
context: &Context,
sql: &Sql,
dbversion: i32,
exists_before_update: bool,
) -> Result<()> {
let migrate = |version: i32, stmt: &'static str| async move {
if dbversion < version {
info!(context, "[migration] v{}", version);
sql.execute_batch(stmt).await?;
sql.set_raw_config_int(context, "dbversion", version)
.await?;
}
Ok::<_, Error>(())
};
migrate(
0,
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 '');
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 '');
CREATE INDEX chats_index1 ON chats (grpid);
CREATE TABLE chats_contacts (chat_id INTEGER, contact_id INTEGER);
CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);
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 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 '');
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);
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 '');
CREATE INDEX jobs_index1 ON jobs (desired_timestamp);
"#,
)
.await?;
migrate(
1,
r#"
CREATE TABLE leftgrps (
id INTEGER PRIMARY KEY,
grpid TEXT DEFAULT '');
CREATE INDEX leftgrps_index1 ON leftgrps (grpid);
"#,
)
.await?;
migrate(
2,
r#"
ALTER TABLE contacts ADD COLUMN authname TEXT DEFAULT '';
"#,
)
.await?;
migrate(
7,
r#"
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);
"#,
)
.await?;
migrate(
10,
r#"
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);
"#,
)
.await?;
migrate(
12,
r#"
CREATE TABLE msgs_mdns (
msg_id INTEGER,
contact_id INTEGER);
CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);
"#,
)
.await?;
migrate(
17,
r#"
ALTER TABLE chats ADD COLUMN archived INTEGER DEFAULT 0;
CREATE INDEX chats_index2 ON chats (archived);
ALTER TABLE msgs ADD COLUMN starred INTEGER DEFAULT 0;
CREATE INDEX msgs_index5 ON msgs (starred);
"#,
)
.await?;
migrate(
18,
r#"
ALTER TABLE acpeerstates ADD COLUMN gossip_timestamp INTEGER DEFAULT 0;
ALTER TABLE acpeerstates ADD COLUMN gossip_key;
"#,
)
.await?;
// chat.id=1 and chat.id=2 are the old deaddrops,
// the current ones are defined by chats.blocked=2
migrate(
27,
r#"
DELETE FROM msgs WHERE chat_id=1 OR chat_id=2;
CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);
ALTER TABLE msgs ADD COLUMN timestamp_sent INTEGER DEFAULT 0;
ALTER TABLE msgs ADD COLUMN timestamp_rcvd INTEGER DEFAULT 0;
"#,
)
.await?;
migrate(
34,
r#"
ALTER TABLE msgs ADD COLUMN hidden INTEGER DEFAULT 0;
ALTER TABLE msgs_mdns ADD COLUMN timestamp_sent INTEGER DEFAULT 0;
ALTER TABLE acpeerstates ADD COLUMN public_key_fingerprint TEXT DEFAULT '';
ALTER TABLE acpeerstates ADD COLUMN gossip_key_fingerprint TEXT DEFAULT '';
CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);
"#,
)
.await?;
migrate(
39,
r#"
CREATE TABLE tokens (
id INTEGER PRIMARY KEY,
namespc INTEGER DEFAULT 0,
foreign_id INTEGER DEFAULT 0,
token TEXT DEFAULT '',
timestamp INTEGER DEFAULT 0);
ALTER TABLE acpeerstates ADD COLUMN verified_key;
ALTER TABLE acpeerstates ADD COLUMN verified_key_fingerprint TEXT DEFAULT '';
CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);
"#,
)
.await?;
migrate(
40,
r#"
ALTER TABLE jobs ADD COLUMN thread INTEGER DEFAULT 0;
"#,
)
.await?;
migrate(
44,
r#"
ALTER TABLE msgs ADD COLUMN mime_headers TEXT;
"#,
)
.await?;
migrate(
46,
r#"
ALTER TABLE msgs ADD COLUMN mime_in_reply_to TEXT;
ALTER TABLE msgs ADD COLUMN mime_references TEXT;
"#,
)
.await?;
migrate(
47,
r#"
ALTER TABLE jobs ADD COLUMN tries INTEGER DEFAULT 0;
"#,
)
.await?;
// NOTE: move_state is not used anymore
migrate(
48,
r#"
ALTER TABLE msgs ADD COLUMN move_state INTEGER DEFAULT 1;
"#,
)
.await?;
migrate(
49,
r#"
ALTER TABLE chats ADD COLUMN gossiped_timestamp INTEGER DEFAULT 0;
"#,
)
.await?;
if dbversion < 50 {
info!(context, "[migration] v50");
// installations <= 0.100.1 used DC_SHOW_EMAILS_ALL implicitly;
// keep this default and use DC_SHOW_EMAILS_NO
// only for new installations
if exists_before_update {
sql.set_raw_config_int(context, "show_emails", ShowEmails::All as i32)
.await?;
}
sql.set_raw_config_int(context, "dbversion", 50).await?;
}
// the messages containing _only_ locations
// are also added to the database as _hidden_.
migrate(
53,
r#"
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);
CREATE INDEX locations_index1 ON locations (from_id);
CREATE INDEX locations_index2 ON locations (timestamp);
ALTER TABLE chats ADD COLUMN locations_send_begin INTEGER DEFAULT 0;
ALTER TABLE chats ADD COLUMN locations_send_until INTEGER DEFAULT 0;
ALTER TABLE chats ADD COLUMN locations_last_sent INTEGER DEFAULT 0;
CREATE INDEX chats_index3 ON chats (locations_send_until);
"#,
)
.await?;
migrate(
54,
r#"
ALTER TABLE msgs ADD COLUMN location_id INTEGER DEFAULT 0;
CREATE INDEX msgs_index6 ON msgs (location_id);
"#,
)
.await?;
migrate(
55,
r#"
ALTER TABLE locations ADD COLUMN independent INTEGER DEFAULT 0;
"#,
)
.await?;
migrate(
59,
r#"
CREATE TABLE devmsglabels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT,
msg_id INTEGER DEFAULT 0);
CREATE INDEX devmsglabels_index1 ON devmsglabels (label);
"#,
)
.await?;
// records in the devmsglabels are kept when the message is deleted.
// so, msg_id may or may not exist.
if dbversion < 59 && exists_before_update && sql.get_raw_config_int("bcc_self").await.is_none()
{
sql.set_raw_config_int(context, "bcc_self", 1).await?;
}
migrate(
60,
r#"
ALTER TABLE chats ADD COLUMN created_timestamp INTEGER DEFAULT 0;
"#,
)
.await?;
migrate(
61,
r#"
ALTER TABLE contacts ADD COLUMN selfavatar_sent INTEGER DEFAULT 0;
"#,
)
.await?;
migrate(
62,
r#"
ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;
"#,
)
.await?;
migrate(
63,
r#"
UPDATE chats SET grpid='' WHERE type=100;
"#,
)
.await?;
migrate(
64,
r#"
ALTER TABLE msgs ADD COLUMN error TEXT DEFAULT '';
"#,
)
.await?;
Ok(())
}

706
src/sql/mod.rs Normal file
View File

@@ -0,0 +1,706 @@
//! # SQLite wrapper
use std::collections::HashSet;
use std::path::Path;
use async_std::prelude::*;
use async_std::sync::RwLock;
use sqlx::sqlite::*;
use crate::chat::{update_device_icon, update_saved_messages_icon};
use crate::constants::DC_CHAT_ID_TRASH;
use crate::context::Context;
use crate::dc_tools::*;
use crate::param::*;
use crate::peerstate::*;
#[macro_use]
mod macros;
mod migrations;
pub use macros::*;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Sqlite: Connection closed")]
SqlNoConnection,
#[error("Sqlite: Already open")]
SqlAlreadyOpen,
#[error("Sqlite: Failed to open")]
SqlFailedToOpen,
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0:?}")]
BlobError(#[from] crate::blob::BlobError),
#[error("{0}")]
Other(#[from] crate::error::Error),
#[error("{0}")]
Sqlx(#[from] sqlx::Error),
#[error("{0}: {1}")]
SqlxWithContext(String, #[source] sqlx::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
/// A wrapper around the underlying Sqlite3 object.
#[derive(Debug)]
pub struct Sql {
xpool: RwLock<Option<sqlx::SqlitePool>>,
}
impl Default for Sql {
fn default() -> Self {
Self {
xpool: RwLock::new(None),
}
}
}
impl Sql {
pub fn new() -> Sql {
Self::default()
}
/// Returns `true` if there is a working sqlite connection, `false` otherwise.
pub async fn is_open(&self) -> bool {
let pool = self.xpool.read().await;
pool.is_some() && !pool.as_ref().unwrap().is_closed()
}
/// Shuts down all sqlite connections.
pub async fn close(&self) {
if let Some(pool) = self.xpool.write().await.take() {
pool.close().await;
}
}
pub async fn open<T: AsRef<Path>>(
&self,
context: &Context,
dbfile: T,
readonly: bool,
) -> crate::error::Result<()> {
if let Err(err) = open(context, self, dbfile, readonly).await {
return match err.downcast_ref::<Error>() {
Some(Error::SqlAlreadyOpen) => Err(err),
_ => {
self.close().await;
Err(err)
}
};
}
Ok(())
}
/// Execute a single query.
pub async fn execute<'a, P>(&self, statement: &'a str, params: P) -> Result<usize>
where
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
{
let lock = self.xpool.read().await;
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
let count = sqlx::query_with(statement, params).execute(xpool).await?;
Ok(count as usize)
}
/// Execute a list of statements, without any bindings
pub async fn execute_batch(&self, statement: &str) -> Result<()> {
let lock = self.xpool.read().await;
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
sqlx::query(statement.as_ref()).execute(xpool).await?;
Ok(())
}
pub async fn get_pool(&self) -> Result<SqlitePool> {
let lock = self.xpool.read().await;
lock.as_ref().cloned().ok_or_else(|| Error::SqlNoConnection)
}
/// Starts a new transaction.
pub async fn begin(
&self,
) -> Result<sqlx::Transaction<'static, Sqlite, sqlx::pool::PoolConnection<Sqlite>>> {
let lock = self.xpool.read().await;
let pool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
let tx = pool.begin().await?;
Ok(tx)
}
/// Execute a query which is expected to return zero or more rows.
pub async fn query_rows<'a, T, P>(&self, statement: &'a str, params: P) -> Result<Vec<T>>
where
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
T: for<'b> sqlx::FromRow<'b, SqliteRow> + Unpin + Send,
{
let lock = self.xpool.read().await;
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
let rows = sqlx::query_with(statement.as_ref(), params)
.try_map(|row: SqliteRow| sqlx::FromRow::from_row(&row))
.fetch_all(xpool)
.await?;
Ok(rows)
}
/// Execute a query which is expected to return zero or more rows.
pub async fn query_values<'a, T, P>(&self, statement: &'a str, params: P) -> Result<Vec<T>>
where
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
T: for<'b> sqlx::decode::Decode<'b, Sqlite>,
T: sqlx::Type<Sqlite>,
T: 'static + Unpin + Send,
{
let lock = self.xpool.read().await;
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
let rows = sqlx::query_with(statement.as_ref(), params)
.try_map(|row: SqliteRow| {
let (val,): (T,) = sqlx::FromRow::from_row(&row)?;
Ok(val)
})
.fetch_all(xpool)
.await?;
Ok(rows)
}
/// Return `true` if a query in the SQL statement it executes returns one or more
/// rows and false if the SQL returns an empty set.
pub async fn exists<'a, P>(&self, statement: &'a str, params: P) -> Result<bool>
where
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
{
let lock = self.xpool.read().await;
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
let mut rows = sqlx::query_with(statement, params).fetch(xpool);
match rows.next().await {
Some(Ok(_)) => Ok(true),
None => Ok(false),
Some(Err(sqlx::Error::RowNotFound)) => Ok(false),
Some(Err(err)) => Err(Error::SqlxWithContext(
format!("exists: '{}'", statement),
err,
)),
}
}
/// Execute a query which is expected to return one row.
pub async fn query_row<'a, T, P>(&self, statement: &'a str, params: P) -> Result<T>
where
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
T: for<'b> sqlx::FromRow<'b, SqliteRow> + Unpin + Send,
{
let lock = self.xpool.read().await;
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
let row = sqlx::query_with(statement.as_ref(), params)
.try_map(|row: SqliteRow| sqlx::FromRow::from_row(&row))
.fetch_one(xpool)
.await?;
Ok(row)
}
/// Execute a query which is expected to return zero or one row.
pub async fn query_row_optional<'a, T, P>(
&self,
statement: &'a str,
params: P,
) -> Result<Option<T>>
where
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
T: for<'b> sqlx::FromRow<'b, SqliteRow> + Unpin + Send,
{
let lock = self.xpool.read().await;
let xpool = lock.as_ref().ok_or_else(|| Error::SqlNoConnection)?;
let row = sqlx::query_with(statement.as_ref(), params)
.try_map(|row: SqliteRow| sqlx::FromRow::from_row(&row))
.fetch_optional(xpool)
.await?;
Ok(row)
}
pub async fn query_value_optional<'a, T, P>(
&self,
statement: &'a str,
params: P,
) -> Result<Option<T>>
where
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
T: for<'b> sqlx::decode::Decode<'b, Sqlite>,
T: sqlx::Type<Sqlite>,
T: 'static + Unpin + Send,
{
match self.query_row_optional(statement, params).await? {
Some((val,)) => Ok(Some(val)),
None => Ok(None),
}
}
pub async fn query_value<'a, T, P>(&self, statement: &'a str, params: P) -> Result<T>
where
P: sqlx::IntoArguments<'a, Sqlite> + 'a,
T: for<'b> sqlx::decode::Decode<'b, Sqlite>,
T: sqlx::Type<Sqlite>,
T: 'static + Unpin + Send,
{
let (val,): (T,) = self.query_row(statement, params).await?;
Ok(val)
}
pub async fn table_exists(&self, name: impl AsRef<str>) -> Result<bool> {
self.exists(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name=?",
paramsx![name.as_ref()],
)
.await
}
/// Set private configuration options.
///
/// Setting `None` deletes the value. On failure an error message
/// will already have been logged.
pub async fn set_raw_config(
&self,
_context: &Context,
key: impl AsRef<str>,
value: Option<&str>,
) -> Result<()> {
let key = key.as_ref();
if let Some(ref value) = value {
let exists = self
.exists("SELECT value FROM config WHERE keyname=?;", paramsx![key])
.await?;
if exists {
self.execute(
"UPDATE config SET value=? WHERE keyname=?;",
paramsx![value, key],
)
.await?;
} else {
self.execute(
"INSERT INTO config (keyname, value) VALUES (?, ?);",
paramsx![key, value],
)
.await?;
}
} else {
self.execute("DELETE FROM config WHERE keyname=?;", paramsx![key])
.await?;
}
Ok(())
}
/// Get configuration options from the database.
pub async fn get_raw_config(&self, key: impl AsRef<str>) -> Option<String> {
if !self.is_open().await || key.as_ref().is_empty() {
return None;
}
self.query_row(
"SELECT value FROM config WHERE keyname=?;",
paramsx![key.as_ref().to_string()],
)
.await
.ok()
.map(|(res,)| res)
}
pub async fn set_raw_config_int(
&self,
context: &Context,
key: impl AsRef<str>,
value: i32,
) -> Result<()> {
self.set_raw_config(context, key, Some(&format!("{}", value)))
.await
}
pub async fn get_raw_config_int(&self, key: impl AsRef<str>) -> Option<i32> {
self.get_raw_config(key).await.and_then(|s| s.parse().ok())
}
pub async fn get_raw_config_bool(&self, key: impl AsRef<str>) -> bool {
// Not the most obvious way to encode bool as string, but it is matter
// of backward compatibility.
let res = self.get_raw_config_int(key).await;
res.unwrap_or_default() > 0
}
pub async fn set_raw_config_bool<T>(&self, context: &Context, key: T, value: bool) -> Result<()>
where
T: AsRef<str>,
{
let value = if value { Some("1") } else { None };
self.set_raw_config(context, key, value).await
}
pub async fn set_raw_config_int64(
&self,
context: &Context,
key: impl AsRef<str>,
value: i64,
) -> Result<()> {
self.set_raw_config(context, key, Some(&format!("{}", value)))
.await
}
pub async fn get_raw_config_int64(&self, key: impl AsRef<str>) -> Option<i64> {
self.get_raw_config(key).await.and_then(|r| r.parse().ok())
}
/// Alternative to sqlite3_last_insert_rowid() which MUST NOT be used due to race conditions, see comment above.
/// the ORDER BY ensures, this function always returns the most recent id,
/// eg. if a Message-ID is split into different messages.
pub async fn get_rowid(
&self,
table: impl AsRef<str>,
field: impl AsRef<str>,
value: impl AsRef<str>,
) -> Result<u32> {
// alternative to sqlite3_last_insert_rowid() which MUST NOT be used due to race conditions, see comment above.
// the ORDER BY ensures, this function always returns the most recent id,
// eg. if a Message-ID is split into different messages.
let query = format!(
"SELECT id FROM {} WHERE {}=? ORDER BY id DESC",
table.as_ref(),
field.as_ref(),
);
let res: i32 = self.query_value(&query, paramsx![value.as_ref()]).await?;
Ok(res as u32)
}
pub async fn get_rowid2(
&self,
table: impl AsRef<str>,
field: impl AsRef<str>,
value: i64,
field2: impl AsRef<str>,
value2: i32,
) -> Result<u32> {
let query = format!(
"SELECT id FROM {} WHERE {}=? AND {}=? ORDER BY id DESC",
table.as_ref(),
field.as_ref(),
field2.as_ref(),
);
let res: i32 = self
.query_value(query.as_ref(), paramsx![value, value2])
.await?;
Ok(res as u32)
}
}
pub async fn housekeeping(context: &Context) -> Result<()> {
let mut files_in_use = HashSet::new();
let mut unreferenced_count = 0usize;
info!(context, "Start housekeeping...");
maybe_add_from_param(
context,
&mut files_in_use,
"SELECT param FROM msgs WHERE chat_id!=3 AND type!=10;",
Param::File,
)
.await?;
maybe_add_from_param(
context,
&mut files_in_use,
"SELECT param FROM jobs;",
Param::File,
)
.await?;
maybe_add_from_param(
context,
&mut files_in_use,
"SELECT param FROM chats;",
Param::ProfileImage,
)
.await?;
maybe_add_from_param(
context,
&mut files_in_use,
"SELECT param FROM contacts;",
Param::ProfileImage,
)
.await?;
let pool = context.sql.get_pool().await?;
let mut rows = sqlx::query_as("SELECT value FROM config;").fetch(&pool);
while let Some(row) = rows.next().await {
let (row,): (String,) = row?;
maybe_add_file(&mut files_in_use, row);
}
info!(context, "{} files in use.", files_in_use.len(),);
/* go through directory and delete unused files */
let p = context.get_blobdir();
match async_std::fs::read_dir(p).await {
Ok(mut dir_handle) => {
/* avoid deletion of files that are just created to build a message object */
let diff = std::time::Duration::from_secs(60 * 60);
let keep_files_newer_than = std::time::SystemTime::now().checked_sub(diff).unwrap();
while let Some(entry) = dir_handle.next().await {
if entry.is_err() {
break;
}
let entry = entry.unwrap();
let name_f = entry.file_name();
let name_s = name_f.to_string_lossy();
if is_file_in_use(&files_in_use, None, &name_s)
|| is_file_in_use(&files_in_use, Some(".increation"), &name_s)
|| is_file_in_use(&files_in_use, Some(".waveform"), &name_s)
|| is_file_in_use(&files_in_use, Some("-preview.jpg"), &name_s)
{
continue;
}
unreferenced_count += 1;
if let Ok(stats) = async_std::fs::metadata(entry.path()).await {
let recently_created =
stats.created().is_ok() && stats.created().unwrap() > keep_files_newer_than;
let recently_modified = stats.modified().is_ok()
&& stats.modified().unwrap() > keep_files_newer_than;
let recently_accessed = stats.accessed().is_ok()
&& stats.accessed().unwrap() > keep_files_newer_than;
if recently_created || recently_modified || recently_accessed {
info!(
context,
"Housekeeping: Keeping new unreferenced file #{}: {:?}",
unreferenced_count,
entry.file_name(),
);
continue;
}
}
info!(
context,
"Housekeeping: Deleting unreferenced file #{}: {:?}",
unreferenced_count,
entry.file_name()
);
let path = entry.path();
dc_delete_file(context, path).await;
}
}
Err(err) => {
warn!(
context,
"Housekeeping: Cannot open {}. ({})",
context.get_blobdir().display(),
err
);
}
}
if let Err(err) = prune_tombstones(context).await {
warn!(
context,
"Houskeeping: Cannot prune message tombstones: {}", err
);
}
info!(context, "Housekeeping done.",);
Ok(())
}
fn is_file_in_use(files_in_use: &HashSet<String>, namespc_opt: Option<&str>, name: &str) -> bool {
let name_to_check = if let Some(namespc) = namespc_opt {
let name_len = name.len();
let namespc_len = namespc.len();
if name_len <= namespc_len || !name.ends_with(namespc) {
return false;
}
&name[..name_len - namespc_len]
} else {
name
};
files_in_use.contains(name_to_check)
}
fn maybe_add_file(files_in_use: &mut HashSet<String>, file: impl AsRef<str>) {
if !file.as_ref().starts_with("$BLOBDIR/") {
return;
}
files_in_use.insert(file.as_ref()[9..].into());
}
async fn maybe_add_from_param(
context: &Context,
files_in_use: &mut HashSet<String>,
query: &str,
param_id: Param,
) -> Result<()> {
info!(context, "maybe_add_from_param: {}", query);
let pool = context.sql.get_pool().await?;
let mut rows = sqlx::query_as(query).fetch(&pool);
while let Some(row) = rows.next().await {
let (row,): (String,) = row?;
info!(context, "param: {}", &row);
let param: Params = row.parse().unwrap_or_default();
if let Some(file) = param.get(param_id) {
info!(context, "got file: {:?} {}", param_id, file);
maybe_add_file(files_in_use, file);
}
}
Ok(())
}
#[allow(clippy::cognitive_complexity)]
async fn open(
context: &Context,
sql: &Sql,
dbfile: impl AsRef<Path>,
readonly: bool,
) -> crate::error::Result<()> {
if sql.is_open().await {
error!(
context,
"Cannot open, database \"{:?}\" already opened.",
dbfile.as_ref(),
);
return Err(Error::SqlAlreadyOpen.into());
}
if readonly {
// TODO: readonly mode
}
let xpool = sqlx::SqlitePool::builder()
.min_size(1)
.max_size(4)
.build(&format!("sqlite://{}", dbfile.as_ref().to_string_lossy()))
.await?;
{
*sql.xpool.write().await = Some(xpool)
}
if !readonly {
let mut exists_before_update = false;
let mut dbversion_before_update: i32 = -1;
if sql.table_exists("config").await? {
exists_before_update = true;
if let Some(version) = sql.get_raw_config_int("dbversion").await {
dbversion_before_update = version;
}
}
// (1) update low-level database structure.
// this should be done before updates that use high-level objects that
// rely themselves on the low-level structure.
// --------------------------------------------------------------------
migrations::run(context, &sql, dbversion_before_update, exists_before_update).await?;
// general updates
// (2) updates that require high-level objects
// (the structure is complete now and all objects are usable)
// --------------------------------------------------------------------
let mut recalc_fingerprints = false;
let mut update_icons = false;
if dbversion_before_update < 34 {
recalc_fingerprints = true;
}
if dbversion_before_update < 61 {
update_icons = true;
}
if recalc_fingerprints {
info!(context, "[migration] recalc fingerprints");
let pool = context.sql.get_pool().await?;
let mut rows = sqlx::query_as("SELECT addr FROM acpeerstates;").fetch(&pool);
while let Some(addr) = rows.next().await {
let (addr,): (String,) = addr?;
if let Ok(ref mut peerstate) = Peerstate::from_addr(context, &addr).await {
peerstate.recalc_fingerprint();
peerstate.save_to_db(sql, false).await?;
}
}
}
if update_icons {
update_saved_messages_icon(context).await?;
update_device_icon(context).await?;
}
}
info!(context, "Opened {:?}.", dbfile.as_ref(),);
Ok(())
}
/// Removes from the database locally deleted messages that also don't
/// have a server UID.
async fn prune_tombstones(context: &Context) -> Result<()> {
context
.sql
.execute(
r#"
DELETE FROM msgs
WHERE (chat_id = ? OR hidden)
AND server_uid = 0
"#,
paramsx![DC_CHAT_ID_TRASH as i32],
)
.await?;
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_maybe_add_file() {
let mut files = Default::default();
maybe_add_file(&mut files, "$BLOBDIR/hello");
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
maybe_add_file(&mut files, "world2.txt");
maybe_add_file(&mut files, "$BLOBDIR");
assert!(files.contains("hello"));
assert!(files.contains("world.txt"));
assert!(!files.contains("world2.txt"));
assert!(!files.contains("$BLOBDIR"));
}
#[test]
fn test_is_file_in_use() {
let mut files = Default::default();
maybe_add_file(&mut files, "$BLOBDIR/hello");
maybe_add_file(&mut files, "$BLOBDIR/world.txt");
maybe_add_file(&mut files, "world2.txt");
assert!(is_file_in_use(&files, None, "hello"));
assert!(!is_file_in_use(&files, Some(".txt"), "hello"));
assert!(is_file_in_use(&files, Some("-suffix"), "world.txt-suffix"));
}
}

View File

@@ -179,6 +179,12 @@ pub enum StockMessage {
#[strum(props(fallback = "Unknown Sender for this chat. See 'info' for more details."))]
UnknownSenderForChat = 72,
#[strum(props(fallback = "Message from %1$s"))]
SubjectForNewContact = 73,
#[strum(props(fallback = "Failed to send message to %1$s."))]
FailedSendingTo = 74,
}
/*
@@ -354,7 +360,7 @@ impl Context {
// create saved-messages chat;
// we do this only once, if the user has deleted the chat, he can recreate it manually.
if !self.sql.get_raw_config_bool(&self, "self-chat-added").await {
if !self.sql.get_raw_config_bool("self-chat-added").await {
self.sql
.set_raw_config_bool(&self, "self-chat-added", true)
.await?;
@@ -403,7 +409,7 @@ mod tests {
#[async_std::test]
async fn test_set_stock_translation() {
let t = dummy_context().await;
let t = TestContext::new().await;
t.ctx
.set_stock_translation(StockMessage::NoMessages, "xyz".to_string())
.await
@@ -413,7 +419,7 @@ mod tests {
#[async_std::test]
async fn test_set_stock_translation_wrong_replacements() {
let t = dummy_context().await;
let t = TestContext::new().await;
assert!(t
.ctx
.set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string())
@@ -428,7 +434,7 @@ mod tests {
#[async_std::test]
async fn test_stock_str() {
let t = dummy_context().await;
let t = TestContext::new().await;
assert_eq!(
t.ctx.stock_str(StockMessage::NoMessages).await,
"No messages."
@@ -437,7 +443,7 @@ mod tests {
#[async_std::test]
async fn test_stock_string_repl_str() {
let t = dummy_context().await;
let t = TestContext::new().await;
// uses %1$s substitution
assert_eq!(
t.ctx
@@ -450,7 +456,7 @@ mod tests {
#[async_std::test]
async fn test_stock_string_repl_int() {
let t = dummy_context().await;
let t = TestContext::new().await;
assert_eq!(
t.ctx
.stock_string_repl_int(StockMessage::MsgAddMember, 42)
@@ -461,7 +467,7 @@ mod tests {
#[async_std::test]
async fn test_stock_string_repl_str2() {
let t = dummy_context().await;
let t = TestContext::new().await;
assert_eq!(
t.ctx
.stock_string_repl_str2(StockMessage::ServerResponse, "foo", "bar")
@@ -472,7 +478,7 @@ mod tests {
#[async_std::test]
async fn test_stock_system_msg_simple() {
let t = dummy_context().await;
let t = TestContext::new().await;
assert_eq!(
t.ctx
.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0)
@@ -483,7 +489,7 @@ mod tests {
#[async_std::test]
async fn test_stock_system_msg_add_member_by_me() {
let t = dummy_context().await;
let t = TestContext::new().await;
assert_eq!(
t.ctx
.stock_system_msg(
@@ -499,7 +505,7 @@ mod tests {
#[async_std::test]
async fn test_stock_system_msg_add_member_by_me_with_displayname() {
let t = dummy_context().await;
let t = TestContext::new().await;
Contact::create(&t.ctx, "Alice", "alice@example.com")
.await
.expect("failed to create contact");
@@ -518,7 +524,7 @@ mod tests {
#[async_std::test]
async fn test_stock_system_msg_add_member_by_other_with_displayname() {
let t = dummy_context().await;
let t = TestContext::new().await;
let contact_id = {
Contact::create(&t.ctx, "Alice", "alice@example.com")
.await
@@ -542,7 +548,7 @@ mod tests {
#[async_std::test]
async fn test_stock_system_msg_grp_name() {
let t = dummy_context().await;
let t = TestContext::new().await;
assert_eq!(
t.ctx
.stock_system_msg(
@@ -558,7 +564,7 @@ mod tests {
#[async_std::test]
async fn test_stock_system_msg_grp_name_other() {
let t = dummy_context().await;
let t = TestContext::new().await;
let id = Contact::create(&t.ctx, "Alice", "alice@example.com")
.await
.expect("failed to create contact");
@@ -573,7 +579,7 @@ mod tests {
#[async_std::test]
async fn test_update_device_chats() {
let t = dummy_context().await;
let t = TestContext::new().await;
t.ctx.update_device_chats().await.ok();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 2);

View File

@@ -18,26 +18,69 @@ pub(crate) struct TestContext {
pub dir: TempDir,
}
/// Create a new, opened [TestContext] using given callback.
///
/// The [Context] will be opened with the SQLite database named
/// "db.sqlite" in the [TestContext.dir] directory.
///
/// [Context]: crate::context::Context
pub(crate) async fn test_context() -> TestContext {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let ctx = Context::new("FakeOs".into(), dbfile.into()).await.unwrap();
TestContext { ctx, dir }
}
impl TestContext {
/// Create a new [TestContext].
///
/// The [Context] will be created and have an SQLite database named "db.sqlite" in the
/// [TestContext.dir] directory. This directory is cleaned up when the [TestContext] is
/// dropped.
///
/// [Context]: crate::context::Context
pub async fn new() -> Self {
pretty_env_logger::try_init_timed().ok();
/// Return a dummy [TestContext].
///
/// The context will be opened and use the SQLite database as
/// specified in [test_context] but there is no callback hooked up,
/// i.e. [Context::call_cb] will always return `0`.
pub(crate) async fn dummy_context() -> TestContext {
test_context().await
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let ctx = Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
let events = ctx.get_event_emitter();
async_std::task::spawn(async move {
while let Some(event) = events.recv().await {
log::info!("{:?}", event);
}
});
TestContext { ctx, dir }
}
/// Create a new configured [TestContext].
///
/// This is a shortcut which automatically calls [TestContext::configure_alice] after
/// creating the context.
pub async fn new_alice() -> Self {
let t = Self::new().await;
t.configure_alice().await;
t
}
/// Configure with alice@example.com.
///
/// The context will be fake-configured as the alice user, with a pre-generated secret
/// key. The email address of the user is returned as a string.
pub async fn configure_alice(&self) -> String {
let keypair = alice_keypair();
self.configure_addr(&keypair.addr.to_string()).await;
key::store_self_keypair(&self.ctx, &keypair, key::KeyPairUse::Default)
.await
.expect("Failed to save Alice's key");
keypair.addr.to_string()
}
/// Configure as a given email address.
///
/// The context will be configured but the key will not be pre-generated so if a key is
/// used the fingerprint will be different every time.
pub async fn configure_addr(&self, addr: &str) {
self.ctx.set_config(Config::Addr, Some(addr)).await.unwrap();
self.ctx
.set_config(Config::ConfiguredAddr, Some(addr))
.await
.unwrap();
self.ctx
.set_config(Config::Configured, Some("1"))
.await
.unwrap();
}
}
/// Load a pre-generated keypair for alice@example.com from disk.
@@ -60,20 +103,6 @@ pub(crate) fn alice_keypair() -> key::KeyPair {
}
}
/// Creates Alice with a pre-generated keypair.
///
/// Returns the address of the keypair created (alice@example.com).
pub(crate) async fn configure_alice_keypair(ctx: &Context) -> String {
let keypair = alice_keypair();
ctx.set_config(Config::ConfiguredAddr, Some(&keypair.addr.to_string()))
.await
.unwrap();
key::store_self_keypair(&ctx, &keypair, key::KeyPairUse::Default)
.await
.expect("Failed to save Alice's key");
keypair.addr.to_string()
}
/// Load a pre-generated keypair for bob@example.net from disk.
///
/// Like [alice_keypair] but a different key and identity.

View File

@@ -11,7 +11,7 @@ use crate::context::Context;
use crate::dc_tools::*;
/// Token namespace
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, Sqlx)]
#[repr(i32)]
pub enum Namespace {
Unknown = 0,
@@ -34,7 +34,7 @@ pub async fn save(context: &Context, namespace: Namespace, foreign_id: ChatId) -
.sql
.execute(
"INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);",
paramsv![namespace, foreign_id, token, time()],
paramsx![namespace, foreign_id, &token, time()],
)
.await
.ok();
@@ -44,12 +44,12 @@ pub async fn save(context: &Context, namespace: Namespace, foreign_id: ChatId) -
pub async fn lookup(context: &Context, namespace: Namespace, foreign_id: ChatId) -> Option<String> {
context
.sql
.query_get_value::<String>(
context,
.query_value(
"SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;",
paramsv![namespace, foreign_id],
paramsx![namespace, foreign_id],
)
.await
.ok()
}
pub async fn lookup_or_new(context: &Context, namespace: Namespace, foreign_id: ChatId) -> String {
@@ -65,7 +65,7 @@ pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> boo
.sql
.exists(
"SELECT id FROM tokens WHERE namespc=? AND token=?;",
paramsv![namespace, token],
paramsx![namespace, token],
)
.await
.unwrap_or_default()

View File

@@ -0,0 +1,242 @@
Delivered-To: alice@gmail.com
Received: by 2002:a1c:b4d7:0:0:0:0:0 with SMTP id d206csp3026053wmf;
Mon, 18 May 2020 09:23:25 -0700 (PDT)
X-Received: by 2002:a5d:4651:: with SMTP id j17mr19532177wrs.50.1589819005555;
Mon, 18 May 2020 09:23:25 -0700 (PDT)
ARC-Seal: i=1; a=rsa-sha256; t=1589819005; cv=none;
d=google.com; s=arc-20160816;
b=IZbNnzzuYzTFuqfuZwpd3ehqpYYGpn31c8DsfGbQ8rpbS0OTTROkVYvihQl8Ne/8X/
brEWsrcmaCh9WpFMzpI+cp/TY39uusnI6qdp5rcgrFmFgoANtwf3TBBj1+f7wBPn46BP
dQOUsg/J8KVfvzVgvL1x4uyJ0m9QirDgJeJ/BvrswbTleRQK7oY3fIireUCDxj6r2lCB
1Z0TKw1mgIb1LiFMZz8kvCNn3R4KSFnwS8rIju0hYwnsioNiExVQgumXL+RVkEZ9BMzf
UdoWIAw3VW+MOZFTpfLCEfgIPtLg/gtE0Q1P+a3KKpi8dkPiV2n6DGMecy9lTLtdhCXt
pnaA==
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816;
h=in-reply-to:references:subject:from:date:message-id:auto-submitted
:to:dkim-signature;
bh=5xjZvcHbEGbMY0K2QB+3U6tpm1L1LAVv5h1pd4YXDEE=;
b=nNP0DktrSjdBaFfhhoDi2O9KVKM0iXE5ZgubQ0q0ff68Z6Ke7c8dDBXEsZoToI0s4Y
w90KyJFpgMJLFmP3iVDRqCfohi2y1HGdWg5VXQPTvzM7+YozZRlbNNV9UsuyRY91CXrJ
a2XREBgB+LPMGQivwcHtUMZfyNv/4uiwWivk+92ySNDhxqOiDt4R5Jak/7RkZMFwQpsE
JGwk6asM6VqZlihkF24lKv3pPaob6feyX3wD5N0+Mqiy1kQTj2JkpQk6nkTmdf0gapZe
fOhU1NkbNfbuS3U7m2gEUiyktE+MhV/MgAzgBhm9bgNt2gQLVWju8rHkPndfv1PDmEkC
FsYQ==
ARC-Authentication-Results: i=1; mx.google.com;
dkim=pass header.i=@googlemail.com header.s=20161025 header.b=dPisws+O;
spf=pass (google.com: best guess record for domain of postmaster@mail-sor-f69.google.com designates 209.85.220.69 as permitted sender) smtp.helo=mail-sor-f69.google.com;
dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=googlemail.com
Return-Path: <>
Received: from mail-sor-f69.google.com (mail-sor-f69.google.com. [209.85.220.69])
by mx.google.com with SMTPS id s18sor5584435wrb.25.2020.05.18.09.23.25
for <alice@gmail.com>
(Google Transport Security);
Mon, 18 May 2020 09:23:25 -0700 (PDT)
Received-SPF: pass (google.com: best guess record for domain of postmaster@mail-sor-f69.google.com designates 209.85.220.69 as permitted sender) client-ip=209.85.220.69;
Authentication-Results: mx.google.com;
dkim=pass header.i=@googlemail.com header.s=20161025 header.b=dPisws+O;
spf=pass (google.com: best guess record for domain of postmaster@mail-sor-f69.google.com designates 209.85.220.69 as permitted sender) smtp.helo=mail-sor-f69.google.com;
dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=googlemail.com
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=googlemail.com; s=20161025;
h=to:auto-submitted:message-id:date:from:subject:references
:in-reply-to;
bh=5xjZvcHbEGbMY0K2QB+3U6tpm1L1LAVv5h1pd4YXDEE=;
b=dPisws+OwGFyOy0a612XYZgvz5T71GcJRJtU068/Tce8vN/+ggIQtUsZnZtsphe71v
2NvfP9ULxR4cXvomTvhrYAk19KdxN/S7SeyBbmXv3x/tg+DBVCmmPS/6RXrcl6Ms3Hkw
uPFQ9S3KcvHe/2bcb5LSTA/stIP4tuxxAXvsX2j+MjPYPWKAl50jkSbWK98U0Q0U+MTl
pKaaC9s9iEBafac8BFZCy4DfpumKlemNEyRa3cSV2hw+DYHKA5peModrK1A2tcsfstFF
rZi8yF/D90RIFbE04DI2QCxB3trsChNF1aYF06aSzI//wsfM1+lb+uGPi0YVkw3n4HrX
Xw4w==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=1e100.net; s=20161025;
h=x-gm-message-state:to:auto-submitted:message-id:date:from:subject
:references:in-reply-to;
bh=5xjZvcHbEGbMY0K2QB+3U6tpm1L1LAVv5h1pd4YXDEE=;
b=A/NCOtgbpA7VzB1G7ZFo8TA2FfrjuqjGdwMrJr3yXe21FrBFwzssprJwOkynqoVLkK
iJU7uMF/KTcQPDEmOLFThzFfe5GCx7eJtZPhwY+FbBlC5sq4I55/xaQLd0gOZ1BYXwMn
2bk169d2aoukbaLbGSQZF3d9atd+/e48YzkRxpmUoLcrWk2LcHAeQIG7SgT9pfX5DKPr
VpxM5/GMVEBbTRhBIWCeVSfpYCs80l0xEeTC3/B5lzpzMVDE8QCW6Dwh75b4Tb2K6yru
Zsy5ZpRmwv0wrkrb2vM+pl4IMkaF7s8XosIvlIT++fQV5xDFItT4atpykZvSDB92RKV0
8lEA==
X-Gm-Message-State: AOAM532RG/PT3ChZHBCDORGLtAjKvX8TGBuOy+AxrnEaJT6v1ieb+VV1
+ejly+/6UthxHYlkOJYAszCSgL4dKVFotoVaN7LhEA==
X-Google-Smtp-Source: ABdhPJz6veVKWhomCL4gK+whrybuMzHCDCq8AowgQvi7sobpMoM/k9CDw79jo1j3OUcTz6MEeUYLxEXuNIuu4zyoS7kVtsUYryGFHAI=
X-Received: by 2002:a5d:5183:: with SMTP id k3mr20545185wrv.159.1589819005394;
Mon, 18 May 2020 09:23:25 -0700 (PDT)
Content-Type: multipart/report; boundary="00000000000012d63005a5ee9520"; report-type=delivery-status
To: alice@gmail.com
Received: by 2002:a5d:5183:: with SMTP id k3mr13704211wrv.159; Mon, 18 May
2020 09:23:25 -0700 (PDT)
Return-Path: <>
Auto-Submitted: auto-replied
Message-ID: <5ec2b67d.1c69fb81.213af.67a5.GMR@mx.google.com>
Date: Mon, 18 May 2020 09:23:25 -0700 (PDT)
From: Mail Delivery Subsystem <mailer-daemon@googlemail.com>
Subject: Delivery Status Notification (Failure)
References: <CABXKi8zruXJc_6e4Dr087H5wE7sLp+u250o0N2q5DdjF_r-8wg@mail.gmail.com>
In-Reply-To: <CABXKi8zruXJc_6e4Dr087H5wE7sLp+u250o0N2q5DdjF_r-8wg@mail.gmail.com>
X-Failed-Recipients: assidhfaaspocwaeofi@gmail.com
--00000000000012d63005a5ee9520
Content-Type: multipart/related; boundary="00000000000012dc0005a5ee952f"
--00000000000012dc0005a5ee952f
Content-Type: multipart/alternative; boundary="00000000000012dc0705a5ee9530"
--00000000000012dc0705a5ee9530
Content-Type: text/plain; charset="UTF-8"
** Die Adresse wurde nicht gefunden **
Ihre Nachricht wurde nicht an assidhfaaspocwaeofi@gmail.com zugestellt, weil die Adresse nicht gefunden wurde oder keine E-Mails empfangen kann.
Hier erfahren Sie mehr: https://support.google.com/mail/?p=NoSuchUser
Antwort:
550 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
--00000000000012dc0705a5ee9530
Content-Type: text/html; charset="UTF-8"
<html>
<head>
<style>
* {
font-family:Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif;
}
</style>
</head>
<body>
<table cellpadding="0" cellspacing="0" class="email-wrapper" style="padding-top:32px;background-color:#ffffff;"><tbody>
<tr><td>
<table cellpadding=0 cellspacing=0><tbody>
<tr><td style="max-width:560px;padding:24px 24px 32px;background-color:#fafafa;border:1px solid #e0e0e0;border-radius:2px">
<img style="padding:0 24px 16px 0;float:left" width=72 height=72 alt="Fehlersymbol" src="cid:icon.png">
<table style="min-width:272px;padding-top:8px"><tbody>
<tr><td><h2 style="font-size:20px;color:#212121;font-weight:bold;margin:0">
Die Adresse wurde nicht gefunden
</h2></td></tr>
<tr><td style="padding-top:20px;color:#757575;font-size:16px;font-weight:normal;text-align:left">
Ihre Nachricht wurde nicht an <a style='color:#212121;text-decoration:none'><b>assidhfaaspocwaeofi@gmail.com</b></a> zugestellt, weil die Adresse nicht gefunden wurde oder keine E-Mails empfangen kann.
</td></tr>
<tr><td style="padding-top:24px;color:#4285F4;font-size:14px;font-weight:bold;text-align:left">
<a style="text-decoration:none" href="https://support.google.com/mail/?p=NoSuchUser">WEITERE INFORMATIONEN</a>
</td></tr>
</tbody></table>
</td></tr>
</tbody></table>
</td></tr>
<tr style="border:none;background-color:#fff;font-size:12.8px;width:90%">
<td align="left" style="padding:48px 10px">
Antwort:<br/>
<p style="font-family:monospace">
550 5.1.1 The email account that you tried to reach does not exist. Please try double-checking the recipient&#39;s email address for typos or unnecessary spaces. Learn more at https://support.google.com/mail/?p=NoSuchUser i18sor6261697wrs.38 - gsmtp
</p>
</td>
</tr>
</tbody></table>
</body>
</html>
--00000000000012dc0705a5ee9530--
--00000000000012dc0005a5ee952f
Content-Type: image/png; name="icon.png"
Content-Disposition: attachment; filename="icon.png"
Content-Transfer-Encoding: base64
Content-ID: <icon.png>
iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAYAAADnRuK4AAAACXBIWXMAABYlAAAWJQFJUiTwAAAA
GXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABTdJREFUeNrsnD9sFEcUh5+PRMqZ
yA0SPhAUQAQFUkyTgiBASARo6QApqVIkfdxGFJFSgGhJAUIiBaQB0ZIOKVCkwUgURjIg2fxL4kS+
YDvkbC/388bi8N16Z4/d7J/5PsniuD3fyePP772ZeTsDQRAYQL/UGAJAIEAgQCBAIAAEAgQCBAIE
AkAgyJT3Mv+Eq7vYK8mTE+MDRCAghQECAeRQA5V2ZOpmg5vDx3NPzRbmGRMEcmTrEbNNB8zWfRD+
f/Efs2e3zCZvMjaksBg27TfbcuSNPEKP9ZyuAQKtHX2O9ncNgWC57umMPKvRNb0GEKgnLoUyxTQC
rcns0/6uIRAs8/hGf9cQCJZpTpjdO2f25/03z+mxntM1eLtsZAgiUtX4JcaBCAQIBAgECARQ8CJa
G5jab4J4pm4WZmO3OALVh802fIwcLkyPkcKAGggAgQCBAIEAgQCBABAIEAjKA/1AnahhbO5FdOOY
VsrrDbPBYcYKgf5D2wLaV3p+22xh1u17tO3S+DTcvxvagUDeivPgx/a/95J/73w7Sj26Hn4pKo2M
ehuV/KyBJM6d0f7k6RKx/R63vvL2tmf/ItDdM2ZTP6f7nkp9Y2fDx1v9akmpIU+KSCLVUghUQfSL
zVKeTklbLxGoctw/nzC5rw8L5KRNbkpnKq6pgSqEClzNnFzY+XnYWrt6VpVk1vbwWvg+RKCKMOUw
Q1LEOXA+/MX3mpJvGDHb265xtnzmFoUK1HaKQGlMtePYM+q2KKjXuaS1NJYIEKgI8jhEgqHt4cqy
Ky53j3hyHz2bqSLp2o2LbJ7MxKovkGqXteoWpaOk96O9/yF/dF7NwlS36AuIQIBA5celQK4PIxBE
4LLzrtoLgaALdSy6CJRkWQCBPGLsTHznomZ9nszUECgJ2ml3WWHe+QVFNPSQx6UdZNtxr9pbEShN
eTTz8mQXHoHSlke7+Z+c9m6VGoHSkEfs/trLW3wQKApN1V3lGfnGu2Z6BFoLtYCs3GWBPAiUCLVh
/HoaeRCoT9R873KLM/IgUBfapnCpe5AHgXry4pf412ihEHkQqCdxd5VqrcezhUIESsJMTJ+Pdthp
Z0WgyNlXXPHc2Mc4IVAELl2Gnh8mhUDvCkfbIVAkcbf/aOoO3fMKhqAD3frTa4quwpn0hUDOkQhI
YYBAgECAQAAU0QlYObl+5Ug8NcprZkZxjUCxRPVA6zmtEXHCBykskrhjgHXN09PoEcgFl4M4H11j
nBAoApcj6ZoPGScEAgTKApcDoTw5sgWB+sGlz1n90IBAPdE6j1o21PfcC11jLagL1oFWRyGlKU3p
OxcSJQ7NZAjkhHp/uG2HFAYIBAgECASAQIBAgECAQAAIBOkxEARBtp9wdVfAMOfIifEBIhCQwgCB
ABAI0oV2jhxZ+nfBatuPZfgBCy0Eqqo8c01b+uu51XZvzOgDWoHNTGR+pCwpLEd5svuAZXlO2uEr
PyEQ8hRWHgRCHmqg0sjTnLalv6crJQ8C/U8stqNO0I4+VZOHFIY8COS1PGL2ybd5yUMKK7s8zYmL
dujyd3n+nESgcsvzZd4/KwIhDwIhT35QA6UyE1qyxZnfvJMHgdKS549JC1qvvJOHFIY8CFR5eV5O
XimqPAhUdHnmfx+zgxdOFXkoqIGKKs/cswnb/8Oeog8HEai48nxUhiFBIORBIOShBioskkbySCLk
IQIhDwIhj28p7FApR6b1qlEbHGpkO/rr6215vi/zH1r2x7tApSGFAQIBAgECAQIBIBAgECAQIBBA
LK8FGADCTxYrr+EVJgAAAABJRU5ErkJggg==
--00000000000012dc0005a5ee952f--
--00000000000012d63005a5ee9520
Content-Type: message/delivery-status
Reporting-MTA: dns; googlemail.com
Arrival-Date: Mon, 18 May 2020 09:23:25 -0700 (PDT)
X-Original-Message-ID: <CABXKi8zruXJc_6e4Dr087H5wE7sLp+u250o0N2q5DdjF_r-8wg@mail.gmail.com>
Final-Recipient: rfc822; assidhfaaspocwaeofi@gmail.com
Action: failed
Status: 5.1.1
Diagnostic-Code: smtp; 550-5.1.1 The email account that you tried to reach does not exist. Please try
550-5.1.1 double-checking the recipient's email address for typos or
550-5.1.1 unnecessary spaces. Learn more at
550 5.1.1 https://support.google.com/mail/?p=NoSuchUser i18sor6261697wrs.38 - gsmtp
Last-Attempt-Date: Mon, 18 May 2020 09:23:25 -0700 (PDT)
--00000000000012d63005a5ee9520
Content-Type: message/rfc822
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=gmail.com; s=20161025;
h=mime-version:from:date:message-id:subject:to;
bh=gtlm3j0shCgZYOVxUt74zkQ69Zq+GTQeHeXLfMlrhlk=;
b=a185ogBcMzF9whNVWvuyUoUunNZk3Vc1kEIFmPkX0IxLpAFcI+fOQajOSromGl7Oyi
yecLwQevpww2Xd0XjZ3UkZvrI9m9koRmh0QeoHvgTRORiVwj08+PVc3N4F9bCO4w9i0J
ir7SSsJqBCDovoIFSFDyNa64vs6Nxno0cH/DaPG7pVTdD+3jfB7nLXIsMQYeX+1eP6rB
UhKxH82r7Mh9CI2PWDQpVtGj63AMUEyHgE9Ou08PWbbKjrQOasoG3Tw8tB1GoN1XYssM
rxOTgWEoTiduZ35AUH6h+eChOn9OHuI3SPECcVob70Qndayia3dMKfHMO6sEx9J0Wpic
29vg==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=1e100.net; s=20161025;
h=x-gm-message-state:mime-version:from:date:message-id:subject:to;
bh=gtlm3j0shCgZYOVxUt74zkQ69Zq+GTQeHeXLfMlrhlk=;
b=miGIfL5BgnkD3wQvS34RtGwRRoh+8gJT5sFFfdX/hVyG/dvjXfdwP4yyNWr8ox8iY2
BLlahS4y4VGcbG1e2aYjurnWNytGu6utQcZax/uUngJ0bTOwXW1VaIiEZtqd6gTV+8d/
rrfQ459+4vXqIoQf0+Oi/U6dWwgJvPPjjRiToWdF3vIJE8R1iTRdZbW4lkgxSADbmskg
noT/gWGWblHtR6uuGuKGJ3bkhJKCBnjavKh0LlbWEeFBZfmVNPRvzEFWHjBDdu5wvSL5
0QJ+Qn0Orfn5CJuN3xPfzT1S2rI2iYZx37KX9zyMnZEx0ilkTYqCtBPWkrXRYDSXcxYS
Y1ag==
X-Gm-Message-State: AOAM531vhwpXiK8M12286dOJx0Q5fBl9ZaH6BJKts93GoxvPv0xdryP0
jg9wYmoP5MUHudsxAMCYDFsCUMVx2PEywyIsaQqklw==
X-Google-Smtp-Source: ABdhPJxlVJtTODM3pZZSTbbpAAAQRU8XbmuosDF9fgQZmVwxGZSzRWl22o+moppVRU/r8xMAyf0r3+qXwEBe1vZfjZo=
X-Received: by 2002:a5d:5183:: with SMTP id k3mr20545162wrv.159.1589819005034;
Mon, 18 May 2020 09:23:25 -0700 (PDT)
MIME-Version: 1.0
From: <alice@gmail.com>
Date: Mon, 18 May 2020 18:23:39 +0200
Message-ID: <CABXKi8zruXJc_6e4Dr087H5wE7sLp+u250o0N2q5DdjF_r-8wg@mail.gmail.com>
Subject: Kommt sowieso nicht an
To: assidhfaaspocwaeofi@gmail.com
Content-Type: multipart/alternative; boundary="0000000000000d652a05a5ee95df"
--0000000000000d652a05a5ee95df
Content-Type: text/plain; charset="UTF-8"
Wollte nur was testen
--0000000000000d652a05a5ee95df
Content-Type: text/html; charset="UTF-8"
<div dir="ltr">Wollte nur was testen<br></div>
--0000000000000d652a05a5ee95df--
--00000000000012d63005a5ee9520--

View File

@@ -0,0 +1,242 @@
Delivered-To: alice@gmail.com
Received: by 2002:a02:6629:0:0:0:0:0 with SMTP id k41csp368502jac;
Wed, 10 Jun 2020 05:17:57 -0700 (PDT)
X-Received: by 2002:a6b:1448:: with SMTP id 69mr2898530iou.83.1591791475733;
Wed, 10 Jun 2020 05:17:55 -0700 (PDT)
ARC-Seal: i=1; a=rsa-sha256; t=1591791475; cv=none;
d=google.com; s=arc-20160816;
b=a0vSKJPbMtGYFnuk1ye/gnnV00Zvva4OOJTMOyfm13xMJD0YAhzGVfa7Z+wn5sQ8dw
VAxpmDHCkjp4jol0C1iutiq2Nl0qma819oFPuuoMLLatKQXHpo8Jt+sL3MnwNR7J5bZC
1c6Fjk75EIsRWhJd1HCkm44A6UYHxqqsTnzQCaNiHbjsRsvbggxwlMGSrZ4silxqSDvo
Pzd/YDLCvsnZNSNIjIckKAwtGmY6sXctZ+JnOTykXAyL32Milfwy1vRL9xm10Q14biTR
+qaIQp4E6WE63g1WHvfAjs0Dru7DalUh4GGl/NAwqVhY1gVyRD5E9/nODyHAfxjvaxDD
4sMw==
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816;
h=in-reply-to:references:subject:from:date:message-id:auto-submitted
:to:dkim-signature;
bh=XaR1H4XeD+InO7mULPJn53omDGmxN+KG6DbSxyyErPM=;
b=OJbgbrktMKyczw25z/ib7lSdRX80PEK3Myh9fj4q6mDlXmPPv//Gv069znRQ4QbadM
HUXZH0WLMZcGyqI6SvGL/noxQ1O8yP0FYJJKTkoX0Gk2hHzfaE3x1scOP/o2FMMQXIFm
S4CgGBD6HHzBJYj/rSL3gzqLzx1Id/z5kTeDvH2cn8JJAcCE2q/nhjTyWUb87geoNlDJ
A1HRrLHK/0JOyRjHfg2zZCqIvSi1xmpiHStMyL9mfVyrQs98tsPxaOkJHjLplFARoPlr
mmmDvsFg7MPvFqkkANzz4JDHidnfKRULCgnrVj1yTU66UagUpQEGjZqz8/99YuU6nt1t
81sQ==
ARC-Authentication-Results: i=1; mx.google.com;
dkim=pass header.i=@googlemail.com header.s=20161025 header.b=aO4aNy7C;
spf=pass (google.com: best guess record for domain of postmaster@mail-sor-f69.google.com designates 209.85.220.69 as permitted sender) smtp.helo=mail-sor-f69.google.com;
dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=googlemail.com
Return-Path: <>
Received: from mail-sor-f69.google.com (mail-sor-f69.google.com. [209.85.220.69])
by mx.google.com with SMTPS id w14sor16686480iow.23.2020.06.10.05.17.55
for <alice@gmail.com>
(Google Transport Security);
Wed, 10 Jun 2020 05:17:55 -0700 (PDT)
Received-SPF: pass (google.com: best guess record for domain of postmaster@mail-sor-f69.google.com designates 209.85.220.69 as permitted sender) client-ip=209.85.220.69;
Authentication-Results: mx.google.com;
dkim=pass header.i=@googlemail.com header.s=20161025 header.b=aO4aNy7C;
spf=pass (google.com: best guess record for domain of postmaster@mail-sor-f69.google.com designates 209.85.220.69 as permitted sender) smtp.helo=mail-sor-f69.google.com;
dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=googlemail.com
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=googlemail.com; s=20161025;
h=to:auto-submitted:message-id:date:from:subject:references
:in-reply-to;
bh=XaR1H4XeD+InO7mULPJn53omDGmxN+KG6DbSxyyErPM=;
b=aO4aNy7CUOk9O4Jnsue/DvMFY6Ph0C34AbpoxJH+mLZpOmt/KYGCGYWgunZgamF15U
Vm8JY5yLKGwkTz2m3abDnKNP4fpl6zeZ5fyk5LvXH2Jema0iocHai6pJZBoFGPnonNmd
MscTf1sEltbOxwfOmM1BRHX34c1jW0+8Yd2+Nhg2DPvzuq1brOVin6bUV4VX5EeeuNqT
ZTewjJVPmO/B5NQhdpG81FO5w4hKSQ/VzZXnap2thMf3gOmnaoR+tbsnOIAiklcLdJ7b
57SKUwI041pwSmh9dffs0STl2GvMRSJyGCtBqMnzXgflqoGTcnPflWgR3LXHM/MIA0q8
WqRQ==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=1e100.net; s=20161025;
h=x-gm-message-state:to:auto-submitted:message-id:date:from:subject
:references:in-reply-to;
bh=XaR1H4XeD+InO7mULPJn53omDGmxN+KG6DbSxyyErPM=;
b=iORAzNvXegQ8oSp4RYb/S168muAiBox769seMk49kDBIvXwI+N8P4mUZq/zDi8DmQd
+wlLzVzowQq6EofiSpjOJWT9IC/k8otk15PMGtgHE4BGSSeKn7L30d3ocQS93HzYnLmA
VBlHBdFTKrsfKhe2+CQyCosTDGRpbkQLuRRyhxChEq8ltvaOHgbu1+eCeb9PsPuh6OxH
kvTHJZeA9A+eLOl26pBmqGIWkr7FlYW0wI6YPoEs9WXX5LSFOQs6fm/9l366eIR7IFFI
ihX5LrZl/Cf0lwwYX7fqIMgnHy1K+QnKuEb+dRQGqLbxdIEls9bXIF98iPQVkEWzgSZy
ip8Q==
X-Gm-Message-State: AOAM531ahfHE6oS9/nuni8pNf9bwC+DXAcaLV0owBwNCj9kcTPLCCNhX
W1JNciK0ivEIVB4dgiyLE/5K7iKbEznQhqyG9Bi1QA==
X-Google-Smtp-Source: ABdhPJygljUXswH0ycJyHmXVthi5IjlDvP8QdYlMdHUPKEtgIZeUk69Acti5LnswGhg63T9/L0PuGZGBM5XE5BsP0mMNNDRZyt+DgnE=
X-Received: by 2002:a05:6638:101c:: with SMTP id r28mr2990163jab.84.1591791475516;
Wed, 10 Jun 2020 05:17:55 -0700 (PDT)
Content-Type: multipart/report; boundary="00000000000074432a05a7b9d512"; report-type=delivery-status
To: alice@gmail.com
Received: by 2002:a05:6638:101c:: with SMTP id r28mr3059870jab.84; Wed, 10 Jun
2020 05:17:55 -0700 (PDT)
Return-Path: <>
Auto-Submitted: auto-replied
Message-ID: <5ee0cf73.1c69fb81.6888.c2f4.GMR@mx.google.com>
Date: Wed, 10 Jun 2020 05:17:55 -0700 (PDT)
From: Mail Delivery Subsystem <mailer-daemon@googlemail.com>
Subject: Delivery Status Notification (Failure)
References: <CADWx9Cs32Wa7Gy-gM0bvbq54P_FEHe7UcsAV=yW7sVVW=fiMYQ@mail.gmail.com>
In-Reply-To: <CADWx9Cs32Wa7Gy-gM0bvbq54P_FEHe7UcsAV=yW7sVVW=fiMYQ@mail.gmail.com>
X-Failed-Recipients: assidhfaaspocwaeofi@gmail.com
--00000000000074432a05a7b9d512
Content-Type: multipart/related; boundary="000000000000745e0805a7b9d51b"
--000000000000745e0805a7b9d51b
Content-Type: multipart/alternative; boundary="000000000000745e1705a7b9d51c"
--000000000000745e1705a7b9d51c
Content-Type: text/plain; charset="UTF-8"
** Die Adresse wurde nicht gefunden **
Ihre Nachricht wurde nicht an assidhfaaspocwaeofi@gmail.com zugestellt, weil die Adresse nicht gefunden wurde oder keine E-Mails empfangen kann.
Hier erfahren Sie mehr: https://support.google.com/mail/?p=NoSuchUser
Antwort:
550 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 h20sor9401601jar.6 - gsmtp
--000000000000745e1705a7b9d51c
Content-Type: text/html; charset="UTF-8"
<html>
<head>
<style>
* {
font-family:Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif;
}
</style>
</head>
<body>
<table cellpadding="0" cellspacing="0" class="email-wrapper" style="padding-top:32px;background-color:#ffffff;"><tbody>
<tr><td>
<table cellpadding=0 cellspacing=0><tbody>
<tr><td style="max-width:560px;padding:24px 24px 32px;background-color:#fafafa;border:1px solid #e0e0e0;border-radius:2px">
<img style="padding:0 24px 16px 0;float:left" width=72 height=72 alt="Fehlersymbol" src="cid:icon.png">
<table style="min-width:272px;padding-top:8px"><tbody>
<tr><td><h2 style="font-size:20px;color:#212121;font-weight:bold;margin:0">
Die Adresse wurde nicht gefunden
</h2></td></tr>
<tr><td style="padding-top:20px;color:#757575;font-size:16px;font-weight:normal;text-align:left">
Ihre Nachricht wurde nicht an <a style='color:#212121;text-decoration:none'><b>assidhfaaspocwaeofi@gmail.com</b></a> zugestellt, weil die Adresse nicht gefunden wurde oder keine E-Mails empfangen kann.
</td></tr>
<tr><td style="padding-top:24px;color:#4285F4;font-size:14px;font-weight:bold;text-align:left">
<a style="text-decoration:none" href="https://support.google.com/mail/?p=NoSuchUser">WEITERE INFORMATIONEN</a>
</td></tr>
</tbody></table>
</td></tr>
</tbody></table>
</td></tr>
<tr style="border:none;background-color:#fff;font-size:12.8px;width:90%">
<td align="left" style="padding:48px 10px">
Antwort:<br/>
<p style="font-family:monospace">
550 5.1.1 The email account that you tried to reach does not exist. Please try double-checking the recipient&#39;s email address for typos or unnecessary spaces. Learn more at https://support.google.com/mail/?p=NoSuchUser h20sor9401601jar.6 - gsmtp
</p>
</td>
</tr>
</tbody></table>
</body>
</html>
--000000000000745e1705a7b9d51c--
--000000000000745e0805a7b9d51b
Content-Type: image/png; name="icon.png"
Content-Disposition: attachment; filename="icon.png"
Content-Transfer-Encoding: base64
Content-ID: <icon.png>
iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAYAAADnRuK4AAAACXBIWXMAABYlAAAWJQFJUiTwAAAA
GXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABTdJREFUeNrsnD9sFEcUh5+PRMqZ
yA0SPhAUQAQFUkyTgiBASARo6QApqVIkfdxGFJFSgGhJAUIiBaQB0ZIOKVCkwUgURjIg2fxL4kS+
YDvkbC/388bi8N16Z4/d7J/5PsniuD3fyePP772ZeTsDQRAYQL/UGAJAIEAgQCBAIAAEAgQCBAIE
AkAgyJT3Mv+Eq7vYK8mTE+MDRCAghQECAeRQA5V2ZOpmg5vDx3NPzRbmGRMEcmTrEbNNB8zWfRD+
f/Efs2e3zCZvMjaksBg27TfbcuSNPEKP9ZyuAQKtHX2O9ncNgWC57umMPKvRNb0GEKgnLoUyxTQC
rcns0/6uIRAs8/hGf9cQCJZpTpjdO2f25/03z+mxntM1eLtsZAgiUtX4JcaBCAQIBAgECARQ8CJa
G5jab4J4pm4WZmO3OALVh802fIwcLkyPkcKAGggAgQCBAIEAgQCBABAIEAjKA/1AnahhbO5FdOOY
VsrrDbPBYcYKgf5D2wLaV3p+22xh1u17tO3S+DTcvxvagUDeivPgx/a/95J/73w7Sj26Hn4pKo2M
ehuV/KyBJM6d0f7k6RKx/R63vvL2tmf/ItDdM2ZTP6f7nkp9Y2fDx1v9akmpIU+KSCLVUghUQfSL
zVKeTklbLxGoctw/nzC5rw8L5KRNbkpnKq6pgSqEClzNnFzY+XnYWrt6VpVk1vbwWvg+RKCKMOUw
Q1LEOXA+/MX3mpJvGDHb265xtnzmFoUK1HaKQGlMtePYM+q2KKjXuaS1NJYIEKgI8jhEgqHt4cqy
Ky53j3hyHz2bqSLp2o2LbJ7MxKovkGqXteoWpaOk96O9/yF/dF7NwlS36AuIQIBA5celQK4PIxBE
4LLzrtoLgaALdSy6CJRkWQCBPGLsTHznomZ9nszUECgJ2ml3WWHe+QVFNPSQx6UdZNtxr9pbEShN
eTTz8mQXHoHSlke7+Z+c9m6VGoHSkEfs/trLW3wQKApN1V3lGfnGu2Z6BFoLtYCs3GWBPAiUCLVh
/HoaeRCoT9R873KLM/IgUBfapnCpe5AHgXry4pf412ihEHkQqCdxd5VqrcezhUIESsJMTJ+Pdthp
Z0WgyNlXXPHc2Mc4IVAELl2Gnh8mhUDvCkfbIVAkcbf/aOoO3fMKhqAD3frTa4quwpn0hUDOkQhI
YYBAgECAQAAU0QlYObl+5Ug8NcprZkZxjUCxRPVA6zmtEXHCBykskrhjgHXN09PoEcgFl4M4H11j
nBAoApcj6ZoPGScEAgTKApcDoTw5sgWB+sGlz1n90IBAPdE6j1o21PfcC11jLagL1oFWRyGlKU3p
OxcSJQ7NZAjkhHp/uG2HFAYIBAgECASAQIBAgECAQAAIBOkxEARBtp9wdVfAMOfIifEBIhCQwgCB
ABAI0oV2jhxZ+nfBatuPZfgBCy0Eqqo8c01b+uu51XZvzOgDWoHNTGR+pCwpLEd5svuAZXlO2uEr
PyEQ8hRWHgRCHmqg0sjTnLalv6crJQ8C/U8stqNO0I4+VZOHFIY8COS1PGL2ybd5yUMKK7s8zYmL
dujyd3n+nESgcsvzZd4/KwIhDwIhT35QA6UyE1qyxZnfvJMHgdKS549JC1qvvJOHFIY8CFR5eV5O
XimqPAhUdHnmfx+zgxdOFXkoqIGKKs/cswnb/8Oeog8HEai48nxUhiFBIORBIOShBioskkbySCLk
IQIhDwIhj28p7FApR6b1qlEbHGpkO/rr6215vi/zH1r2x7tApSGFAQIBAgECAQIBIBAgECAQIBBA
LK8FGADCTxYrr+EVJgAAAABJRU5ErkJggg==
--000000000000745e0805a7b9d51b--
--00000000000074432a05a7b9d512
Content-Type: message/delivery-status
Reporting-MTA: dns; googlemail.com
Arrival-Date: Wed, 10 Jun 2020 05:17:55 -0700 (PDT)
X-Original-Message-ID: <CADWx9Cs32Wa7Gy-gM0bvbq54P_FEHe7UcsAV=yW7sVVW=fiMYQ@mail.gmail.com>
Final-Recipient: rfc822; assidhfaaspocwaeofi@gmail.com
Action: failed
Status: 5.1.1
Diagnostic-Code: smtp; 550-5.1.1 The email account that you tried to reach does not exist. Please try
550-5.1.1 double-checking the recipient's email address for typos or
550-5.1.1 unnecessary spaces. Learn more at
550 5.1.1 https://support.google.com/mail/?p=NoSuchUser h20sor9401601jar.6 - gsmtp
Last-Attempt-Date: Wed, 10 Jun 2020 05:17:55 -0700 (PDT)
--00000000000074432a05a7b9d512
Content-Type: message/rfc822
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=gmail.com; s=20161025;
h=mime-version:from:date:message-id:subject:to;
bh=Y1ylbv3YC5frF/LtF2it4tQQ0OVZstDdWqivvggIOB0=;
b=eyr60XbgOrgHoZFpRYzw9WQIR7aEBaYKWhiEcqdnugB+hn0W2KVcTkKiL2C6zSF+jh
l+lM+dNZZTUcMqWx4kVgTVtqwUNea8OUqe+WLqx04ULwdKZn1okbKYovaiavCLKOKDnf
ZP5mNz3Ka/ywpCGoq8rdgnXc7NunnkWeaBpYY/BWOmLU4WNXX8zS7etXXhQE4YPQEJT4
Sh2o/YIIjDLncJFMyE+25n3tbd2mIoLt4sjaCHE5ibm9w7zojyHM+LDCQ37cM74FEAAa
88KTn0gSnCFBCfojhfxOH78CpySHG3FFfTlpCefwP2A5J9MQlb6QdSVa9STYSx3IntJ4
L7Tg==
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=1e100.net; s=20161025;
h=x-gm-message-state:mime-version:from:date:message-id:subject:to;
bh=Y1ylbv3YC5frF/LtF2it4tQQ0OVZstDdWqivvggIOB0=;
b=pBL4/bKUDw5E2zo1uR2Tl69h2iTlMgIAcnzQgodPCbU4jZ9kH+F5H9rfbzXCjT06J7
L72SYpdfgc5fOwM4GhRcdYnyK3wiXQ8ugpL19nbYt2iWo/vRF3GidawXXDGb2GUYpkzX
1Mz531cy2/HOsmQbUQ7304KV+OUghtcg8eLNnFuhQch7n12Kk3yy3AOzjrLoktcdgIsy
/HxBjyut0Au+A2t6si+PVwTHvC647a0BioeV0tUYLigzu3/jgP9Hb8eRZaXTX5VC6iZi
9QMH/+rXp05IK7OpGWh22xDpeV8CDkQ2sLFaBhKxtJ+nYoerM64t8EJXBBsVQb18ojGz
pW/A==
X-Gm-Message-State: AOAM5330q6kn/TKataMNEVigNfNdr/xii/PQgHXzJyMbwLvsETlNfLoy
1rM9JBIGrcHeEDRx4qhZfl5S4bircceU7c3i6Fyn2fRO
X-Google-Smtp-Source: ABdhPJwysG+S90b/g+9mK7LgeHhmJTBowst6JMhL16+a0coTi7P1NVp9jjaNHJfhvhLodYG6eHIvWdbQGJnAP2brEzI=
X-Received: by 2002:a05:6638:101c:: with SMTP id r28mr2990137jab.84.1591791475066;
Wed, 10 Jun 2020 05:17:55 -0700 (PDT)
MIME-Version: 1.0
From: Deltachat Test <alice@gmail.com>
Date: Wed, 10 Jun 2020 14:18:26 +0200
Message-ID: <CADWx9Cs32Wa7Gy-gM0bvbq54P_FEHe7UcsAV=yW7sVVW=fiMYQ@mail.gmail.com>
Subject: test
To: bob@example.org, assidhfaaspocwaeofi@gmail.com
Content-Type: multipart/alternative; boundary="0000000000006d8d7d05a7b9d5b3"
--0000000000006d8d7d05a7b9d5b3
Content-Type: text/plain; charset="UTF-8"
test
--0000000000006d8d7d05a7b9d5b3
Content-Type: text/html; charset="UTF-8"
<div dir="ltr">test<br></div>
--0000000000006d8d7d05a7b9d5b3--
--00000000000074432a05a7b9d512--

View File

@@ -0,0 +1,113 @@
Return-Path: <>
Received: from mout-bounce.gmx.net ([212.227.15.44]) by mx-ha.gmx.net
(mxgmx101 [212.227.17.5]) with ESMTPS (Nemesis) id 1Mr97m-1jC6Y01o86-00oEqk
for <alice@gmx.de>; Tue, 09 Jun 2020 14:35:30 +0200
Received: from localhost by mout-bounce.gmx.net id 0LhiZF-1jDTj11ZoH-00msO3
Tue, 09 Jun 2020 14:35:30 +0200
Date: Tue, 09 Jun 2020 14:35:30 +0200
From: "GMX Mailer Daemon" <mailer-daemon@gmx.de>
To: alice@gmx.de
Subject: Mail delivery failed: returning message to sender
Auto-Submitted: auto-replied
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
X-UI-Out-Filterresults: unknown:0;V03:K0:O8yx6kuPaGQ=:0wIDPNXEr0wX2oNsLnXaWA
==
Envelope-To: <alice@gmx.de>
X-GMX-Antispam: 0 (Mail was not recognized as spam); Detail=V3;
X-Spam-Flag: NO
X-UI-Filterresults: notjunk:1;V03:K0:QcE43EBhMmU=:IC5vvzi9jhPS/698Wuubzw1Q4N
h87X9j9B3CBN0ZKXB67KepwyNHmh9pxmFIUOMimylv7UK9np+j3X55roOd0nX9BmaaZ3Twvqf
UaSsxmyU+cNr6m3+oOb3udJBLe2pJEZDk1cOwACb5NXzYPSaIj4APfGCyvrzIx3FGkNuScNBb
tCbbKUJ0GB/VmJLB34XfF6dNN+Iwv9IQ9Yrvw/VXv9vWKsi3qRGGUt3yRw5jUKhQlBY21Pnoq
m0LqoMbAKfH1tKEQ/5TymH1ei50YKyWzZ89ISkQwkbYLaqN+6meGACpY18j43VCU9Fk4WQR7y
3XvBYh2CO0CnCn+M9VsnasYag2sNrySe9nzyKfRTaxEg8qlJtl7kS4GX/FsxhHPavkqnU62Gl
9V5TxIG7tmIR0Bf11sPzG/WGegoOHxrfz+qYR81llLMOHznpdDRKjsYDtO/rFBGZzYTiCZsrW
dZPVXV25SVcrDGZOaop3JoCbglmXLcSLLhmfE5MzyJEGte3I+6EiZJNeIe8qN3wMDTsRtJL9S
J6b2F/5/kTGVAWnXtNlf69BholCrxvjC4Snt3Xjc+7WIO8iw2c5YjmWy+4bAwd4uWll529hZd
6pUYGwjFRnKleivCaJIt7DqbvbE7GZSbQH8fXm3zYqYTrrxiWdlykdoOA1OGbeM06RHJt3mJB
osZPU8BZKt0OiBOW64vg6gyAsNC0f02EA7dvRWYgFYqlSogfWZQIOKDKibMVHpIaA0foXg4BG
TEQDlsTIL0n2WC9WVqkMdm6xUXHgpArCrAsUhw3mEqPywEfJeBHn60tP2vQ9+pDIQAj5dQCDV
y96qSiCX4p31HfrWwAXB9mHfl4OO/tPcKUGBclj2rZ/NMc4O+7yDedLWXQnRzQExfOJLBbBh3
xgiNlWFHvDLn0pKG9EI1+3wJ7m2GF2jzDtbQTBv9z26DuAq5WbHZHupzeyfP7VCVXcKuB6sG1
3+LWcdYtcXfqT58HwcvDLwowC4uJpiHfHwtVdiGMtHnmYLysp0V425g+vofQfNzBgR3d9JC15
G+HS44o6x6Legm6KnHYH3k0KhR7fgcgswJv/S+I/ryppUhGb2jezVZIUzgvAplzIUDAWnrHdF
KVqZ5wBJ0acShIfgMlsIxnBmcnIQ4R4jq3zAyj4XTFxVUFanU8ySiXubxV5PzJqj+GsVa2sjx
9n/xQRJLwgMC4BYqzP6lEPwg/g5AneDAnl7ZlcQPC4SCMblC8N1KZyyIDTXPOI/o4lfdMYb9P
7DmBp2S8aA2yuDe5XT20OmX3kVWeBOsBaAGvVFpIn7gwIDqnFh9WSMP6mkCwfChN3D1yLquYB
KAODgRZV5lVNmK+eOjW8m2oiRxmfxrjXLtw5PEhn3RkiRN4HnoePJeoYC7SG4EUwg+wYPu3M6
exP/YigoE6bjuBS5d0imUTRDMiwg469GyrFo1J1GkRVvj3lXSF4Nt11j6waqu1l0ReDYfU+QM
EMPGLEh1vRChCaqz4L7YI5FlSAVXxfmst0JRyE4k5r9CToEbZuYlPQ+jbcvptwBSaqzMb/YfT
cCrU2RWHUfmYIy8x8A/t8ScRbYPzs1lTK3yn1hYeXpw8Fgkip6DIIXAJwUUp+2SLcELICIo+p
uumMN0P/OZHH3V/hZ0dPr9xsYi7gdd/vyRIRPUwiL1rSp2WJGi+w4atun8kQBgnbAznRObDh1
4zzKhApX9jo5gtFN6640QDEI5KpsMuPoty4rj9OK163ntKWGR451n+5ZX2FilTlpZYlIPO9Hy
SrjHzBog4texceR1OLh4pb/WFB0XFSjQchPAltXCYQFs82aDDk/A0nOPk=
This message was created automatically by mail delivery software.
A message that you sent could not be delivered to one or more of
its recipients. This is a permanent error. The following address(es)
failed:
snaerituhaeirns@gmail.com:
SMTP 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
try
550-5.1.1 double-checking the recipient's email address for typos or
550-5.1.1 unnecessary spaces. Learn more at
550 5.1.1 https://support.google.com/mail/?p=NoSuchUser f6si2517766wmc.21
9 - gsmtp
--- The header of the original message is following. ---
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=gmx.net;
s=badeba3b8450; t=1591706130;
bh=g3zLYH4xKxcPrHOD18z9YfpQcnk/GaJedfustWU5uGs=;
h=X-UI-Sender-Class:To:From:Subject:Date;
b=NwY6W33mI1bAq6lpr6kbY+LD2hO9cDJBItTgY3NRIT94A6rKTVlSmhFM3AxYgFnj0
Db0hncsNRDqcdtRoOo8Emcah5NJURvEQohG37lkug3GqneB4+FNTdYCeQbOKlZn6on
pYYD/T9CmeL2HG3+8voeBjZIUenyXrF2WXG37hFY=
X-UI-Sender-Class: 01bb95c1-4bf8-414a-932a-4f6e2808ef9c
Received: from [192.168.178.30] ([84.57.126.154]) by mail.gmx.com (mrgmx005
[212.227.17.190]) with ESMTPSA (Nemesis) id 1MKbkM-1jNoq60HKm-00KyL2 for
<snaerituhaeirns@gmail.com>; Tue, 09 Jun 2020 14:35:30 +0200
To: snaerituhaeirns@gmail.com
From: Alice <alice@gmx.de>
Subject: test
Message-ID: <9c9c2a32-056b-3592-c372-d7e8f0bd4bc2@gmx.de>
Date: Tue, 9 Jun 2020 14:36:10 +0200
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101
Thunderbird/68.8.1
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit
Content-Language: de-DE
X-Provags-ID: V03:K1:7awoptmynaF5MqxCAincdsa7zFCwQFNkFb6xRxigK06uEiGN00b
Mv2wJU91CEd4mvCCWzrTtaWLZDLH8pjAWaT4+HvYIUbpwNx2jC6WHTppkYYRMVJnm4lG9pr
SUx1OlIcp0kbsnl3mB9xYNFwm9jzpR9Kx8QEHwIbZiiSFBcH56498UGQi//kKXVMos8C14o
I7cmwYmr8xB09DwLMKXfg==
X-Spam-Flag: NO
X-UI-Out-Filterresults: notjunk:1;V03:K0:NWQAUbhAkBc=:yAaolOnVCDWEZhgUwwvtEs
wXbSJ/GfMvDRpCkYpFBvHXOTpGm6hjdjQ0vLK2hvu/Hz22UdlWbIdc1J2oO9S5U20mIdc+1bS
TPSSpqPFc7ICPx4Wbvv2SEp9ZqH2q7ORC52UvUWfI6OjAJEPDNrXQFdUiZAa72hLj1NPeG6Qi
4AbL2HwLfJ8s6TeOCm6TXRRuD+w1o/ASFOqQmoao2dFyZ2BaoAgOKPKxXYfwVGceuUygpchyS
0d2bZYOXSLR+6rUYevjZAq1OCi9AIC6/wlkOe5yIRk4gJFMfPauaICsdnq3uZ9ikCAX83VWun
PJVMxTLTP54lgo2h0jMBX3uKk10+/wzXWplllxX9NnSa3x1V28n6raslNF0IoC6Pm72kC6Jzr
GkC22viCm3/Y4uHlPMOXbY5WFrQe/D9GKeJeXBLoGciNwIFkUG12a+iqWtoT+h5HVObTW8LxM
+UtEl97nAwxYSM+sGfIpasRpZc7r/SgN3JWGO9R9WaXpW4Cc1dH7RI+hzuZDsDUBEGTUTVPDo
0SvjKHiJ6sUqGDyfv4HUgVutus6EYP27LALND4ekfom2DPRFopZhbtV5fZT7CL1Q1NogU7tYf
/FdmR1T1J1zCAZSFvyR5LBkfglZlHzgdnTF2heuxyqKq4dm0hnLFSULB1+CVWsg8hzrruvO5q
XzA6qIhBQUZmWo7wBpqpkBPxzjgTGGtXc5y5e6+crxYbbuQdnUWEnyw2xI4d6pJPqtHDA2/vT
ZgvNDUGceavTR5Rtyb14hhX4Q6dWK3ATy16j4hs9Aq+q/IKyVAX3A5nFYbJRIz+2YnoLr2YOa
IrScEorXjvTxjw+aBy73SZBe2REPzJ+O6k7chVrYjV9Q28FiGVuRYJYxWw/59Pes7IAbmfQBV
4vqGCQQr4eG78gVwjw+SNdp4/6jdNkIHDqR4XW9id/r5wYxKKj4UUkSor3/+h9Rd9srh+GApy
uOxw/ejFvbRcxFIjvadpq1KLnO7nM27nJ4lp44ul3i7VUGefLM/45TCsuds2HM1iQWhPFQ54y
SA5sYjf73EUJdkHchaf5i+4uSOmbOWQ4Yvmd8+IoyoXAxvEzY2Xh53nWi8ZPY1Tu4Bw8GRrz7
L+VK0QiWCg3/hM7wRlFFyshmMrScMk5fOf9ynqd0JbHB7u+n4/GUwx3im/w8+NgSd3YOz7wNU
KD1snDWoMUO8f23Ik1Osym688OLWNwKYT+mZbMIMXcz1fB+olRZn4czMhN5DiSb8hyOxRI8NE
PNfaoN87CXiRkgazV6U1eiRkfcK2AvI7zOJF1tclUHZ9awyYoXtxfEzZ+J/2TCXiC7V2iSkUF
EjwgPxlJmccccjsxc46v1ajnTxLo0tJbZ0+DJXWkCgQ0d/iiScQ=

View File

@@ -0,0 +1,113 @@
Return-Path: <>
Delivered-To: alice@posteo.org
Received: from proxy02.posteo.name ([127.0.0.1])
by dovecot03.posteo.local (Dovecot) with LMTP id zvCFJRzX317LGQIA+3EWog
for <alice@posteo.org>; Tue, 09 Jun 2020 20:44:24 +0200
Received: from proxy02.posteo.de ([127.0.0.1])
by proxy02.posteo.name (Dovecot) with LMTP id mhNkNAnR316xBQMAGFAyLg
; Tue, 09 Jun 2020 20:44:23 +0200
Received: from mailin06.posteo.de (unknown [10.0.1.6])
by proxy02.posteo.de (Postfix) with ESMTPS id 49hJtv3RRcz11m7
for <alice@posteo.org>; Tue, 9 Jun 2020 20:44:23 +0200 (CEST)
Received: from mx04.posteo.de (mailin06.posteo.de [127.0.0.1])
by mailin06.posteo.de (Postfix) with ESMTPS id 6935920DD2
for <alice@posteo.org>; Tue, 9 Jun 2020 20:44:23 +0200 (CEST)
X-Virus-Scanned: amavisd-new at posteo.de
X-Spam-Flag: NO
X-Spam-Score: -1
X-Spam-Level:
X-Spam-Status: No, score=-1 tagged_above=-1000 required=8
tests=[ALL_TRUSTED=-1] autolearn=disabled
Received: from mout01.posteo.de (mout01.posteo.de [185.67.36.65])
by mx04.posteo.de (Postfix) with ESMTPS id 49hJtv001Vz10kT
for <alice@posteo.org>; Tue, 9 Jun 2020 20:44:22 +0200 (CEST)
Authentication-Results: mx04.posteo.de; dmarc=none (p=none dis=none) header.from=mout01.posteo.de
Received: by mout01.posteo.de (Postfix)
id DCB6B1200DD; Tue, 9 Jun 2020 20:44:22 +0200 (CEST)
Date: Tue, 9 Jun 2020 20:44:22 +0200 (CEST)
From: MAILER-DAEMON@mout01.posteo.de (Mail Delivery System)
Subject: Undelivered Mail Returned to Sender
To: alice@posteo.org
Auto-Submitted: auto-replied
MIME-Version: 1.0
Content-Type: multipart/report; report-type=delivery-status;
boundary="B39111200B9.1591728262/mout01.posteo.de"
Content-Transfer-Encoding: 7bit
Message-Id: <20200609184422.DCB6B1200DD@mout01.posteo.de>
This is a MIME-encapsulated message.
--B39111200B9.1591728262/mout01.posteo.de
Content-Description: Notification
Content-Type: text/plain; charset=us-ascii
This is the mail system at host mout01.posteo.de.
I'm sorry to have to inform you that your message could not
be delivered to one or more recipients. It's attached below.
For further assistance, please send mail to postmaster.
If you do so, please include this problem report. You can
delete your own text from the attached returned message.
The mail system
<hanerthaertidiuea@gmx.de>: host mx01.emig.gmx.net[212.227.17.5] said: 550
Requested action not taken: mailbox unavailable (in reply to RCPT TO
command)
--B39111200B9.1591728262/mout01.posteo.de
Content-Description: Delivery report
Content-Type: message/delivery-status
Reporting-MTA: dns; mout01.posteo.de
X-Postfix-Queue-ID: B39111200B9
X-Postfix-Sender: rfc822; alice@posteo.org
Arrival-Date: Tue, 9 Jun 2020 20:44:22 +0200 (CEST)
Final-Recipient: rfc822; hanerthaertidiuea@gmx.de
Original-Recipient: rfc822;hanerthaertidiuea@gmx.de
Action: failed
Status: 5.0.0
Remote-MTA: dns; mx01.emig.gmx.net
Diagnostic-Code: smtp; 550 Requested action not taken: mailbox unavailable
--B39111200B9.1591728262/mout01.posteo.de
Content-Description: Undelivered Message Headers
Content-Type: text/rfc822-headers
Return-Path: <alice@posteo.org>
Received: from mout01.posteo.de (unknown [10.0.0.65])
by mout01.posteo.de (Postfix) with ESMTPS id B39111200B9
for <hanerthaertidiuea@gmx.de>; Tue, 9 Jun 2020 20:44:22 +0200 (CEST)
Received: from submission-encrypt01.posteo.de (unknown [10.0.0.75])
by mout01.posteo.de (Postfix) with ESMTPS id 8A684160060
for <hanerthaertidiuea@gmx.de>; Tue, 9 Jun 2020 20:44:22 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=posteo.de; s=2017;
t=1591728262; bh=g3zLYH4xKxcPrHOD18z9YfpQcnk/GaJedfustWU5uGs=;
h=To:From:Subject:Date:From;
b=brJnt4PLAX3Tda1RHCo91aB1kMAL/Ku9dmO7D2DD41Zu5ShNsyqqyDkyxb1DsDn3O
6KuBZe3/8gemBuCJ/mxzwd9v8sBnlrV+5afIk0Ye9VvthZsc4HoG79+FiVOi9F38o0
DtJJFYFw/X7mAc5Xyt0B0JvtiTPpBdRAkluUQm+QW6cW6GGlwicVW19qvebzq+sHyP
X2bZ8wpo78yVgvjPBK3DLaXa+pKFMBjLdDUcIE2bZnY6u6F1x8SXGKGBoxVwdJipJx
v14so5IejNsf4LYJjH3Qb8xgK1aAi6e6nQn4YXV0INL6ahzgALiT9N6vwunNKYVJNi
fPPKvBWDfUS4Q==
Received: from customer (localhost [127.0.0.1])
by submission (posteo.de) with ESMTPSA id 49hJtt1WPbz6tmV
for <hanerthaertidiuea@gmx.de>; Tue, 9 Jun 2020 20:44:22 +0200 (CEST)
To: hanerthaertidiuea@gmx.de
From: deltachat <alice@posteo.org>
Subject: test
Message-ID: <04422840-f884-3e37-5778-8192fe22d8e1@posteo.de>
Date: Tue, 9 Jun 2020 20:45:02 +0200
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101
Thunderbird/68.8.1
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit
Content-Language: de-DE
Posteo-User: alice@posteo.org
Posteo-Dkim: ok
--B39111200B9.1591728262/mout01.posteo.de--

View File

@@ -0,0 +1,107 @@
Return-Path: <>
Delivered-To: alice@testrun.org
Received: from hq5.merlinux.eu
by hq5.merlinux.eu (Dovecot) with LMTP id Ye02K6PB5F43cQAAPzvFDg
for <alice@testrun.org>; Sat, 13 Jun 2020 14:08:03 +0200
Received: by hq5.merlinux.eu (Postfix)
id 9EBE627A0B2E; Sat, 13 Jun 2020 14:08:03 +0200 (CEST)
Date: Sat, 13 Jun 2020 14:08:03 +0200 (CEST)
From: MAILER-DAEMON@hq5.merlinux.eu (Mail Delivery System)
Subject: Undelivered Mail Returned to Sender
To: alice@testrun.org
Auto-Submitted: auto-replied
MIME-Version: 1.0
Content-Type: multipart/report; report-type=delivery-status;
boundary="CDB8D27A0B2C.1592050083/hq5.merlinux.eu"
Content-Transfer-Encoding: 8bit
Message-Id: <20200613120803.9EBE627A0B2E@hq5.merlinux.eu>
This is a MIME-encapsulated message.
--CDB8D27A0B2C.1592050083/hq5.merlinux.eu
Content-Description: Notification
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
This is the mail system at host hq5.merlinux.eu.
I'm sorry to have to inform you that your message could not
be delivered to one or more recipients. It's attached below.
For further assistance, please send mail to postmaster.
If you do so, please include this problem report. You can
delete your own text from the attached returned message.
The mail system
<hcksocnsofoejx@five.chat>: host mail.five.chat[195.62.125.103] said: 550 5.1.1
<hcksocnsofoejx@five.chat>: Recipient address rejected: User unknown in
virtual mailbox table (in reply to RCPT TO command)
--CDB8D27A0B2C.1592050083/hq5.merlinux.eu
Content-Description: Delivery report
Content-Type: message/global-delivery-status
Content-Transfer-Encoding: 8bit
Reporting-MTA: dns; hq5.merlinux.eu
X-Postfix-Queue-ID: CDB8D27A0B2C
X-Postfix-Sender: rfc822; alice@testrun.org
Arrival-Date: Sat, 13 Jun 2020 14:08:01 +0200 (CEST)
Final-Recipient: rfc822; hcksocnsofoejx@five.chat
Original-Recipient: rfc822;hcksocnsofoejx@five.chat
Action: failed
Status: 5.1.1
Remote-MTA: dns; mail.five.chat
Diagnostic-Code: smtp; 550 5.1.1 <hcksocnsofoejx@five.chat>: Recipient address
rejected: User unknown in virtual mailbox table
--CDB8D27A0B2C.1592050083/hq5.merlinux.eu
Content-Description: Undelivered Message
Content-Type: message/global
Content-Transfer-Encoding: 8bit
Return-Path: <alice@testrun.org>
Received: from localhost (p200300edb723070079835ce22985a199.dip0.t-ipconnect.de [IPv6:2003:ed:b723:700:7983:5ce2:2985:a199])
by hq5.merlinux.eu (Postfix) with UTF8SMTPSA id CDB8D27A0B2C;
Sat, 13 Jun 2020 14:08:01 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=testrun.org;
s=testrun; t=1592050082;
bh=Kvhta0OMsTRVC7OlaAqo68TBE0KuGBv4vUBp6Ez/7VY=;
h=Subject:References:In-Reply-To:Date:To:From:From;
b=Ql60JEGFXLNvjsyihATw2z34ct++8xZvTPNw0snXe6+oqdqsRZJ9tWNDTxOgx8Iqf
HQ4puBVGcWjIlszYQVLlq3APi04o2ep3GrD8EF0J0GpDdW8yw6wCos6Q8r+TWmXwET
kGXHTRPVaUIqZF2i/utypxMfd1ua0S3jBDnIXTe/p2+XvfC3Cf3hZGW+FQ/Zd7G8Vh
/U2rgX5BTIGf26ZCbmcMaXWkftgv6+yns0AmzorV9yB+EhTkWIUjk+C25bRtMbJ5mZ
93dwdr+sXrrSZLSi+LBqc57Dv9j4p/SUmB4zPlvfUv7/bqLi36pypvtCJ5Ul8UEXSb
XNFZPaEl+mwjA==
Content-Type: text/plain; charset=utf-8
Chat-Disposition-Notification-To: alice@testrun.org
Subject: =?utf-8?q?Message_from_hocuri1=40testrun=2Eorg?=
MIME-Version: 1.0
References: <Mr.VSg3KXFUOTG.9sn7JBxZn1W@testrun.org>
<Mr.F4nR4LnXb6v.gqVbCJRgsmn@testrun.org>
In-Reply-To: <Mr.F4nR4LnXb6v.gqVbCJRgsmn@testrun.org>
Date: Sat, 13 Jun 2020 12:08:01 +0000
X-Mailer: Delta Chat Core 1.35.0/Android 1.9.5
Chat-Version: 1.0
Autocrypt: addr=alice@testrun.org; prefer-encrypt=mutual;
keydata=xjMEXt3z1xYJKwYBBAHaRw8BAQdAf6MctU/8cmEqwEN9VFZ3gHBFIxKiEaARZl1DFUkI7e
rNFTxob2N1cmkxQHRlc3RydW4ub3JnPsKLBBAWCAAzAhkBBQJe3fPXAhsDBAsJCAcGFQgJCgsCAxYC
ARYhBIZ3ajRUkki89+04sgctAtaIXygAAAoJEActAtaIXygAG2IA/1nTmmmkHAc1Bjtx2FOstbaS+N
XHjxaK+hkoWllsyhz0AQDJJ1++u7jVZPRn/j1LlByrT3Jv/D1aY14J5rjj+ADVBM44BF7d89cSCisG
AQQBl1UBBQEBB0DpSTaZ30dAVwM9PkBe2h+gFyxn9HSorP4XCHJu/lIdPAMBCAfCeAQYFggAIAUCXt
3z1wIbDBYhBIZ3ajRUkki89+04sgctAtaIXygAAAoJEActAtaIXygA2QkA/16toWCtseYKw8G1X2j7
xYR3Cyabq37hgbesDOThIIzNAP0UCUS8mnunmkS5adEbftRaDi2JZoGxDw46jtJJ2+13Cw==
Message-ID: <Mr.A7pTA5IgrUA.q4bP41vAJOp@testrun.org>
To: <hcksocnsofoejx@five.chat>
From: <alice@testrun.org>
F
--
Sent with my Delta Chat Messenger: https://delta.chat
--CDB8D27A0B2C.1592050083/hq5.merlinux.eu--

View File

@@ -0,0 +1,91 @@
Return-Path: <>
Delivered-To: alice@tiscali.it
Received: from director-5.mail.tiscali.sys ([10.39.80.174])
by dovecot-08.mail.tiscali.sys with LMTP id SBRfEpGb517VAgAAd2fHbg
for <alice@tiscali.it>; Mon, 15 Jun 2020 16:02:25 +0000
Received: from cmgw-4.mail.tiscali.it ([10.39.80.174])
by director-5.mail.tiscali.sys with LMTP id MFUPL5Cb516tawAArQJVuQ
; Mon, 15 Jun 2020 16:02:25 +0000
Received: from michael.mail.tiscali.it ([213.205.33.246])
by cmgw-4.mail.tiscali.it with
id rTtS2200V5JdeUd01U2RlV; Mon, 15 Jun 2020 16:02:25 +0000
x-cnfs-analysis: v=2.3 cv=ZdPMyfdA c=1 sm=1 tr=0 cx=a_idp_d
a=AfTPebshMYb+aQOCLa9q3Q==:117 a=HpEJnUlJZJkA:10 a=jmdcTMp_Gj4A:10
a=r77TgQKjGQsHNAKrUKIA:9 a=b8iBRs35AAAA:8 a=NMdB-582e605uxHDr_AA:9
a=QEXdDO2ut3YA:10 a=MhhPCb74-dYA:10 a=HXlsH_Kov2KnitTn7A4A:9
a=BkuCPOF3BOzethIN9HQA:9 a=qG5HpJ6ZyD35YNEB:21 a=kvHihYffoorsyJbA:21
a=xD8EQi6zkreDqSNPYj5l:22
Date: Mon, 15 Jun 2020 16:02:25 +0000
From: Mail Delivery System <mail-daemon@smtp.tiscali.it>
To: alice@tiscali.it
Subject: Delivery status notification
MIME-Version: 1.0
Content-Type: multipart/report; boundary="------------I305M09060309060P_896715922369450"
This is a multi-part message in MIME format.
--------------I305M09060309060P_896715922369450
Content-Type: text/plain; charset=UTF-8;
Content-Transfer-Encoding: 8bit
This is an automatically generated Delivery Status Notification.
Delivery to the following recipients was aborted after 2 second(s):
* shenauithz@testrun.org
--------------I305M09060309060P_896715922369450
Content-Type: message/delivery-status; charset=UTF-8;
Content-Transfer-Encoding: 8bit
Reporting-MTA: dns; michael.mail.tiscali.it [213.205.33.13]
Received-From-MTA: dns; localhost [146.241.100.150]
Arrival-Date: Mon, 15 Jun 2020 16:02:23 +0000
Final-recipient: rfc822; shenauithz@testrun.org
Action: failed
Status: 5.1.1
Diagnostic-Code: smtp; 550 5.1.1 <shenauithz@testrun.org>: Recipient address rejected: User unknown in virtual mailbox table
Last-attempt-Date: Mon, 15 Jun 2020 16:02:25 +0000
--------------I305M09060309060P_896715922369450
Content-Type: text/rfc822-headers; Content-Transfer-Encoding: 8bit
Content-Disposition: attachment
x-auth-user: alice@tiscali.it
Chat-Disposition-Notification-To: alice@tiscali.it
Chat-User-Avatar: avatar.jpg
Subject: =?utf-8?q?Message_from_=F0=9F=8F=9E=EF=B8=8F_Mefiscali?=
MIME-Version: 1.0
Date: Mon, 15 Jun 2020 16:02:22 +0000
X-Mailer: Delta Chat Core 1.35.0/Android 1.9.5
Chat-Version: 1.0
Autocrypt: addr=alice@tiscali.it; prefer-encrypt=mutual;
keydata=xjMEXtFRUBYJKwYBBAHaRw8BAQdA5sqHJqkWlveCgsNd0rtwtZrT1mmo1gwaGC5+WheYk5
nNHTxhbmRyZWFzLmxhdHRtYW5uQHRpc2NhbGkuaXQ+wosEEBYIADMCGQEFAl7RUXoCGwMECwkIBwYV
CAkKCwIDFgIBFiEEJUsbRIjZEaRNAs1p1Z5M1vshkrAACgkQ1Z5M1vshkrAAaAEA4wssXeU2IXnowv
iu3zmcNzDgE4HdmW4RFyqJC6bgxXQA/3aTfE/PhQgZvi6RrKMvP4zygXpD9y+3ydIZP88Bp8kIzjgE
XtFRUBIKKwYBBAGXVQEFAQEHQKaAwlP0j9m0aYsCtO+qD9+foH0kiTN5BWDe5YcZrckVAwEIB8J4BB
gWCAAgBQJe0VF6AhsMFiEEJUsbRIjZEaRNAs1p1Z5M1vshkrAACgkQ1Z5M1vshkrA1JgEAkscCQlps
h3ZxlLqBlbf2+85f4S4aGQfFPtIYEkKKhYEBAJbQulNNp9UarvhfyBiIdvkBVDcCnJZwzbORqp8RM0 gC
Message-ID: <Mr.un2NYERi1RM.lbQ5F9q-QyJ@tiscali.it>
To: <shenauithz@testrun.org>
From: =?utf-8?b?8J+Pnu+4jyBNZWZpc2NhbGk=?= <alice@tiscali.it>
Content-Type: multipart/mixed; boundary="5uAmYQux1HZxxriijTjjKSp4DMoJwq"
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=tiscali.it; s=smtp;
t=1592236943; bh=C3taz+zuSre1ko5Q5CzGPmbyrgegKYBClx/3Dv7t/Xw=;
h=Subject:Date:To:From;
b=LrcfLfrQoemOkHTQsqR8MExqNlx5KPYNFWhwlBWylvVc5GlmlhzqM6SAVKd0NVsKE
gVRlBId5FvnlwoJ2WZnXaw/+3lWKilMTuzzQ1oFGvLnZ1XUaUEfuliIv+9NI79dJWX
+S3jsSgzJMJc9+fO6s9bJsX1EHQ2a8GXNbwDtLXs=
--------------I305M09060309060P_896715922369450--

View File

@@ -0,0 +1,100 @@
X-Atlas-Received: from 10.218.250.153 by atlas222.free.mail.ne1.yahoo.com with http; Wed, 10 Jun 2020 10:11:23 +0000
Return-Path: <>
Received: from 77.238.177.145 (EHLO sonic314-19.consmr.mail.ir2.yahoo.com)
by atlas222.free.mail.ne1.yahoo.com with SMTPs; Wed, 10 Jun 2020 10:11:23 +0000
X-Originating-Ip: [77.238.177.145]
Received-SPF: none (domain of sonic314-19.consmr.mail.ir2.yahoo.com does not designate permitted sender hosts)
Authentication-Results: atlas222.free.mail.ne1.yahoo.com;
dkim=pass header.i=@yahoo.com header.s=@bounce;
spf=none smtp.mailfrom=sonic314-19.consmr.mail.ir2.yahoo.com;
dmarc=success(p=REJECT) header.from=yahoo.com;
X-Apparently-To: alice@yahoo.com; Wed, 10 Jun 2020 10:11:23 +0000
X-YMailISG: RT5ZnycWLDvIW52uqHS_EWNgl31NdJPyLLB2F4SYb1GCAoo9
pcninuVU5GDMBZykeMT4cSUt4ZqXxS5FdEeWJqtGIAtbEGbIL8Uhcoszqm4m
JuMJiQZwEE7W_fsS_9MUK5gZtMkhKkSnAuaeaOLKNYAwFZdBqA0uEYA5EmVf
EC9J4RGQ4hZvrMqMj_W.cj4pvbEC.pyirLxTfkICuUkZVguYoxG16y1EOJPw
B48fhXvF5ErU7WAHKxyRM3bMOg7b5pXHKn1dtRSVAXEuqBAQrWig1pePpYH1
wO54sYT7cgmdiFvfLY5rR7YcBzopmKJBycKzBVoRLCY4gvoNyTLPKx9o3AAz
WU4B7TGejDBElYSLpfnyvQg8wU27zzo2IVBZWUNztP0Ca8CQ07Y7TxUZAO.f
DNO5c7nd81PHMRDbSeaw1BTV2Yd9vlBc7syYmwGvtVBJQwRU7qPN.DpFO2jC
9j9DytVhm5231gdBBRSzW78yG.VvaIdJgq_YViKNM9VxFseTz3Sjt3TaYznP
gAVq.MxpopNsSZf_tedwAhXDWyrjKsRPK.v2ANivmuWGPednniEaMYhxJ05M
_5SnJ.hAU.l6h3HCEfiU.SH390_3tZgYNfxCo4GPPFMfnNPmKa3.rgpChBCz
9CRexJ8BSFyCEeAhuqQ8vSJfSuittJmXvS6Tk8Rxd9HUJAtKzZ.xCWZQ4tA6
Yp2aRG23_rK_C6hH8ArkkvbG.uVQTt6DltSX6avJLObBfIhBH0x64RoFjGee
vYXxM741Okm0jH7r79c8GhnAwas_bwfkaTW9e1nhYP0eyI36z_QwLYgOH3Mm
LrUcejpOMDR60QWDuDyRbWXOJdr3Q2K0ERhuAy6YnINq0sL3HX7t5wjsFLvp
_7Ri_eruTfIst4C7DZwERwui6aDSEAdF1Z8oZukBVmiyZsHmhJQCUik646iy
3ASMR3lX7R3q2PBHQo2oC3qte8Fzz1FhKoMtfCGtIpeCazlkhDEJ6eTBSQ3R
Pe7M_GPiv3QNp7qu5CWHlzy6hWEKIkNwx.WRGYzfxkyJMmJm4UrhQYUfa4lG
Wb8n.mfYnS_KGYtzyRFNqAL0IGo.1MB9aG6qQk456Fz9GJgbHLWrMXVtyfrr
Uo7mKih8FCrdUKv5X6KBnpY0vvyoH5jrWyrvo3DW0bq_JvZ9U51JwUhoGY5U
c1t.yCSJbs8tnrGZHuUTOvouWzpCAJsk34AqRyH0wDJZQsAwBW5UZ3jx8ARA
FicoSqZCa4wEP9WaaXvfzFbmLW0-
X-Originating-IP: [77.238.177.145]
Received: from 10.217.135.165 (EHLO sonic314-19.consmr.mail.ir2.yahoo.com) (77.238.177.145)
by mta4277.mail.ne1.yahoo.com with SMTPS; Wed, 10 Jun 2020 10:11:21 +0000
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yahoo.com; s=bounce; t=1591783879; bh=4BmZzBC/nu0AJ9r0i0xNuCENks2KZcuXCbjSHdzbg9Y=; h=Date:From:To:Subject:From:Subject; b=lPxu8goOGOLVgnwbndfdptZ7zI5VEo0lSSr+ONGxwdtuhrySKDU6Sp41/g6jWbAiVPT1947j/B5wOlPfa5tv4XkWrGf0JCbT1I20ZJIkNfNwt4F0qPnbJAiHFIDPxcY68utjC9IgPWJd0cGqJNXbFwbJBu88rtrbMoInzLakh5I=
Received: from sonic.gate.mail.ne1.yahoo.com by sonic314.consmr.mail.ir2.yahoo.com with HTTP; Wed, 10 Jun 2020 10:11:19 +0000
Date: Wed, 10 Jun 2020 10:11:19 +0000
From: MAILER-DAEMON@yahoo.com
To: alice@yahoo.com
Message-ID: <1713051795.39992.1591783879940@sonic314.consmr.mail.ir2.yahoo.com>
Subject: Failure Notice
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 7bit
Content-Length: 3347
Sorry, we were unable to deliver your message to the following address.
<haeclirth.sinoenrat@yahoo.com>:
554: delivery error: dd Not a valid recipient - atlas117.free.mail.ne1.yahoo.com
--- Below this line is a copy of the message.
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yahoo.com; s=s2048; t=1591783876; bh=kXD6TZuQDjqZf/AEAJ2HCX32Titkj3IywytG6GSm4yQ=; h=Date:From:To:Subject:References:From:Subject; b=PexnIBxnVSkyutv/jVn+Wlt5QPuVHnWleP3baWlvqkXaKR51pCaZIoGJggMEonitEeJkcYpgplBawEBz0hlGf63NqOHHkxUk6U1olwc0y9kj5kDH5lrORgXrf7U4z5t+i5n0II36MxbG9n/5tDRXoiabLFoWx//3O9x/ZJvVWPlq8RBFzVG8aoL2TkBQAVcqX/vW1f3WJuopaUYWB4AzR3TyuC2kVQPFqbPMk+G/VmyuZFmZesh1bSBva5hdKYLxES5v8hvTIDRqSYZrnZ4V67MqicZ8m229Xf4Za8qOE+a2Z+Vv5VrQ+CjjPZwRAcmKHkLY80VCSkpeL2R2YG4APg==
X-YMail-OSG: 4spnw98VM1k_g51CM6oepdNEMiPFRtZ0ZG_zOBGIlhcNvS1mkzr8l8VTB0CY_Q0
dR52ikl7QVYESerRQgcGqBfLwKpem7i1XSrCl2HuyHeWzF6Gu5MqFPMCak.v8GyDXNO075NEwNt1
i18CJ29cEjiHthoamgmj0oqerAglglKRhTuuAFy4wUmZZm7VyvaW4wHUD1g7DeWGijQsCglSYMUK
CmoFcKsWOZBSYPMkp7iRwUp52pXHFin3qf4uQ27K_Sh.6s7KLAfWVkV7L_5AR3MyCPAVzm71.1yG
G7Vy5HSBgGMQ90B7VbcjOkCg3F4JNl4Z_P2ejV1KZ.tNoPLgO.FmsfFy1OXBGf3m2mDmRcuEO4K2
mTRhsjZf.2iiWpx02b3tY.oUtYrIBXBVIFPbTB9sBMn_9Z_qdVmO3gjD6gCPEBzuVvEO0eZIrgaw
EDTZt8Z9tSRDm1.4gV8LWQBYShF7XuMV0togiLYIO8s_iTHcTbhKhPlwxP.mxr06xcx_9kzReVTL
9lB1FkB5Jm0WccWHGhLBqeMjGDoaNqPxLqJ.1tI58tLXsPoR6m1NFVEdzI1G.4AVBeXZ_9BjgUhm
KY33sEg.GwIjUlWWWuSyRZ1q1K2nqi1z29wH2R1Glmdmx0lyqfMg9Xe8HV7YZu2CuZ8SlDLLB.rX
NU4PwMsNfU6pK2HejQPsJuyOlI5Q824rXRF5xTLYKsYcQptoFXyLe6MXKW1ThBLQV2nWYDRs_V.e
SBmt22TfuOwu4Y5ju0sXmztZ8zpiIC8_rnAa5bVBEHxzkic64UZdukDX9V12Pk3G2sGYRyPTH472
wBX33JpDuq6BtrKr4FXjCLeVppARTHpiKM0jHMjmNf1bF0TvrcCsC9zAYtitAqgcGZNFETNuV3KM
57XifdDEwUPOuww0ApSWO.iwP2POvIRBVlrxdgA8MbLmuuX4UxNCw23z1f7MVY6F3L60LUrX5GZO
aKaMmD1XTzx32J6c_TUmyuViT5vphqpEooTzHG2X7ALb4xC8yHlE4wDKyaEDARZ.8P2lO9T18oCz
OQvJjwDaLOkeAmo23yRMn70bYJK3tP9Z5cS1C0TE8PEtz4sd1syQUIZZ2g8JG_AQcE4lUZSZlIKN
AHjB8h8Uin35zKe0Le1DBjdQUmpgAETlmYE7V0nJDEmagB3dtpbokgRBuuBfhXlFpxHcnAmBFFFm
XOSLWEPnmxu2o8CCjjz3QUBy2fr3EI_D2VFpy..MuZgRwtES.l24m_95xtQxI28R4SWZN6LsS_rr
1S33BJCCCAfXtCAFzCfz5.qSzHRYbLdY5do6yKj0pPLQTUTjlMwmCUGPcSJhsyxkkEVIK1W_Z16R
ZRls-
Received: from sonic.gate.mail.ne1.yahoo.com by sonic314.consmr.mail.ir2.yahoo.com with HTTP; Wed, 10 Jun 2020 10:11:16 +0000
Date: Wed, 10 Jun 2020 10:11:12 +0000 (UTC)
From: Delta Chat Test <alice@yahoo.com>
To: "haeclirth.sinoenrat@yahoo.com" <haeclirth.sinoenrat@yahoo.com>
Message-ID: <1680295672.3657931.1591783872936@mail.yahoo.com>
Subject: test
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="----=_Part_3657930_145367320.1591783872935"
References: <1680295672.3657931.1591783872936.ref@mail.yahoo.com>
X-Mailer: WebService/1.1.16072 YMailNorrin Mozilla/5.0 (X11; Linux x86_64; rv:77.0) Gecko/20100101 Firefox/77.0
Content-Length: 494
------=_Part_3657930_145367320.1591783872935
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 7bit
test
------=_Part_3657930_145367320.1591783872935
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: 7bit
<html><head></head><body><div class="yahoo-style-wrap" style="font-family:Helvetica Neue, Helvetica, Arial, sans-serif;font-size:16px;"><div dir="ltr" data-setdir="false">test<br></div></div></body></html>
------=_Part_3657930_145367320.1591783872935--