Compare commits

...

147 Commits

Author SHA1 Message Date
Alexander Krotov
b0a1f7b421 Update mailparse to parse names with comma 2020-02-12 23:08:45 +03:00
holger krekel
5c17ec5f01 use new URL format and service provided by mailadm 2020-02-11 22:48:24 +01:00
B. Petersen
8b4edc46a7 implement dc_set_config_from_qr() 2020-02-11 21:04:18 +01:00
B. Petersen
2b7a0a4585 prototype dc_set_config_from_qr() 2020-02-11 21:04:18 +01:00
B. Petersen
1882176489 let dc_check_qr() accept DCACCOUNT-schemes 2020-02-11 21:04:18 +01:00
holger krekel
875e89e71a better event information for moved messages 2020-02-11 20:38:14 +01:00
Alexander Krotov
52520635ea Fix a typo ("requests") 2020-02-11 22:32:35 +03:00
Friedel Ziegelmayer
aa50a9ba83 Merge pull request #1258 from deltachat/cargo-check-ci
GitHub actions: check all packages, examples, tests and features
2020-02-11 11:50:26 +01:00
Alexander Krotov
489e5111ac GitHub actions: check all packages, examples, tests and features 2020-02-11 02:09:47 +03:00
Alexander Krotov
2d4c20af35 Revert "Make dc_receive_imf non-public"
This reverts commit cabb58a9aa.

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

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

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

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

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

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

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

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

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

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

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

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

all this works even when both devices are offline.

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

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

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

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

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

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

View File

@@ -15,6 +15,7 @@ jobs:
- uses: actions-rs/cargo@v1
with:
command: check
args: --workspace --examples --tests --all-features
fmt:
name: Rustfmt

View File

@@ -1,5 +1,18 @@
# Changelog
## 1.0.0-beta.24
- fix oauth2/gmail bug introduced in beta23 (not used in releases) #1219
- fix panic when receiving eg. cyrillic filenames #1216
- delete all consumed secure-join handshake messagess #1209 #1212
- rust-level cleanups #1218 #1217 #1210 #1205
- python-level cleanups #1204 #1202 #1201
## 1.0.0-beta.23
- #1197 fix imap-deletion of messages

138
Cargo.lock generated
View File

@@ -89,41 +89,42 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.1.1"
source = "git+https://github.com/async-email/async-imap?branch=dcc-stable#50e843113e3a67e924a8a14c477833da3ebc1b44"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"async-attributes 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"async-native-tls 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"async-native-tls 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"async-std 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
"byte-pool 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"futures_codec 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"imap-proto 0.9.1 (git+https://github.com/djc/tokio-imap)",
"imap-proto 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"nom 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)",
"rental 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
"stop-token 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"thiserror 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "async-native-tls"
version = "0.1.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"futures-io 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"async-std 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"thiserror 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "async-smtp"
version = "0.1.0"
source = "git+https://github.com/async-email/async-smtp#6a4830032953f06020edc09db8daa06193e2b93f"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"async-native-tls 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"async-native-tls 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"async-std 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"async-trait 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -131,7 +132,6 @@ dependencies = [
"fast_chemail 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)",
"hostname 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"nom 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"pin-project 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -343,6 +343,15 @@ name = "bumpalo"
version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "byte-pool"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"crossbeam-queue 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "byte-tools"
version = "0.3.1"
@@ -358,15 +367,6 @@ name = "byteorder"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bytes"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "bytes"
version = "0.5.3"
@@ -528,6 +528,15 @@ dependencies = [
"scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "crossbeam-queue"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "crossbeam-utils"
version = "0.6.6"
@@ -620,11 +629,11 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.0.0-beta.23"
version = "1.0.0-beta.24"
dependencies = [
"async-imap 0.1.1 (git+https://github.com/async-email/async-imap?branch=dcc-stable)",
"async-native-tls 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"async-smtp 0.1.0 (git+https://github.com/async-email/async-smtp)",
"async-imap 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"async-native-tls 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"async-smtp 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"async-std 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"backtrace 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -633,7 +642,7 @@ dependencies = [
"charset 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"debug_stub_derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"deltachat_derive 1.0.0-beta.22",
"deltachat_derive 2.0.0",
"email 0.0.21 (git+https://github.com/deltachat/rust-email)",
"encoded-words 0.1.0 (git+https://github.com/async-email/encoded-words)",
"escaper 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -647,7 +656,7 @@ dependencies = [
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lettre_email 0.9.2 (git+https://github.com/deltachat/lettre)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"mailparse 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
"mailparse 0.10.4 (git+https://github.com/link2xt/mailparse?branch=address-comma)",
"native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"num-derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -676,20 +685,9 @@ dependencies = [
"thread-local-object 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "deltachat-provider-database"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "deltachat_derive"
version = "1.0.0-beta.22"
version = "2.0.0"
dependencies = [
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -697,10 +695,9 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.0.0-beta.23"
version = "1.0.0-beta.24"
dependencies = [
"deltachat 1.0.0-beta.23",
"deltachat-provider-database 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"deltachat 1.0.0-beta.24",
"failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"human-panic 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1126,17 +1123,6 @@ dependencies = [
"slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "futures_codec"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"pin-project 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "generic-array"
version = "0.12.3"
@@ -1169,11 +1155,6 @@ name = "glob"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "h2"
version = "0.2.1"
@@ -1345,8 +1326,8 @@ dependencies = [
[[package]]
name = "imap-proto"
version = "0.9.1"
source = "git+https://github.com/djc/tokio-imap#de420791b4e240242592616938407b742454c46c"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"nom 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1534,12 +1515,12 @@ dependencies = [
[[package]]
name = "mailparse"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
version = "0.10.4"
source = "git+https://github.com/link2xt/mailparse?branch=address-comma#f5e7a2175966f85f5c079a32c5ab171fc6e9d08e"
dependencies = [
"base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
"charset 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"quoted_printable 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"quoted_printable 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -2092,7 +2073,7 @@ dependencies = [
[[package]]
name = "quoted_printable"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -3260,14 +3241,6 @@ dependencies = [
"zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "yaml-rust"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zeroize"
version = "1.1.0"
@@ -3300,9 +3273,9 @@ dependencies = [
"checksum arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"
"checksum ascii_utils 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
"checksum async-attributes 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "efd3d156917d94862e779f356c5acae312b08fd3121e792c857d7928c8088423"
"checksum async-imap 0.1.1 (git+https://github.com/async-email/async-imap?branch=dcc-stable)" = "<none>"
"checksum async-native-tls 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a29e9e4ed87f4070dd6099d0bd5edfc7692c94442c80d656b50a802c6fde23b7"
"checksum async-smtp 0.1.0 (git+https://github.com/async-email/async-smtp)" = "<none>"
"checksum async-imap 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "46ff8df29e2a90154d85d3c21e843d1f6d9337dcdcf23b3b5a87228d18122c84"
"checksum async-native-tls 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d40a615e861c981117e15c28c577daf9918cabd2e2d588a5e06811ae79c9da1a"
"checksum async-smtp 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3652e5c6072c0694a2bcdb7e8409980d2676bd4f024adf4aab10c68fd2b48f5"
"checksum async-std 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0bf6039b315300e057d198b9d3ab92ee029e31c759b7f1afae538145e6f18a3e"
"checksum async-task 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de6bd58f7b9cc49032559422595c81cbfcf04db2f2133592f70af19e258a1ced"
"checksum async-trait 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "8b6dd385bb33043b833ba049048d57bdbb4d654a121ed68c71871ca51ff67070"
@@ -3326,10 +3299,10 @@ dependencies = [
"checksum buf_redux 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
"checksum bufstream 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
"checksum bumpalo 3.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5fb8038c1ddc0a5f73787b130f4cc75151e96ed33e417fde765eb5a81e3532f4"
"checksum byte-pool 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9342e102eac8b1879fbedf9a7e0572c40b0cc5805b663c4d4ca791cae0bae221"
"checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
"checksum bytecount 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b92204551573580e078dc80017f36a213eb77a0450e4ddd8cfa0f3f2d1f0178f"
"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5"
"checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c"
"checksum bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "10004c15deb332055f7a4a208190aed362cf9a7c2f6ab70a305fba50e1105f38"
"checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb"
"checksum cargo_metadata 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e5d1b4d380e1bab994591a24c2bdd1b054f64b60bef483a8c598c7c345bc3bbe"
@@ -3351,6 +3324,7 @@ dependencies = [
"checksum crossbeam-channel 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "acec9a3b0b3559f15aee4f90746c4e5e293b701c0f7d3925d24e01645267b68c"
"checksum crossbeam-deque 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3aa945d63861bfe624b55d153a39684da1e8c0bc8fba932f7ee3a3c16cea3ca"
"checksum crossbeam-epoch 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5064ebdbf05ce3cb95e45c8b086f72263f4166b29b97f6baff7ef7fe047b55ac"
"checksum crossbeam-queue 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db"
"checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6"
"checksum crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce446db02cdc3165b94ae73111e570793400d0794e46125cc4056c81cbb039f4"
"checksum ctor 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cd8ce37ad4184ab2ce004c33bf6379185d3b1c95801cab51026bd271bf68eedc"
@@ -3360,7 +3334,6 @@ dependencies = [
"checksum darling_macro 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
"checksum debug_stub_derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "496b7f8a2f853313c3ca370641d7ff3e42c32974fdccda8f0684599ed0a3ff6b"
"checksum deflate 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)" = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4"
"checksum deltachat-provider-database 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "814dba060d9fdc7a989fccdc4810ada9d1c7a1f09131c78e42412bc6c634b93b"
"checksum derive_builder 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0"
"checksum derive_builder_core 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef"
"checksum des 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "74ba5f1b5aee9772379c2670ba81306e65a93c0ee3caade7a1d22b188d88a3af"
@@ -3412,12 +3385,10 @@ dependencies = [
"checksum futures-timer 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a1de7508b218029b0f01662ed8f61b1c964b3ae99d6f25462d0f55a595109df6"
"checksum futures-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c0d66274fb76985d3c62c886d1da7ac4c0903a8c9f754e8fe0f35a6a6cc39e76"
"checksum futures-util-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)" = "5ce968633c17e5f97936bd2797b6e38fb56cf16a7422319f7ec2e30d3c470e8d"
"checksum futures_codec 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "793d2283ff61ffff52d51cc631be0c8e75370d96056a38e09f124a67263913da"
"checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
"checksum getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407"
"checksum gif 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)" = "471d90201b3b223f3451cd4ad53e34295f16a1df17b1edf3736d47761c3981af"
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
"checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
"checksum h2 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9433d71e471c1736fd5a61b671fc0b148d7a2992f666c958d03cd8feb3b88d1"
"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
"checksum hermit-abi 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f629dc602392d3ec14bfc8a09b5e644d7ffd725102b48b81e59f90f2633621d7"
@@ -3434,7 +3405,7 @@ dependencies = [
"checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
"checksum image 0.22.4 (registry+https://github.com/rust-lang/crates.io-index)" = "53cb19c4e35102e5c6fb9ade5e0e236c5588424dc171a849af3141bf0b47768a"
"checksum image-meta 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b00861cbbb254a627d8acc0cec786b484297d896ab8f20fdc8e28536a3e918ef"
"checksum imap-proto 0.9.1 (git+https://github.com/djc/tokio-imap)" = "<none>"
"checksum imap-proto 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "16a6def1d5ac8975d70b3fd101d57953fe3278ef2ee5d7816cba54b1d1dfc22f"
"checksum indexmap 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712d7b3ea5827fcb9d4fda14bf4da5f136f0db2ae9c8f4bd4e2d1c6fde4e6db2"
"checksum inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff"
"checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
@@ -3458,7 +3429,7 @@ dependencies = [
"checksum lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
"checksum lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084"
"checksum mach 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "86dd2487cdfea56def77b88438a2c915fb45113c5319bfe7e14306ca4cd0b0e1"
"checksum mailparse 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "04c64e94d3a5d02abe0cf459507689d70fb3b9b1d5fea5773a9f5273117959fb"
"checksum mailparse 0.10.4 (git+https://github.com/link2xt/mailparse?branch=address-comma)" = "<none>"
"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
"checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
"checksum md-5 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a18af3dcaf2b0219366cdb4e2af65a6101457b415c3d1a5c71dd9c2b7c77b9c8"
@@ -3516,7 +3487,7 @@ dependencies = [
"checksum quick-xml 0.17.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fe1e430bdcf30c9fdc25053b9c459bb1a4672af4617b6c783d7d91dc17c6bbb0"
"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
"checksum quoted_printable 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "86cedf331228892e747bb85beb130b6bb23fc628c40dde9ea01eb6becea3c798"
"checksum quoted_printable 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "47b080c5db639b292ac79cbd34be0cfc5d36694768d8341109634d90b86930e2"
"checksum r2d2 0.8.7 (registry+https://github.com/rust-lang/crates.io-index)" = "22b5c5fc5fba064373f03887337b412e0e1562d63023393db77251146cb75553"
"checksum r2d2_sqlite 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b061f5b16692bbe81eeb260f92e6fc7d13aea455c4cbe67f5c4aa20aa92d1d9e"
"checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
@@ -3650,6 +3621,5 @@ dependencies = [
"checksum winutil 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e"
"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
"checksum x25519-dalek 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "637ff90c9540fa3073bb577e65033069e4bae7c79d49d74aa3ffdf5342a53217"
"checksum yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d"
"checksum zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8"
"checksum zeroize_derive 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2"

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.0.0-beta.23"
version = "1.0.0-beta.24"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -16,7 +16,6 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
deltachat = { path = "../", default-features = false }
deltachat-provider-database = "0.2.1"
libc = "0.2"
human-panic = "1.0.1"
num-traits = "0.2.6"

View File

@@ -403,6 +403,7 @@ int dc_set_config (dc_context_t* context, const char*
*/
char* dc_get_config (dc_context_t* context, const char* key);
/**
* Set stock string translation.
*
@@ -417,6 +418,22 @@ char* dc_get_config (dc_context_t* context, const char*
int dc_set_stock_translation(dc_context_t* context, uint32_t stock_id, const char* stock_msg);
/**
* Set configuration values from a QR code containing an account.
* Before this function is called, dc_check_qr() should confirm the type of the
* QR code is DC_QR_ACCOUNT.
*
* Internally, the function will call dc_set_config()
* at least with the keys `addr` and `mail_pw`.
*
* @memberof dc_context_t
* @param context The context object
* @param qr scanned QR code
* @return int (==0 on error, 1 on success)
*/
int dc_set_config_from_qr (dc_context_t* context, const char* qr);
/**
* Get information about the context.
*
@@ -852,6 +869,27 @@ void dc_interrupt_smtp_idle (dc_context_t* context);
void dc_maybe_network (dc_context_t* context);
/**
* Save a keypair as the default keys for the user.
*
* This API is only for testing purposes and should not be used as part of a
* normal application, use the import-export APIs instead.
*
* This saves a public/private keypair as the default keypair in the context.
* It allows avoiding having to generate a secret key for unittests which need
* one.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @param addr The email address of the user. This must match the
* configured_addr setting of the context as well as the UID of the key.
* @param public_data The public key as base64.
* @param secret_data The secret key as base64.
* @return 1 on success, 0 on failure.
*/
int dc_preconfigure_keypair (dc_context_t* context, const char *addr, const char *public_data, const char *secret_data);
// handle chatlists
#define DC_GCL_ARCHIVED_ONLY 0x01
@@ -1556,6 +1594,22 @@ int dc_set_chat_name (dc_context_t* context, uint32_t ch
int dc_set_chat_profile_image (dc_context_t* context, uint32_t chat_id, const char* image);
/**
* Set mute duration of a chat.
*
* This value can be checked by the ui upon receiving a new message to decide whether it should trigger an notification.
*
* Sends out #DC_EVENT_CHAT_MODIFIED.
*
* @memberof dc_context_t
* @param chat_id The chat ID to set the mute duration.
* @param duration The duration (0 for no mute, -1 for forever mute, everything else is is the relative mute duration from now in seconds)
* @param context The context as created by dc_context_new().
* @return 1=success, 0=error
*/
int dc_set_chat_mute_duration (dc_context_t* context, uint32_t chat_id, int64_t duration);
// handle messages
/**
@@ -1756,7 +1810,7 @@ uint32_t dc_create_contact (dc_context_t* context, const char*
* Trying to add email-addresses that are already in the contact list,
* results in updating the name unless the name was changed manually by the user.
* If any email-address or any name is really updated,
* the event DC_EVENT_CONTACTS_CHANGED is sent.
* the event #DC_EVENT_CONTACTS_CHANGED is sent.
*
* To add a single contact entered by the user, you should prefer dc_create_contact(),
* however, for adding a bunch of addresses, this function is _much_ faster.
@@ -2074,6 +2128,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_FPR_OK 210 // id=contact
#define DC_QR_FPR_MISMATCH 220 // id=contact
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
#define DC_QR_ACCOUNT 250 // text1=domain
#define DC_QR_ADDR 320 // id=contact
#define DC_QR_TEXT 330 // text1=text
#define DC_QR_URL 332 // text1=URL
@@ -2091,12 +2146,12 @@ void dc_stop_ongoing_process (dc_context_t* context);
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::test1=Formatted fingerprint
* - DC_QR_ACCOUNT allows creation of an account, dc_lot_t::text1=domain
* - DC_QR_ADDR with dc_lot_t::id=Contact ID
* - DC_QR_TEXT with dc_lot_t::text1=Text
* - DC_QR_URL with dc_lot_t::text1=URL
* - DC_QR_ERROR with dc_lot_t::text1=Error string
*
*
* @memberof dc_context_t
* @param context The context object.
* @param qr The text of the scanned QR code.
@@ -2107,20 +2162,22 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
/**
* Get QR code text that will offer an secure-join verification.
* Get QR code text that will offer an Setup-Contact or Verified-Group invitation.
* The QR code is compatible to the OPENPGP4FPR format
* so that a basic fingerprint comparison also works eg. with OpenKeychain.
*
* The scanning device will pass the scanned content to dc_check_qr() then;
* if this function returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP
* if dc_check_qr() returns DC_QR_ASK_VERIFYCONTACT or DC_QR_ASK_VERIFYGROUP
* an out-of-band-verification can be joined using dc_join_securejoin()
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id If set to a group-chat-id,
* the group-join-protocol is offered in the QR code;
* the Verified-Group-Invite protocol is offered in the QR code;
* works for verified groups as well as for normal groups.
* If set to 0, the setup-Verified-contact-protocol is offered in the QR code.
* If set to 0, the Setup-Contact protocol is offered in the QR code.
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
* @return Text that should go to the QR code,
* On errors, an empty QR code is returned, NULL is never returned.
* The returned string must be released using dc_str_unref() after usage.
@@ -2129,13 +2186,29 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
/**
* Join an out-of-band-verification initiated on another device with dc_get_securejoin_qr().
* Continue a Setup-Contact or Verified-Group-Invite protocol
* started on another device with dc_get_securejoin_qr().
* This function is typically called when dc_check_qr() returns
* lot.state=DC_QR_ASK_VERIFYCONTACT or lot.state=DC_QR_ASK_VERIFYGROUP.
*
* This function takes some time and sends and receives several messages.
* You should call it in a separate thread; if you want to abort it, you should
* call dc_stop_ongoing_process().
* Depending on the given QR code,
* this function may takes some time and sends and receives several messages.
* Therefore, you should call it always in a separate thread;
* if you want to abort it, you should call dc_stop_ongoing_process().
*
* - If the given QR code starts the Setup-Contact protocol,
* the function typically returns immediately
* and the handshake runs in background.
* Subsequent calls of dc_join_securejoin() will abort unfinished tasks.
* The returned chat is the one-to-one opportunistic chat.
* When the protocol has finished, an info-message is added to that chat.
* - If the given QR code starts the Verified-Group-Invite protocol,
* the function waits until the protocol has finished.
* This is because the verified group is not opportunistic
* and can be created only when the contacts have verified each other.
*
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
*
* @memberof dc_context_t
* @param context The context object
@@ -2143,6 +2216,9 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
* to dc_check_qr().
* @return Chat-id of the joined chat, the UI may redirect to the this chat.
* If the out-of-band verification failed or was aborted, 0 is returned.
* A returned chat-id does not guarantee that the chat or the belonging contact is verified.
* If needed, this be checked with dc_chat_is_verified() and dc_contact_is_verified(),
* however, in practise, the UI will just listen to #DC_EVENT_CONTACTS_CHANGED unconditionally.
*/
uint32_t dc_join_securejoin (dc_context_t* context, const char* qr);
@@ -2877,6 +2953,26 @@ int dc_chat_is_verified (const dc_chat_t* chat);
int dc_chat_is_sending_locations (const dc_chat_t* chat);
/**
* Check whether the chat is currently muted
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=muted, 0=not muted
*/
int dc_chat_is_muted (const dc_chat_t* chat);
/**
* Get the exact state of the mute of a chat
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return 0=not muted, -1=forever muted, (x>0)=remaining seconds until the mute is lifted
*/
int64_t dc_chat_get_remaining_mute_duration (const dc_chat_t* chat);
/**
* @class dc_msg_t
*
@@ -3665,30 +3761,19 @@ int dc_contact_is_verified (dc_contact_t* contact);
*/
/**
* Create a provider struct for the given domain.
*
* @memberof dc_provider_t
* @param domain The domain to get provider info for.
* @return a dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
*/
dc_provider_t* dc_provider_new_from_domain (const char* domain);
/**
* Create a provider struct for the given email address.
*
* The provider is extracted from the email address and it's information is returned.
*
* @memberof dc_provider_t
* @param context The context object as created by dc_context_new().
* @param email The user's email address to extract the provider info form.
* @return a dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
*/
dc_provider_t* dc_provider_new_from_email (const char* email);
dc_provider_t* dc_provider_new_from_email (const dc_context_t*, const char* email);
/**
@@ -3698,53 +3783,35 @@ dc_provider_t* dc_provider_new_from_email (const char* email);
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string which must be released using dc_str_unref().
* @return String with a fully-qualified URL,
* if there is no such URL, an empty string is returned, NULL is never returned.
* The returned value must be released using dc_str_unref().
*/
char* dc_provider_get_overview_page (const dc_provider_t* provider);
/**
* The provider's name.
* Get hints to be shown to the user on the login screen.
* Depending on the @ref DC_PROVIDER_STATUS returned by dc_provider_get_status(),
* the ui may want to highlight the hint.
*
* The name of the provider, e.g. "POSTEO".
* Moreover, the ui should display a "More information" link
* that forwards to the url returned by dc_provider_get_overview_page().
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string which must be released using dc_str_unref().
* @return A string with the hint to show to the user, may contain multiple lines,
* if there is no such hint, an empty string is returned, NULL is never returned.
* The returned value must be released using dc_str_unref().
*/
char* dc_provider_get_name (const dc_provider_t* provider);
/**
* The markdown content of the providers page.
*
* This contains the preparation steps or additional information if the status
* is @ref DC_PROVIDER_STATUS_BROKEN.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string which must be released using dc_str_unref().
*/
char* dc_provider_get_markdown (const dc_provider_t* provider);
/**
* Date of when the state was last checked/updated.
*
* This is returned as a string.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
* @return A string which must be released using dc_str_unref().
*/
char* dc_provider_get_status_date (const dc_provider_t* provider);
char* dc_provider_get_before_login_hint (const dc_provider_t* provider);
/**
* Whether DC works with this provider.
*
* Can be one of @ref DC_PROVIDER_STATUS_OK, @ref
* DC_PROVIDER_STATUS_PREPARATION and @ref DC_PROVIDER_STATUS_BROKEN.
* Can be one of #DC_PROVIDER_STATUS_OK,
* #DC_PROVIDER_STATUS_PREPARATION or #DC_PROVIDER_STATUS_BROKEN.
*
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
@@ -3759,7 +3826,7 @@ int dc_provider_get_status (const dc_provider_t* prov
* @memberof dc_provider_t
* @param provider The dc_provider_t struct.
*/
void dc_provider_unref (const dc_provider_t* provider);
void dc_provider_unref (dc_provider_t* provider);
/**
@@ -4347,7 +4414,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* Contact(s) created, renamed, blocked or deleted.
* Contact(s) created, renamed, verified, blocked or deleted.
*
* @param data1 (int) If not 0, this is the contact_id of an added contact that should be selected.
* @param data2 0
@@ -4485,23 +4552,43 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
*/
/**
* Provider status returned by dc_provider_get_status().
* Prover works out-of-the-box.
* This provider status is returned for provider where the login
* works by just entering the name or the email-address.
*
* Works right out of the box without any preperation steps needed
* - There is no need for the user to do any special things
* (enable IMAP or so) in the provider's webinterface or at other places.
* - There is no need for the user to enter advanced settings;
* server, port etc. are known by the core.
*
* The status is returned by dc_provider_get_status().
*/
#define DC_PROVIDER_STATUS_OK 1
/**
* Provider status returned by dc_provider_get_status().
* Provider works, but there are preparations needed.
*
* Works, but preparation steps are needed
* - The user has to do some special things as "Enable IMAP in the Webinterface",
* what exactly, is described in the string returnd by dc_provider_get_before_login_hints()
* and, typically more detailed, in the page linked by dc_provider_get_overview_page().
* - There is no need for the user to enter advanced settings;
* server, port etc. should be known by the core.
*
* The status is returned by dc_provider_get_status().
*/
#define DC_PROVIDER_STATUS_PREPARATION 2
/**
* Provider status returned by dc_provider_get_status().
* Provider is not working.
* This provider status is returned for providers
* that are known to not work with Delta Chat.
* The ui should block logging in with this provider.
*
* Doesn't work (too unstable to use falls also in this category)
* More information about that is typically provided
* in the string returned by dc_provider_get_before_login_hints()
* and in the page linked by dc_provider_get_overview_page().
*
* The status is returned by dc_provider_get_status().
*/
#define DC_PROVIDER_STATUS_BROKEN 3

View File

@@ -20,14 +20,17 @@ use std::fmt::Write;
use std::ptr;
use std::str::FromStr;
use std::sync::RwLock;
use std::time::{Duration, SystemTime};
use libc::uintptr_t;
use num_traits::{FromPrimitive, ToPrimitive};
use deltachat::chat::ChatId;
use deltachat::chat::MuteDuration;
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::stock::StockMessage;
use deltachat::*;
@@ -94,6 +97,14 @@ impl ContextWrapper {
self.translate_cb(Event::Error(msg.to_string()));
}
/// Log a warning on the FFI context.
///
/// Like [error] but logs as a warning which only goes to the
/// logfile rather than being shown directly to the user.
unsafe fn warning(&self, msg: &str) {
self.translate_cb(Event::Warning(msg.to_string()));
}
/// Unlock the context and execute a closure with it.
///
/// This unlocks the context and gets a read lock. The Rust
@@ -109,17 +120,33 @@ impl ContextWrapper {
/// the appropriate return value for an error return since this
/// differs for various functions on the FFI API: sometimes 0,
/// NULL, an empty string etc.
fn with_inner<T, F>(&self, ctxfn: F) -> Result<T, ()>
///
/// Prefer to use [ContextWrapper::try_inner], we might want to
/// remove this function at some point to reduce the cognitive
/// overload of having two functions which are too similar.
unsafe fn with_inner<T, F>(&self, ctxfn: F) -> Result<T, ()>
where
F: FnOnce(&Context) -> T,
{
self.try_inner(|ctx| Ok(ctxfn(ctx))).map_err(|err| {
self.warning(&err.to_string());
})
}
/// Unlock the context and execute a closure with it.
///
/// This is like [ContextWrapper::with_inner] but uses
/// [failure::Error] as error type. This allows you to write a
/// closure which could produce many errors, use the `?` operator
/// to return them and handle them all as the return of this call.
fn try_inner<T, F>(&self, ctxfn: F) -> Result<T, failure::Error>
where
F: FnOnce(&Context) -> Result<T, failure::Error>,
{
let guard = self.inner.read().unwrap();
match guard.as_ref() {
Some(ref ctx) => Ok(ctxfn(ctx)),
None => {
unsafe { self.error("context not open") };
Err(())
}
Some(ref ctx) => ctxfn(ctx),
None => Err(failure::err_msg("context not open")),
}
}
@@ -396,6 +423,28 @@ pub unsafe extern "C" fn dc_set_stock_translation(
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_config_from_qr(
context: *mut dc_context_t,
qr: *mut libc::c_char,
) -> libc::c_int {
if context.is_null() || qr.is_null() {
eprintln!("ignoring careless call to dc_set_config_from_qr");
return 0;
}
let qr = to_string_lossy(qr);
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| match qr::set_config_from_qr(ctx, &qr) {
Ok(()) => 1,
Err(err) => {
error!(ctx, "Failed to create account from QR code: {}", err);
0
}
})
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_info(context: *mut dc_context_t) -> *mut libc::c_char {
if context.is_null() {
@@ -665,6 +714,35 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
.unwrap_or(())
}
#[no_mangle]
pub unsafe extern "C" fn dc_preconfigure_keypair(
context: *mut dc_context_t,
addr: *const libc::c_char,
public_data: *const libc::c_char,
secret_data: *const libc::c_char,
) -> i32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_preconfigure_keypair()");
return 0;
}
let ffi_context = &*context;
ffi_context
.try_inner(|ctx| {
let addr = dc_tools::EmailAddress::new(&to_string_lossy(addr))?;
let public = key::SignedPublicKey::from_base64(&to_string_lossy(public_data))?;
let secret = key::SignedSecretKey::from_base64(&to_string_lossy(secret_data))?;
let keypair = key::KeyPair {
addr,
public,
secret,
};
key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default)?;
Ok(1)
})
.log_warn(ffi_context, "Failed to save keypair")
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_chatlist(
context: *mut dc_context_t,
@@ -836,7 +914,7 @@ pub unsafe extern "C" fn dc_set_draft(
Some(&mut ffi_msg.message)
};
ffi_context
.with_inner(|ctx| chat::set_draft(ctx, ChatId::new(chat_id), msg))
.with_inner(|ctx| ChatId::new(chat_id).set_draft(ctx, msg))
.unwrap_or(())
}
@@ -911,7 +989,7 @@ pub unsafe extern "C" fn dc_get_draft(context: *mut dc_context_t, chat_id: u32)
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| match chat::get_draft(ctx, ChatId::new(chat_id)) {
.with_inner(|ctx| match ChatId::new(chat_id).get_draft(ctx) {
Ok(Some(draft)) => {
let ffi_msg = MessageWrapper {
context,
@@ -966,7 +1044,7 @@ pub unsafe extern "C" fn dc_get_msg_cnt(context: *mut dc_context_t, chat_id: u32
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| chat::get_msg_cnt(ctx, ChatId::new(chat_id)) as libc::c_int)
.with_inner(|ctx| ChatId::new(chat_id).get_msg_cnt(ctx) as libc::c_int)
.unwrap_or(0)
}
@@ -981,7 +1059,7 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
}
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| chat::get_fresh_msg_cnt(ctx, ChatId::new(chat_id)) as libc::c_int)
.with_inner(|ctx| ChatId::new(chat_id).get_fresh_msg_cnt(ctx) as libc::c_int)
.unwrap_or(0)
}
@@ -1145,7 +1223,8 @@ pub unsafe extern "C" fn dc_archive_chat(
};
ffi_context
.with_inner(|ctx| {
chat::archive(ctx, ChatId::new(chat_id), archive)
ChatId::new(chat_id)
.set_archived(ctx, archive)
.log_err(ctx, "Failed archive chat")
.unwrap_or(())
})
@@ -1161,7 +1240,8 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32
let ffi_context = &*context;
ffi_context
.with_inner(|ctx| {
chat::delete(ctx, ChatId::new(chat_id))
ChatId::new(chat_id)
.delete(ctx)
.log_err(ctx, "Failed chat delete")
.unwrap_or(())
})
@@ -1351,6 +1431,37 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_mute_duration(
context: *mut dc_context_t,
chat_id: u32,
duration: i64,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_set_chat_mute_duration()");
return 0;
}
let ffi_context = &*context;
let muteDuration = match duration {
0 => MuteDuration::NotMuted,
-1 => MuteDuration::Forever,
n if n > 0 => MuteDuration::Until(SystemTime::now() + Duration::from_secs(duration as u64)),
_ => {
ffi_context.warning(
"dc_chat_set_mute_duration(): Can not use negative duration other than -1",
);
return 0;
}
};
ffi_context
.with_inner(|ctx| {
chat::set_muted(ctx, ChatId::new(chat_id), muteDuration)
.map(|_| 1)
.unwrap_or_log_default(ctx, "Failed to set mute duration")
})
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_msg_info(
context: *mut dc_context_t,
@@ -1753,7 +1864,9 @@ pub unsafe extern "C" fn dc_imex_has_backup(
.with_inner(|ctx| match imex::has_backup(ctx, to_string_lossy(dir)) {
Ok(res) => res.strdup(),
Err(err) => {
error!(ctx, "dc_imex_has_backup: {}", err);
// do not bubble up error to the user,
// the ui will expect that the file does not exist or cannot be accessed
warn!(ctx, "dc_imex_has_backup: {}", err);
ptr::null_mut()
}
})
@@ -2423,6 +2536,37 @@ pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> l
ffi_chat.chat.is_sending_locations() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_muted(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_muted()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_muted() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_get_remaining_mute_duration(chat: *mut dc_chat_t) -> i64 {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_get_remaining_mute_duration()");
return 0;
}
let ffi_chat = &*chat;
if !ffi_chat.chat.is_muted() {
return 0;
}
// If the chat was muted to before the epoch, it is not muted.
match ffi_chat.chat.mute_duration {
MuteDuration::NotMuted => 0,
MuteDuration::Forever => -1,
MuteDuration::Until(when) => when
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_get_info_json(
context: *mut dc_context_t,
@@ -3154,11 +3298,21 @@ pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
libc::free(s as *mut _)
}
pub mod providers;
pub trait ResultExt<T, E> {
fn unwrap_or_log_default(self, context: &context::Context, message: &str) -> T;
fn log_err(self, context: &context::Context, message: &str) -> Result<T, E>;
/// Log a warning to a [ContextWrapper] for an [Err] result.
///
/// Does nothing for an [Ok]. This is usually preferable over
/// [ResultExt::log_err] because warnings go to the logfile and
/// errors are displayed directly to the user. Usually problems
/// on the FFI layer are coding errors and not errors which need
/// to be displayed to the user.
///
/// You can do this as soon as the wrapper exists, it does not
/// have to be open (which is required for the `warn!()` macro).
fn log_warn(self, wrapper: &ContextWrapper, message: &str) -> Result<T, E>;
}
impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
@@ -3178,6 +3332,15 @@ impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
err
})
}
fn log_warn(self, wrapper: &ContextWrapper, message: &str) -> Result<T, E> {
self.map_err(|err| {
unsafe {
wrapper.warning(&format!("{}: {}", message, err));
}
err
})
}
}
unsafe fn strdup_opt(s: Option<impl AsRef<str>>) -> *mut libc::c_char {
@@ -3210,3 +3373,68 @@ fn convert_and_prune_message_ids(msg_ids: *const u32, msg_cnt: libc::c_int) -> V
msg_ids
}
// dc_provider_t
#[no_mangle]
pub type dc_provider_t = provider::Provider;
#[no_mangle]
pub unsafe extern "C" fn dc_provider_new_from_email(
context: *const dc_context_t,
addr: *const libc::c_char,
) -> *const dc_provider_t {
if context.is_null() || addr.is_null() {
eprintln!("ignoring careless call to dc_provider_new_from_email()");
return ptr::null();
}
let addr = to_string_lossy(addr);
match provider::get_provider_info(addr.as_str()) {
Some(provider) => provider,
None => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_overview_page(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_get_overview_page()");
return "".strdup();
}
let provider = &*provider;
provider.overview_page.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_before_login_hint(
provider: *const dc_provider_t,
) -> *mut libc::c_char {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_get_before_login_hint()");
return "".strdup();
}
let provider = &*provider;
provider.before_login_hint.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t) -> libc::c_int {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_get_status()");
return 0;
}
let provider = &*provider;
provider.status as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_unref()");
return;
}
// currently, there is nothing to free, the provider info is a static object.
// this may change once we start localizing string.
}

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ use std::str::FromStr;
use deltachat::chat::{self, Chat, ChatId};
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::constants::*;
use deltachat::contact::*;
use deltachat::context::*;
@@ -19,6 +18,7 @@ use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
use deltachat::Event;
use deltachat::{config, provider};
/// Reset database tables.
/// Argument is a bitmask, executing single or multiple actions in one call.
@@ -392,6 +392,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
getqr [<chat-id>]\n\
getbadqr\n\
checkqr <qr-content>\n\
providerinfo <addr>\n\
event <event-id to test>\n\
fileinfo <file>\n\
emptyserver <flags> (1=MVBOX 2=INBOX)\n\
@@ -514,7 +515,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
chat::get_fresh_msg_cnt(context, chat.get_id()),
chat.get_id().get_fresh_msg_cnt(context),
);
let lot = chatlist.get_summary(context, i, Some(&chat));
let statestr = if chat.is_archived() {
@@ -598,14 +599,11 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
},
);
log_msglist(context, &msglist)?;
if let Some(draft) = chat::get_draft(context, sel_chat.get_id())? {
if let Some(draft) = sel_chat.get_id().get_draft(context)? {
log_msg(context, "Draft", &draft);
}
println!(
"{} messages.",
chat::get_msg_cnt(context, sel_chat.get_id())
);
println!("{} messages.", sel_chat.get_id().get_msg_cnt(context));
chat::marknoticed_chat(context, sel_chat.get_id())?;
}
"createchat" => {
@@ -801,14 +799,14 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
if !arg1.is_empty() {
let mut draft = Message::new(Viewtype::Text);
draft.set_text(Some(arg1.to_string()));
chat::set_draft(
context,
sel_chat.as_ref().unwrap().get_id(),
Some(&mut draft),
);
sel_chat
.as_ref()
.unwrap()
.get_id()
.set_draft(context, Some(&mut draft));
println!("Draft saved.");
} else {
chat::set_draft(context, sel_chat.as_ref().unwrap().get_id(), None);
sel_chat.as_ref().unwrap().get_id().set_draft(context, None);
println!("Draft deleted.");
}
}
@@ -847,12 +845,12 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
"archive" | "unarchive" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat::archive(context, chat_id, arg0 == "archive")?;
chat_id.set_archived(context, arg0 == "archive")?;
}
"delchat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat::delete(context, chat_id)?;
chat_id.delete(context)?;
}
"msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
@@ -969,6 +967,28 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
res.get_text2()
);
}
"setqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
set_config_from_qr(context, arg1);
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
match provider::get_provider_info(arg1) {
Some(info) => {
println!("Information for provider belonging to {}:", arg1);
println!("status: {}", info.status as u32);
println!("before_login_hint: {}", info.before_login_hint);
println!("after_login_hint: {}", info.after_login_hint);
println!("overview_page: {}", info.overview_page);
for server in info.server.iter() {
println!("server: {}:{}", server.hostname, server.port,);
}
}
None => {
println!("No information for provider belonging to {} found.", arg1);
}
}
}
// TODO: implement this again, unclear how to match this through though, without writing a parser.
// "event" => {
// ensure!(!arg1.is_empty(), "Argument <id> missing.");

View File

@@ -21,13 +21,13 @@ fn cb(_ctx: &Context, event: Event) {
match event {
Event::ConfigureProgress(progress) => {
print!(" progress: {}\n", progress);
println!(" progress: {}", progress);
}
Event::Info(msg) | Event::Warning(msg) | Event::Error(msg) | Event::ErrorNetwork(msg) => {
print!(" {}\n", msg);
println!(" {}", msg);
}
_ => {
print!("\n");
println!();
}
}
}

View File

@@ -52,20 +52,21 @@ Python packages before running the tests::
pytest -v tests
running "live" tests (experimental)
-----------------------------------
running "live" tests with temporary accounts
---------------------------------------------
If you want to run "liveconfig" functional tests you can set
``DCC_PY_LIVECONFIG`` to:
``DCC_NEW_TMP_EMAIL`` to:
- a particular https-url that you can ask for from the delta
chat devs.
chat devs. This is implemented on the server side via
the [mailadm](https://github.com/deltachat/mailadm) command line tool.
- or the path of a file that contains two lines, each describing
via "addr=... mail_pw=..." a test account login that will
be used for the live tests.
With ``DCC_PY_LIVECONFIG`` set pytest invocations will use real
With ``DCC_NEW_TMP_EMAIL`` set pytest invocations will use real
e-mail accounts and run through all functional "liveconfig" tests.
@@ -129,7 +130,7 @@ organization::
This docker image can be used to run tests and build Python wheels for all interpreters::
$ docker run -e DCC_PY_LIVECONFIG \
$ docker run -e DCC_NEW_TMP_EMAIL \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh

View File

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

View File

@@ -118,6 +118,18 @@ class Account(object):
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
return from_dc_charpointer(res)
def _preconfigure_keypair(self, addr, public, secret):
"""See dc_preconfigure_keypair() in deltachat.h.
In other words, you don't need this.
"""
res = lib.dc_preconfigure_keypair(self._dc_context,
as_dc_charpointer(addr),
as_dc_charpointer(public),
as_dc_charpointer(secret))
if res == 0:
raise Exception("Failed to set key")
def configure(self, **kwargs):
""" set config values and configure this account.
@@ -578,6 +590,11 @@ class IOThreads:
def stop(self, wait=False):
self._thread_quitflag = True
# Workaround for a race condition. Make sure that thread is
# not in between checking for quitflag and entering idle.
time.sleep(0.5)
lib.dc_interrupt_imap_idle(self._dc_context)
lib.dc_interrupt_smtp_idle(self._dc_context)
lib.dc_interrupt_mvbox_idle(self._dc_context)
@@ -600,23 +617,28 @@ class IOThreads:
self._log_event("py-bindings-info", 0, "MVBOX THREAD START")
while not self._thread_quitflag:
lib.dc_perform_mvbox_jobs(self._dc_context)
lib.dc_perform_mvbox_fetch(self._dc_context)
lib.dc_perform_mvbox_idle(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_mvbox_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_mvbox_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "MVBOX THREAD FINISHED")
def sentbox_thread_run(self):
self._log_event("py-bindings-info", 0, "SENTBOX THREAD START")
while not self._thread_quitflag:
lib.dc_perform_sentbox_jobs(self._dc_context)
lib.dc_perform_sentbox_fetch(self._dc_context)
lib.dc_perform_sentbox_idle(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_sentbox_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_sentbox_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "SENTBOX THREAD FINISHED")
def smtp_thread_run(self):
self._log_event("py-bindings-info", 0, "SMTP THREAD START")
while not self._thread_quitflag:
lib.dc_perform_smtp_jobs(self._dc_context)
lib.dc_perform_smtp_idle(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_smtp_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "SMTP THREAD FINISHED")

View File

@@ -58,6 +58,13 @@ class Chat(object):
"""
return self.id == const.DC_CHAT_ID_DEADDROP
def is_muted(self):
""" return true if this chat is muted.
:returns: True if chat is muted, False otherwise.
"""
return lib.dc_chat_is_muted(self._dc_chat)
def is_promoted(self):
""" return True if this chat is promoted, i.e.
the member contacts are aware of their membership,
@@ -84,12 +91,43 @@ class Chat(object):
def set_name(self, name):
""" set name of this chat.
:param: name as a unicode string.
:param name: as a unicode string.
:returns: None
"""
name = as_dc_charpointer(name)
return lib.dc_set_chat_name(self._dc_context, self.id, name)
def mute(self, duration=None):
""" mutes the chat
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
:returns: None
"""
if duration is None:
mute_duration = -1
else:
mute_duration = duration
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, mute_duration)
if not bool(ret):
raise ValueError("Call to dc_set_chat_mute_duration failed")
def unmute(self):
""" unmutes the chat
:returns: None
"""
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, 0)
if not bool(ret):
raise ValueError("Failed to unmute chat")
def get_mute_duration(self):
""" Returns the number of seconds until the mute of this chat is lifted.
:param duration:
:returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted)
"""
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
def get_type(self):
""" return type of this chat.

View File

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

View File

@@ -1,5 +1,6 @@
from __future__ import print_function
import os
import py
import pytest
import requests
import time
@@ -15,16 +16,29 @@ def pytest_addoption(parser):
help="a file with >=2 lines where each line "
"contains NAME=VALUE config settings for one account"
)
parser.addoption(
"--ignored", action="store_true",
help="Also run tests marked with the ignored marker",
)
def pytest_configure(config):
config.addinivalue_line(
"markers", "ignored: Mark test as bing slow, skipped unless --ignored is used."
)
cfg = config.getoption('--liveconfig')
if not cfg:
cfg = os.getenv('DCC_PY_LIVECONFIG')
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
if cfg:
config.option.liveconfig = cfg
def pytest_runtest_setup(item):
if (list(item.iter_markers(name="ignored"))
and not item.config.getoption("ignored")):
pytest.skip("Ignored tests not requested, use --ignored")
def pytest_report_header(config, startdir):
summary = []
@@ -83,17 +97,16 @@ class SessionLiveConfigFromFile:
class SessionLiveConfigFromURL:
def __init__(self, url, create_token):
def __init__(self, url):
self.configlist = []
self.url = url
self.create_token = create_token
def get(self, index):
try:
return self.configlist[index]
except IndexError:
assert index == len(self.configlist), index
res = requests.post(self.url, json={"token_create_user": int(self.create_token)})
res = requests.post(self.url)
if res.status_code != 200:
pytest.skip("creating newtmpuser failed {!r}".format(res))
d = res.json()
@@ -110,14 +123,24 @@ def session_liveconfig(request):
liveconfig_opt = request.config.option.liveconfig
if liveconfig_opt:
if liveconfig_opt.startswith("http"):
url, create_token = liveconfig_opt.split("#", 1)
return SessionLiveConfigFromURL(url, create_token)
return SessionLiveConfigFromURL(liveconfig_opt)
else:
return SessionLiveConfigFromFile(liveconfig_opt)
@pytest.fixture(scope='session')
def datadir():
"""The py.path.local object of the test-data/ directory."""
for path in reversed(py.path.local(__file__).parts()):
datadir = path.join('test-data')
if datadir.isdir():
return datadir
else:
pytest.skip('test-data directory not found')
@pytest.fixture
def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
class AccountMaker:
def __init__(self):
@@ -125,6 +148,8 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
self.offline_count = 0
self._finalizers = []
self.init_time = time.time()
self._generated_keys = ["alice", "bob", "charlie",
"dom", "elena", "fiona"]
def finalize(self):
while self._finalizers:
@@ -144,26 +169,32 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
ac._evlogger.set_timeout(2)
return ac
def _preconfigure_key(self, account, addr):
# Only set a key if we haven't used it yet for another account.
if self._generated_keys:
keyname = self._generated_keys.pop(0)
fname_pub = "key/{name}-public.asc".format(name=keyname)
fname_sec = "key/{name}-secret.asc".format(name=keyname)
account._preconfigure_keypair(addr,
datadir.join(fname_pub).read(),
datadir.join(fname_sec).read())
def get_configured_offline_account(self):
ac = self.get_unconfigured_account()
# do a pseudo-configured account
addr = "addr{}@offline.org".format(self.offline_count)
ac.set_config("addr", addr)
self._preconfigure_key(ac, addr)
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
ac.set_config("mail_pw", "123")
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
lib.dc_set_config(ac._dc_context, b"configured", b"1")
return ac
def peek_online_config(self):
def get_online_config(self, pre_generated_key=True):
if not session_liveconfig:
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
return session_liveconfig.get(self.live_count)
def get_online_config(self):
if not session_liveconfig:
pytest.skip("specify DCC_PY_LIVECONFIG or --liveconfig")
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig")
configdict = session_liveconfig.get(self.live_count)
self.live_count += 1
if "e2ee_enabled" not in configdict:
@@ -175,18 +206,23 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
if pre_generated_key:
self._preconfigure_key(ac, configdict['addr'])
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(30)
return ac, dict(configdict)
def get_online_configuring_account(self, mvbox=False, sentbox=False):
ac, configdict = self.get_online_config()
def get_online_configuring_account(self, mvbox=False, sentbox=False,
pre_generated_key=True):
ac, configdict = self.get_online_config(
pre_generated_key=pre_generated_key)
ac.configure(**configdict)
ac.start_threads(mvbox=mvbox, sentbox=sentbox)
return ac
def get_one_online_account(self):
ac1 = self.get_online_configuring_account()
def get_one_online_account(self, pre_generated_key=True):
ac1 = self.get_online_configuring_account(
pre_generated_key=pre_generated_key)
wait_successful_IMAP_SMTP_connection(ac1)
wait_configuration_progress(ac1, 1000)
return ac1
@@ -200,10 +236,12 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig):
wait_configuration_progress(ac2, 1000)
return ac1, ac2
def clone_online_account(self, account):
def clone_online_account(self, account, pre_generated_key=True):
self.live_count += 1
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
if pre_generated_key:
self._preconfigure_key(ac, account.get_config("addr"))
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(30)
ac.configure(addr=account.get_config("addr"), mail_pw=account.get_config("mail_pw"))

View File

@@ -6,7 +6,8 @@ import time
from deltachat import const, Account
from deltachat.message import Message
from datetime import datetime, timedelta
from conftest import wait_configuration_progress, wait_successful_IMAP_SMTP_connection, wait_securejoin_inviter_progress
from conftest import (wait_configuration_progress,
wait_securejoin_inviter_progress)
class TestOfflineAccountBasic:
@@ -24,6 +25,12 @@ class TestOfflineAccountBasic:
ac1 = Account(p.strpath, os_name="solarpunk")
ac1.get_info()
def test_preconfigure_keypair(self, acfactory, datadir):
ac = acfactory.get_unconfigured_account()
ac._preconfigure_keypair("alice@example.com",
datadir.join("key/alice-public.asc").read(),
datadir.join("key/alice-secret.asc").read())
def test_getinfo(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
d = ac1.get_info()
@@ -212,6 +219,18 @@ class TestOfflineChat:
chat.remove_profile_image()
assert chat.get_profile_image() is None
def test_mute(self, ac1):
chat = ac1.create_group_chat(name="title1")
assert not chat.is_muted()
chat.mute()
assert chat.is_muted()
chat.unmute()
assert not chat.is_muted()
chat.mute(50)
assert chat.is_muted()
with pytest.raises(ValueError):
chat.mute(-51)
def test_delete_and_send_fails(self, ac1, chat1):
chat1.delete()
ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
@@ -409,6 +428,12 @@ class TestOfflineChat:
class TestOnlineAccount:
@pytest.mark.ignored
def test_configure_generate_key(self, acfactory):
# A slow test which will generate a new key.
ac = acfactory.get_one_online_account(pre_generated_key=False)
ac.check_is_configured()
def get_chat(self, ac1, ac2, both_created=False):
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
@@ -435,37 +460,53 @@ class TestOnlineAccount:
def test_one_account_send_bcc_setting(self, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2_config = acfactory.peek_online_config()
c2 = ac1.create_contact(email=ac2_config["addr"])
chat = ac1.create_chat_by_contact(c2)
assert chat.id > const.DC_CHAT_ID_LAST_SPECIAL
wait_successful_IMAP_SMTP_connection(ac1)
ac2 = acfactory.get_online_configuring_account()
# Clone the first account: we will test if sent messages
# are copied to it via BCC.
ac1_clone = acfactory.clone_online_account(ac1)
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
wait_configuration_progress(ac1_clone, 1000)
chat = self.get_chat(ac1, ac2)
self_addr = ac1.get_config("addr")
other_addr = ac2.get_config("addr")
lp.sec("send out message without bcc to ourselves")
ac1.set_config("bcc_self", "0")
msg_out = chat.send_text("message1")
assert not msg_out.is_forwarded()
# wait for send out (no BCC)
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert ac1.get_config("bcc_self") == "0"
# make sure we are not sending message to ourselves
assert self_addr not in ev[2]
assert other_addr in ev[2]
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
lp.sec("ac1: setting bcc_self=1")
ac1.set_config("bcc_self", "1")
lp.sec("send out message with bcc to ourselves")
msg_out = chat.send_text("message2")
ev = ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev[2] == msg_out.id
# wait for send out (BCC)
assert ac1.get_config("bcc_self") == "1"
self_addr = ac1.get_config("addr")
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert ac1.get_config("bcc_self") == "1"
# now make sure we are sending message to ourselves too
assert self_addr in ev[2]
assert other_addr in ev[2]
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
ac1._evlogger.consume_events()
lp.sec("send out message without bcc")
ac1.set_config("bcc_self", "0")
msg_out = chat.send_text("message3")
assert not msg_out.is_forwarded()
ev = ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev[2] == msg_out.id
ev = ac1._evlogger.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
assert self_addr not in ev[2]
ev = ac1._evlogger.get_matching("DC_EVENT_DELETED_BLOB_FILE")
# Second client receives only second message, but not the first
ev = ac1_clone._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ac1_clone.get_message_by_id(ev[2]).text == msg_out.text
def test_send_file_twice_unicode_filename_mangling(self, tmpdir, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -662,7 +703,7 @@ class TestOnlineAccount:
assert not msg_in.is_forwarded()
assert msg_in.get_sender_contact().display_name == ac1.get_config("displayname")
lp.sec("check the message arrived in contact-requets/deaddrop")
lp.sec("check the message arrived in contact-requests/deaddrop")
chat2 = msg_in.chat
assert msg_in in chat2.get_messages()
assert chat2.is_deaddrop()

View File

@@ -102,19 +102,12 @@ def test_get_special_message_id_returns_empty_message(acfactory):
assert msg.id == 0
def test_provider_info():
provider = lib.dc_provider_new_from_email(cutil.as_dc_charpointer("ex@example.com"))
assert cutil.from_dc_charpointer(
lib.dc_provider_get_overview_page(provider)
) == "https://providers.delta.chat/example.com"
assert cutil.from_dc_charpointer(lib.dc_provider_get_name(provider)) == "Example"
assert cutil.from_dc_charpointer(lib.dc_provider_get_markdown(provider)) == "\n..."
assert cutil.from_dc_charpointer(lib.dc_provider_get_status_date(provider)) == "2018-09"
assert lib.dc_provider_get_status(provider) == const.DC_PROVIDER_STATUS_PREPARATION
def test_provider_info_none():
assert lib.dc_provider_new_from_email(cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
def test_get_info_closed():

View File

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

View File

@@ -7,7 +7,7 @@ envlist =
[testenv]
commands =
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx {posargs:tests}
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs:tests}
python tests/package_wheels.py {toxworkdir}/wheelhouse
passenv =
TRAVIS
@@ -65,11 +65,11 @@ commands =
[pytest]
addopts = -v -ra
addopts = -v -ra --strict-markers
python_files = tests/test_*.py
norecursedirs = .tox
xfail_strict=true
timeout = 60
timeout = 90
timeout_method = thread
[flake8]

View File

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

View File

@@ -322,13 +322,12 @@ impl<'a> BlobObject<'a> {
let clean = sanitize_filename::sanitize_with_options(name, opts);
let mut iter = clean.splitn(2, '.');
let mut stem = iter.next().unwrap_or_default().to_string();
let mut ext = iter.next().unwrap_or_default().to_string();
stem.truncate(64);
ext.truncate(32);
match ext.len() {
0 => (stem, "".to_string()),
_ => (stem, format!(".{}", ext).to_lowercase()),
let stem: String = iter.next().unwrap_or_default().chars().take(64).collect();
let ext: String = iter.next().unwrap_or_default().chars().take(32).collect();
if ext.is_empty() {
(stem, "".to_string())
} else {
(stem, format!(".{}", ext).to_lowercase())
}
}
@@ -631,4 +630,11 @@ mod tests {
assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
}
#[test]
fn test_sanitise_name() {
let (_, ext) =
BlobObject::sanitise_name("Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt");
assert_eq!(ext, ".txt");
}
}

View File

@@ -1,6 +1,8 @@
//! # Chat module
use std::convert::TryFrom;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use itertools::Itertools;
use num_traits::FromPrimitive;
@@ -45,7 +47,7 @@ impl ChatId {
/// An unset ChatId
///
/// Like [is_error], from which it is indistinguishable, this is
/// Like [ChatId::is_error], from which it is indistinguishable, this is
/// transitional and should not be used in new code.
pub fn is_unset(self) -> bool {
self.0 == 0
@@ -104,6 +106,256 @@ impl ChatId {
self.0 == DC_CHAT_ID_ALLDONE_HINT
}
pub fn set_selfavatar_timestamp(self, context: &Context, timestamp: i64) -> Result<(), Error> {
context.sql.execute(
"UPDATE contacts
SET selfavatar_sent=?
WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=?);",
params![timestamp, self],
)?;
Ok(())
}
pub fn set_blocked(self, context: &Context, new_blocked: Blocked) -> bool {
if self.is_special() {
warn!(context, "ignoring setting of Block-status for {}", self);
return false;
}
sql::execute(
context,
&context.sql,
"UPDATE chats SET blocked=? WHERE id=?;",
params![new_blocked, self],
)
.is_ok()
}
pub fn unblock(self, context: &Context) {
self.set_blocked(context, Blocked::Not);
}
/// Archives or unarchives a chat.
pub fn set_archived(self, context: &Context, new_archived: bool) -> Result<(), Error> {
ensure!(
!self.is_special(),
"bad chat_id, can not be special chat: {}",
self
);
if new_archived {
sql::execute(
context,
&context.sql,
"UPDATE msgs SET state=? WHERE chat_id=? AND state=?;",
params![MessageState::InNoticed, self, MessageState::InFresh],
)?;
}
sql::execute(
context,
&context.sql,
"UPDATE chats SET archived=? WHERE id=?;",
params![new_archived, self],
)?;
context.call_cb(Event::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
Ok(())
}
// note that unarchive() is not the same as set_archived(false) -
// eg. unarchive() does not send events as done for set_archived(false).
pub fn unarchive(self, context: &Context) -> Result<(), Error> {
sql::execute(
context,
&context.sql,
"UPDATE chats SET archived=0 WHERE id=?",
params![self],
)?;
Ok(())
}
/// Deletes a chat.
pub fn delete(self, context: &Context) -> Result<(), Error> {
ensure!(
!self.is_special(),
"bad chat_id, can not be a special chat: {}",
self
);
/* Up to 2017-11-02 deleting a group also implied leaving it, see above why we have changed this. */
let _chat = Chat::load_from_db(context, self)?;
sql::execute(
context,
&context.sql,
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);",
params![self],
)?;
sql::execute(
context,
&context.sql,
"DELETE FROM msgs WHERE chat_id=?;",
params![self],
)?;
sql::execute(
context,
&context.sql,
"DELETE FROM chats_contacts WHERE chat_id=?;",
params![self],
)?;
sql::execute(
context,
&context.sql,
"DELETE FROM chats WHERE id=?;",
params![self],
)?;
context.call_cb(Event::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
job_kill_action(context, Action::Housekeeping);
job_add(context, Action::Housekeeping, 0, Params::new(), 10);
Ok(())
}
/// Sets draft message.
///
/// Passing `None` as message just deletes the draft
pub fn set_draft(self, context: &Context, msg: Option<&mut Message>) {
if self.is_special() {
return;
}
let changed = match msg {
None => self.maybe_delete_draft(context),
Some(msg) => self.set_draft_raw(context, msg),
};
if changed {
context.call_cb(Event::MsgsChanged {
chat_id: self,
msg_id: MsgId::new(0),
});
}
}
// similar to as dc_set_draft() but does not emit an event
fn set_draft_raw(self, context: &Context, msg: &mut Message) -> bool {
let deleted = self.maybe_delete_draft(context);
let set = self.do_set_draft(context, msg).is_ok();
// Can't inline. Both functions above must be called, no shortcut!
deleted || set
}
fn get_draft_msg_id(self, context: &Context) -> Option<MsgId> {
context.sql.query_get_value::<_, MsgId>(
context,
"SELECT id FROM msgs WHERE chat_id=? AND state=?;",
params![self, MessageState::OutDraft],
)
}
pub fn get_draft(self, context: &Context) -> Result<Option<Message>, Error> {
if self.is_special() {
return Ok(None);
}
match self.get_draft_msg_id(context) {
Some(draft_msg_id) => Ok(Some(Message::load_from_db(context, draft_msg_id)?)),
None => Ok(None),
}
}
/// Delete draft message in specified chat, if there is one.
///
/// Returns `true`, if message was deleted, `false` otherwise.
fn maybe_delete_draft(self, context: &Context) -> bool {
match self.get_draft_msg_id(context) {
Some(msg_id) => {
Message::delete_from_db(context, msg_id);
true
}
None => false,
}
}
/// Set provided message as draft message for specified chat.
///
/// Return true on success, false on database error.
fn do_set_draft(self, context: &Context, msg: &mut Message) -> Result<(), Error> {
match msg.viewtype {
Viewtype::Unknown => bail!("Can not set draft of unknown type."),
Viewtype::Text => match msg.text.as_ref() {
Some(text) => {
if text.is_empty() {
bail!("No text in draft");
}
}
None => bail!("No text in draft"),
},
_ => {
let blob = msg
.param
.get_blob(Param::File, context, !msg.is_increation())?
.ok_or_else(|| format_err!("No file stored in params"))?;
msg.param.set(Param::File, blob.as_name());
}
}
sql::execute(
context,
&context.sql,
"INSERT INTO msgs (chat_id, from_id, timestamp, type, state, txt, param, hidden)
VALUES (?,?,?, ?,?,?,?,?);",
params![
self,
DC_CONTACT_ID_SELF,
time(),
msg.viewtype,
MessageState::OutDraft,
msg.text.as_ref().map(String::as_str).unwrap_or(""),
msg.param.to_string(),
1,
],
)?;
Ok(())
}
/// Returns number of messages in a chat.
pub fn get_msg_cnt(self, context: &Context) -> usize {
context
.sql
.query_get_value::<_, i32>(
context,
"SELECT COUNT(*) FROM msgs WHERE chat_id=?;",
params![self],
)
.unwrap_or_default() as usize
}
pub fn get_fresh_msg_cnt(self, context: &Context) -> usize {
context
.sql
.query_get_value::<_, i32>(
context,
"SELECT COUNT(*)
FROM msgs
WHERE state=10
AND hidden=0
AND chat_id=?;",
params![self],
)
.unwrap_or_default() as usize
}
/// Bad evil escape hatch.
///
/// Avoid using this, eventually types should be cleaned up enough
@@ -172,6 +424,7 @@ pub struct Chat {
blocked: Blocked,
pub param: Params,
is_sending_locations: bool,
pub mute_duration: MuteDuration,
}
impl Chat {
@@ -179,7 +432,7 @@ impl Chat {
pub fn load_from_db(context: &Context, chat_id: ChatId) -> Result<Self, Error> {
let res = context.sql.query_row(
"SELECT c.type, c.name, c.grpid, c.param, c.archived,
c.blocked, c.locations_send_until
c.blocked, c.locations_send_until, c.muted_until
FROM chats c
WHERE c.id=?;",
params![chat_id],
@@ -193,6 +446,7 @@ impl Chat {
archived: row.get(4)?,
blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(),
is_sending_locations: row.get(6)?,
mute_duration: row.get(7)?,
};
Ok(c)
},
@@ -333,23 +587,19 @@ impl Chat {
sql.query_row(&query, params, collect).ok()
}
fn parent_is_encrypted(&self, context: &Context) -> bool {
fn parent_is_encrypted(&self, context: &Context) -> Result<bool, Error> {
let sql = &context.sql;
let params = params![self.id];
let query = Self::parent_query("param");
let packed: Option<String> = sql.query_get_value(context, &query, params);
let packed: Option<String> = sql.query_get_value_result(&query, params)?;
if let Some(ref packed) = packed {
match packed.parse::<Params>() {
Ok(param) => param.exists(Param::GuaranteeE2ee),
Err(err) => {
error!(context, "invalid params stored: '{}', {:?}", packed, err);
false
}
}
let param = packed.parse::<Params>()?;
Ok(param.exists(Param::GuaranteeE2ee))
} else {
false
// No messages
Ok(false)
}
}
@@ -396,7 +646,7 @@ impl Chat {
/// This is somewhat experimental, even more so than the rest of
/// deltachat, and the data returned is still subject to change.
pub fn get_info(&self, context: &Context) -> Result<ChatInfo, Error> {
let draft = match get_draft(context, self.id)? {
let draft = match self.id.get_draft(context)? {
Some(message) => message.text.unwrap_or_else(String::new),
_ => String::new(),
};
@@ -412,6 +662,7 @@ impl Chat {
profile_image: self.get_profile_image(context).unwrap_or_else(PathBuf::new),
subtitle: self.get_subtitle(context),
draft,
is_muted: self.is_muted(),
})
}
@@ -438,6 +689,14 @@ impl Chat {
self.is_sending_locations
}
pub fn is_muted(&self) -> bool {
match self.mute_duration {
MuteDuration::NotMuted => false,
MuteDuration::Forever => true,
MuteDuration::Until(when) => when > SystemTime::now(),
}
}
fn prepare_msg_raw(
&mut self,
context: &Context,
@@ -551,7 +810,7 @@ impl Chat {
}
}
if can_encrypt && (all_mutual || self.parent_is_encrypted(context)) {
if can_encrypt && (all_mutual || self.parent_is_encrypted(context)?) {
msg.param.set_int(Param::GuaranteeE2ee, 1);
}
}
@@ -722,6 +981,11 @@ pub struct ChatInfo {
/// which contain non-text parts. Perhaps it should be a
/// simple `has_draft` bool instead.
pub draft: String,
/// Whether the chat is muted
///
/// The exact time its muted can be found out via the `chat.mute_duration` property
pub is_muted: bool,
// ToDo:
// - [ ] deaddrop,
// - [ ] summary,
@@ -759,7 +1023,7 @@ pub fn create_by_msg_id(context: &Context, msg_id: MsgId) -> Result<ChatId, Erro
"Message can not belong to a special chat"
);
if chat.blocked != Blocked::Not {
unblock(context, chat.id);
chat.id.unblock(context);
// Sending with 0s as data since multiple messages may have changed.
context.call_cb(Event::MsgsChanged {
@@ -782,7 +1046,7 @@ pub fn create_by_contact_id(context: &Context, contact_id: u32) -> Result<ChatId
Ok((chat_id, chat_blocked)) => {
if chat_blocked != Blocked::Not {
// unblock chat (typically move it from the deaddrop to view
unblock(context, chat_id);
chat_id.unblock(context);
}
chat_id
}
@@ -811,24 +1075,6 @@ pub fn create_by_contact_id(context: &Context, contact_id: u32) -> Result<ChatId
Ok(chat_id)
}
pub fn unblock(context: &Context, chat_id: ChatId) {
set_blocking(context, chat_id, Blocked::Not);
}
pub fn set_blocking(context: &Context, chat_id: ChatId, new_blocking: Blocked) -> bool {
if chat_id.is_special() {
warn!(context, "ignoring setting of Block-status for {}", chat_id);
return false;
}
sql::execute(
context,
&context.sql,
"UPDATE chats SET blocked=? WHERE id=?;",
params![new_blocking, chat_id],
)
.is_ok()
}
pub fn update_saved_messages_icon(context: &Context) -> Result<(), Error> {
// if there is no saved-messages chat, there is nothing to update. this is no error.
if let Ok((chat_id, _)) = lookup_by_contact_id(context, DC_CONTACT_ID_SELF) {
@@ -1053,7 +1299,7 @@ fn prepare_msg_common(
) -> Result<MsgId, Error> {
msg.id = MsgId::new_unset();
prepare_msg_blob(context, msg)?;
unarchive(context, chat_id)?;
chat_id.unarchive(context)?;
let mut chat = Chat::load_from_db(context, chat_id)?;
ensure!(chat.can_send(), "cannot send to {}", chat_id);
@@ -1086,18 +1332,6 @@ pub fn is_contact_in_chat(context: &Context, chat_id: ChatId, contact_id: u32) -
.unwrap_or_default()
}
// note that unarchive() is not the same as archive(false) -
// eg. unarchive() does not send events as done for archive(false).
pub fn unarchive(context: &Context, chat_id: ChatId) -> Result<(), Error> {
sql::execute(
context,
&context.sql,
"UPDATE chats SET archived=0 WHERE id=?",
params![chat_id],
)?;
Ok(())
}
/// Send a message defined by a dc_msg_t object to a chat.
///
/// Sends the event #DC_EVENT_MSGS_CHANGED on succcess.
@@ -1173,106 +1407,6 @@ pub fn send_text_msg(
send_msg(context, chat_id, &mut msg)
}
// passing `None` as message jsut deletes the draft
pub fn set_draft(context: &Context, chat_id: ChatId, msg: Option<&mut Message>) {
if chat_id.is_special() {
return;
}
let changed = match msg {
None => maybe_delete_draft(context, chat_id),
Some(msg) => set_draft_raw(context, chat_id, msg),
};
if changed {
context.call_cb(Event::MsgsChanged {
chat_id,
msg_id: MsgId::new(0),
});
}
}
/// Delete draft message in specified chat, if there is one.
///
/// Return {true}, if message was deleted, {false} otherwise.
fn maybe_delete_draft(context: &Context, chat_id: ChatId) -> bool {
match get_draft_msg_id(context, chat_id) {
Some(msg_id) => {
Message::delete_from_db(context, msg_id);
true
}
None => false,
}
}
/// Set provided message as draft message for specified chat.
///
/// Return true on success, false on database error.
fn do_set_draft(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<(), Error> {
match msg.viewtype {
Viewtype::Unknown => bail!("Can not set draft of unknown type."),
Viewtype::Text => match msg.text.as_ref() {
Some(text) => {
if text.is_empty() {
bail!("No text in draft");
}
}
None => bail!("No text in draft"),
},
_ => {
let blob = msg
.param
.get_blob(Param::File, context, !msg.is_increation())?
.ok_or_else(|| format_err!("No file stored in params"))?;
msg.param.set(Param::File, blob.as_name());
}
}
sql::execute(
context,
&context.sql,
"INSERT INTO msgs (chat_id, from_id, timestamp, type, state, txt, param, hidden)
VALUES (?,?,?, ?,?,?,?,?);",
params![
chat_id,
DC_CONTACT_ID_SELF,
time(),
msg.viewtype,
MessageState::OutDraft,
msg.text.as_ref().map(String::as_str).unwrap_or(""),
msg.param.to_string(),
1,
],
)?;
Ok(())
}
// similar to as dc_set_draft() but does not emit an event
fn set_draft_raw(context: &Context, chat_id: ChatId, msg: &mut Message) -> bool {
let deleted = maybe_delete_draft(context, chat_id);
let set = do_set_draft(context, chat_id, msg).is_ok();
// Can't inline. Both functions above must be called, no shortcut!
deleted || set
}
fn get_draft_msg_id(context: &Context, chat_id: ChatId) -> Option<MsgId> {
context.sql.query_get_value::<_, MsgId>(
context,
"SELECT id FROM msgs WHERE chat_id=? AND state=?;",
params![chat_id, MessageState::OutDraft],
)
}
pub fn get_draft(context: &Context, chat_id: ChatId) -> Result<Option<Message>, Error> {
if chat_id.is_special() {
return Ok(None);
}
match get_draft_msg_id(context, chat_id) {
Some(draft_msg_id) => Ok(Some(Message::load_from_db(context, draft_msg_id)?)),
None => Ok(None),
}
}
pub fn get_chat_msgs(
context: &Context,
chat_id: ChatId,
@@ -1360,33 +1494,6 @@ pub fn get_chat_msgs(
}
}
/// Returns number of messages in a chat.
pub fn get_msg_cnt(context: &Context, chat_id: ChatId) -> usize {
context
.sql
.query_get_value::<_, i32>(
context,
"SELECT COUNT(*) FROM msgs WHERE chat_id=?;",
params![chat_id],
)
.unwrap_or_default() as usize
}
pub fn get_fresh_msg_cnt(context: &Context, chat_id: ChatId) -> usize {
context
.sql
.query_get_value::<_, i32>(
context,
"SELECT COUNT(*)
FROM msgs
WHERE state=10
AND hidden=0
AND chat_id=?;",
params![chat_id],
)
.unwrap_or_default() as usize
}
pub fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<(), Error> {
if !context.sql.exists(
"SELECT id FROM msgs WHERE chat_id=? AND state=?;",
@@ -1535,87 +1642,6 @@ pub fn get_next_media(
ret
}
/// Archives or unarchives a chat.
pub fn archive(context: &Context, chat_id: ChatId, archive: bool) -> Result<(), Error> {
ensure!(
!chat_id.is_special(),
"bad chat_id, can not be special chat: {}",
chat_id
);
if archive {
sql::execute(
context,
&context.sql,
"UPDATE msgs SET state=? WHERE chat_id=? AND state=?;",
params![MessageState::InNoticed, chat_id, MessageState::InFresh],
)?;
}
sql::execute(
context,
&context.sql,
"UPDATE chats SET archived=? WHERE id=?;",
params![archive, chat_id],
)?;
context.call_cb(Event::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
Ok(())
}
/// Deletes a chat.
pub fn delete(context: &Context, chat_id: ChatId) -> Result<(), Error> {
ensure!(
!chat_id.is_special(),
"bad chat_id, can not be a special chat: {}",
chat_id
);
/* Up to 2017-11-02 deleting a group also implied leaving it, see above why we have changed this. */
let _chat = Chat::load_from_db(context, chat_id)?;
sql::execute(
context,
&context.sql,
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);",
params![chat_id],
)?;
sql::execute(
context,
&context.sql,
"DELETE FROM msgs WHERE chat_id=?;",
params![chat_id],
)?;
sql::execute(
context,
&context.sql,
"DELETE FROM chats_contacts WHERE chat_id=?;",
params![chat_id],
)?;
sql::execute(
context,
&context.sql,
"DELETE FROM chats WHERE id=?;",
params![chat_id],
)?;
context.call_cb(Event::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
job_kill_action(context, Action::Housekeeping);
job_add(context, Action::Housekeeping, 0, Params::new(), 10);
Ok(())
}
pub fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Vec<u32> {
/* Normal chats do not include SELF. Group chats do (as it may happen that one is deleted from a
groupchat but the chats stays visible, moreover, this makes displaying lists easier) */
@@ -1675,7 +1701,7 @@ pub fn create_group_chat(
if add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF) {
let mut draft_msg = Message::new(Viewtype::Text);
draft_msg.set_text(Some(draft_txt));
set_draft_raw(context, chat_id, &mut draft_msg);
chat_id.set_draft_raw(context, &mut draft_msg);
}
context.call_cb(Event::MsgsChanged {
@@ -1893,17 +1919,63 @@ pub fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result<boo
Ok(needs_attach)
}
pub fn set_selfavatar_timestamp(
context: &Context,
chat_id: ChatId,
timestamp: i64,
) -> Result<(), Error> {
context.sql.execute(
"UPDATE contacts
SET selfavatar_sent=?
WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=?);",
params![timestamp, chat_id],
)?;
#[derive(Debug, Clone, PartialEq)]
pub enum MuteDuration {
NotMuted,
Forever,
Until(SystemTime),
}
impl rusqlite::types::ToSql for MuteDuration {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let duration: i64 = match &self {
MuteDuration::NotMuted => 0,
MuteDuration::Forever => -1,
MuteDuration::Until(when) => {
let duration = when
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
i64::try_from(duration.as_secs())
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?
}
};
let val = rusqlite::types::Value::Integer(duration);
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
impl rusqlite::types::FromSql for MuteDuration {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
// Negative values other than -1 should not be in the
// database. If found they'll be NotMuted.
match i64::column_result(value)? {
0 => Ok(MuteDuration::NotMuted),
-1 => Ok(MuteDuration::Forever),
n if n > 0 => match SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(n as u64)) {
Some(t) => Ok(MuteDuration::Until(t)),
None => Err(rusqlite::types::FromSqlError::OutOfRange(n)),
},
_ => Ok(MuteDuration::NotMuted),
}
}
}
pub fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuration) -> Result<(), Error> {
ensure!(!chat_id.is_special(), "Invalid chat ID");
if real_group_exists(context, chat_id)
&& sql::execute(
context,
&context.sql,
"UPDATE chats SET muted_until=? WHERE id=?;",
params![duration, chat_id],
)
.is_ok()
{
context.call_cb(Event::ChatModified(chat_id));
} else {
bail!("Failed to set name");
}
Ok(())
}
@@ -2152,7 +2224,7 @@ pub fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId) -> Re
let mut created_msgs: Vec<MsgId> = Vec::new();
let mut curr_timestamp: i64;
unarchive(context, chat_id)?;
chat_id.unarchive(context)?;
if let Ok(mut chat) = Chat::load_from_db(context, chat_id) {
ensure!(chat.can_send(), "cannot send to {}", chat_id);
curr_timestamp = dc_create_smeared_timestamps(context, msg_ids.len());
@@ -2295,7 +2367,7 @@ pub fn add_device_msg(
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
msg.try_calc_and_set_dimensions(context).ok();
prepare_msg_blob(context, msg)?;
unarchive(context, chat_id)?;
chat_id.unarchive(context)?;
context.sql.execute(
"INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,param,rfc724_mid) \
@@ -2404,7 +2476,7 @@ mod tests {
let chat = Chat::load_from_db(&t.ctx, chat_id).unwrap();
let info = chat.get_info(&t.ctx).unwrap();
// Ensure we can serialise this.
// Ensure we can serialize this.
println!("{}", serde_json::to_string_pretty(&info).unwrap());
let expected = r#"
@@ -2419,11 +2491,12 @@ mod tests {
"color": 15895624,
"profile_image": "",
"subtitle": "bob@example.com",
"draft": ""
"draft": "",
"is_muted": false
}
"#;
// Ensure we can deserialise this.
// Ensure we can deserialize this.
let loaded: ChatInfo = serde_json::from_str(expected).unwrap();
assert_eq!(info, loaded);
}
@@ -2432,14 +2505,16 @@ mod tests {
fn test_get_draft_no_draft() {
let t = dummy_context();
let chat_id = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF).unwrap();
let draft = get_draft(&t.ctx, chat_id).unwrap();
let draft = chat_id.get_draft(&t.ctx).unwrap();
assert!(draft.is_none());
}
#[test]
fn test_get_draft_special_chat_id() {
let t = dummy_context();
let draft = get_draft(&t.ctx, ChatId::new(DC_CHAT_ID_LAST_SPECIAL)).unwrap();
let draft = ChatId::new(DC_CHAT_ID_LAST_SPECIAL)
.get_draft(&t.ctx)
.unwrap();
assert!(draft.is_none());
}
@@ -2448,7 +2523,7 @@ mod tests {
// This is a weird case, maybe this should be an error but we
// do not get this info from the database currently.
let t = dummy_context();
let draft = get_draft(&t.ctx, ChatId::new(42)).unwrap();
let draft = ChatId::new(42).get_draft(&t.ctx).unwrap();
assert!(draft.is_none());
}
@@ -2458,8 +2533,8 @@ mod tests {
let chat_id = create_by_contact_id(&t.ctx, DC_CONTACT_ID_SELF).unwrap();
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hello".to_string()));
set_draft(&t.ctx, chat_id, Some(&mut msg));
let draft = get_draft(&t.ctx, chat_id).unwrap().unwrap();
chat_id.set_draft(&t.ctx, Some(&mut msg));
let draft = chat_id.get_draft(&t.ctx).unwrap().unwrap();
let msg_text = msg.get_text();
let draft_text = draft.get_text();
assert_eq!(msg_text, draft_text);
@@ -2535,7 +2610,7 @@ mod tests {
assert_eq!(msg2.text.as_ref().unwrap(), "second message");
// check device chat
assert_eq!(get_msg_cnt(&t.ctx, msg2.chat_id), 2);
assert_eq!(msg2.chat_id.get_msg_cnt(&t.ctx), 2);
}
#[test]
@@ -2568,7 +2643,7 @@ mod tests {
// check device chat
let chat_id = msg1.chat_id;
assert_eq!(get_msg_cnt(&t.ctx, chat_id), 1);
assert_eq!(chat_id.get_msg_cnt(&t.ctx), 1);
assert!(!chat_id.is_special());
let chat = Chat::load_from_db(&t.ctx, chat_id);
assert!(chat.is_ok());
@@ -2636,7 +2711,7 @@ mod tests {
assert_eq!(chats.len(), 1);
// after the device-chat and all messages are deleted, a re-adding should do nothing
delete(&t.ctx, chats.get_chat_id(0)).ok();
chats.get_chat_id(0).delete(&t.ctx).ok();
add_device_msg(&t.ctx, Some("some-label"), Some(&mut msg)).ok();
assert_eq!(chatlist_len(&t.ctx, 0), 0)
}
@@ -2703,7 +2778,7 @@ mod tests {
assert_eq!(DC_GCL_NO_SPECIALS, 0x02);
// archive first chat
assert!(archive(&t.ctx, chat_id1, true).is_ok());
assert!(chat_id1.set_archived(&t.ctx, true).is_ok());
assert!(Chat::load_from_db(&t.ctx, chat_id1).unwrap().is_archived());
assert!(!Chat::load_from_db(&t.ctx, chat_id2).unwrap().is_archived());
assert_eq!(get_chat_cnt(&t.ctx), 2);
@@ -2712,7 +2787,7 @@ mod tests {
assert_eq!(chatlist_len(&t.ctx, DC_GCL_ARCHIVED_ONLY), 1);
// archive second chat
assert!(archive(&t.ctx, chat_id2, true).is_ok());
assert!(chat_id2.set_archived(&t.ctx, true).is_ok());
assert!(Chat::load_from_db(&t.ctx, chat_id1).unwrap().is_archived());
assert!(Chat::load_from_db(&t.ctx, chat_id2).unwrap().is_archived());
assert_eq!(get_chat_cnt(&t.ctx), 2);
@@ -2721,9 +2796,9 @@ mod tests {
assert_eq!(chatlist_len(&t.ctx, DC_GCL_ARCHIVED_ONLY), 2);
// archive already archived first chat, unarchive second chat two times
assert!(archive(&t.ctx, chat_id1, true).is_ok());
assert!(archive(&t.ctx, chat_id2, false).is_ok());
assert!(archive(&t.ctx, chat_id2, false).is_ok());
assert!(chat_id1.set_archived(&t.ctx, true).is_ok());
assert!(chat_id2.set_archived(&t.ctx, false).is_ok());
assert!(chat_id2.set_archived(&t.ctx, false).is_ok());
assert!(Chat::load_from_db(&t.ctx, chat_id1).unwrap().is_archived());
assert!(!Chat::load_from_db(&t.ctx, chat_id2).unwrap().is_archived());
assert_eq!(get_chat_cnt(&t.ctx), 2);
@@ -2778,7 +2853,52 @@ mod tests {
t.ctx.set_config(Config::Selfavatar, None).unwrap(); // setting to None also forces re-sending
assert!(shall_attach_selfavatar(&t.ctx, chat_id).unwrap());
assert!(set_selfavatar_timestamp(&t.ctx, chat_id, time()).is_ok());
assert!(chat_id.set_selfavatar_timestamp(&t.ctx, time()).is_ok());
assert!(!shall_attach_selfavatar(&t.ctx, chat_id).unwrap());
}
#[test]
fn test_set_mute_duration() {
let t = dummy_context();
let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap();
// Initial
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
false
);
// Forever
set_muted(&t.ctx, chat_id, MuteDuration::Forever).unwrap();
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
true
);
// unMute
set_muted(&t.ctx, chat_id, MuteDuration::NotMuted).unwrap();
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
false
);
// Timed in the future
set_muted(
&t.ctx,
chat_id,
MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)),
)
.unwrap();
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
true
);
// Time in the past
set_muted(
&t.ctx,
chat_id,
MuteDuration::Until(SystemTime::now() - Duration::from_secs(3600)),
)
.unwrap();
assert_eq!(
Chat::load_from_db(&t.ctx, chat_id).unwrap().is_muted(),
false
);
}
}

View File

@@ -358,7 +358,6 @@ fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
mod tests {
use super::*;
use crate::chat;
use crate::test_utils::*;
#[test]
@@ -378,7 +377,7 @@ mod tests {
// drafts are sorted to the top
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hello".to_string()));
set_draft(&t.ctx, chat_id2, Some(&mut msg));
chat_id2.set_draft(&t.ctx, Some(&mut msg));
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.get_chat_id(0), chat_id2);
@@ -389,7 +388,7 @@ mod tests {
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
assert_eq!(chats.len(), 0);
chat::archive(&t.ctx, chat_id1, true).ok();
chat_id1.set_archived(&t.ctx, true).ok();
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
assert_eq!(chats.len(), 1);
}

View File

@@ -8,6 +8,7 @@ use crate::constants::DC_VERSION_STR;
use crate::context::Context;
use crate::dc_tools::*;
use crate::job::*;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::stock::StockMessage;
use rusqlite::NO_PARAMS;
@@ -98,7 +99,7 @@ impl Context {
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
}
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", 24 * 1024 * 1024 / 4 * 3)),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(self, key),
};

View File

@@ -12,12 +12,13 @@ use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::e2ee;
use crate::job::{self, job_add, job_kill_action};
use crate::login_param::{CertificateChecks, LoginParam};
use crate::oauth2::*;
use crate::param::Params;
use crate::{chat, e2ee, provider};
use crate::message::Message;
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
@@ -200,10 +201,7 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
7 => {
progress!(context, 310);
if param_autoconfig.is_none() {
let url = format!(
"https://{}{}/autodiscover/autodiscover.xml",
"", param_domain
);
let url = format!("https://{}/autodiscover/autodiscover.xml", param_domain);
param_autoconfig = outlk_autodiscover(context, &url, &param).ok();
}
true
@@ -439,45 +437,77 @@ pub fn JobConfigureImap(context: &Context) -> job::Status {
LoginParam::from_database(context, "configured_raw_").save_to_database(context, "");
}
if let Some(provider) = provider::get_provider_info(&param.addr) {
if !provider.after_login_hint.is_empty() {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(provider.after_login_hint.to_string());
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg)).is_err() {
warn!(context, "cannot add after_login_hint as core-provider-info");
}
}
}
context.free_ongoing();
progress!(context, if success { 1000 } else { 0 });
job::Status::Finished(Ok(()))
}
#[allow(clippy::unnecessary_unwrap)]
fn get_offline_autoconfig(context: &Context, param: &LoginParam) -> Option<LoginParam> {
// XXX we don't have https://github.com/deltachat/provider-db APIs
// integrated yet but we'll already add nauta as a first use case, also
// showing what we need from provider-db in the future.
info!(
context,
"checking internal provider-info for offline autoconfig"
);
if param.addr.ends_with("@nauta.cu") {
let mut p = LoginParam::new();
if let Some(provider) = provider::get_provider_info(&param.addr) {
match provider.status {
provider::Status::OK | provider::Status::PREPARATION => {
let imap = provider.get_imap_server();
let smtp = provider.get_smtp_server();
// clippy complains about these is_some()/unwrap() settings,
// however, rewriting the code to "if let" would make things less obvious,
// esp. if we allow more combinations of servers (pop, jmap).
// therefore, #[allow(clippy::unnecessary_unwrap)] is added above.
if imap.is_some() && smtp.is_some() {
let imap = imap.unwrap();
let smtp = smtp.unwrap();
p.addr = param.addr.clone();
p.mail_server = "imap.nauta.cu".to_string();
p.mail_user = param.addr.clone();
p.mail_pw = param.mail_pw.clone();
p.mail_port = 143;
p.imap_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
let mut p = LoginParam::new();
p.addr = param.addr.clone();
p.send_server = "smtp.nauta.cu".to_string();
p.send_user = param.addr.clone();
p.send_pw = param.mail_pw.clone();
p.send_port = 25;
p.smtp_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags = DC_LP_AUTH_NORMAL as i32
| DC_LP_IMAP_SOCKET_STARTTLS as i32
| DC_LP_SMTP_SOCKET_STARTTLS as i32;
p.mail_server = imap.hostname.to_string();
p.mail_user = imap.apply_username_pattern(param.addr.clone());
p.mail_port = imap.port as i32;
p.imap_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags |= match imap.socket {
provider::Socket::STARTTLS => DC_LP_IMAP_SOCKET_STARTTLS,
provider::Socket::SSL => DC_LP_IMAP_SOCKET_SSL,
};
info!(context, "found offline autoconfig: {}", p);
Some(p)
} else {
info!(context, "no offline autoconfig found");
None
p.send_server = smtp.hostname.to_string();
p.send_user = smtp.apply_username_pattern(param.addr.clone());
p.send_port = smtp.port as i32;
p.smtp_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags |= match smtp.socket {
provider::Socket::STARTTLS => DC_LP_SMTP_SOCKET_STARTTLS as i32,
provider::Socket::SSL => DC_LP_SMTP_SOCKET_SSL as i32,
};
info!(context, "offline autoconfig found: {}", p);
return Some(p);
} else {
info!(context, "offline autoconfig found, but no servers defined");
return None;
}
}
provider::Status::BROKEN => {
info!(context, "offline autoconfig found, provider is broken");
return None;
}
}
}
info!(context, "no offline autoconfig found");
None
}
fn try_imap_connections(

View File

@@ -14,7 +14,7 @@ use crate::events::Event;
use crate::imap::*;
use crate::job::*;
use crate::job_thread::JobThread;
use crate::key::*;
use crate::key::Key;
use crate::login_param::LoginParam;
use crate::lot::Lot;
use crate::message::{self, Message, MessengerMessage, MsgId};
@@ -83,6 +83,8 @@ pub fn get_info() -> HashMap<&'static str, String> {
impl Context {
/// Creates new context.
pub fn new(cb: Box<ContextCallback>, os_name: String, dbfile: PathBuf) -> Result<Context> {
pretty_env_logger::try_init_timed().ok();
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");

View File

@@ -74,18 +74,12 @@ pub fn dc_receive_imf(
// helper method to handle early exit and memory cleanup
let cleanup = |context: &Context,
create_event_to_send: &Option<CreateEvent>,
created_db_entries: &Vec<(ChatId, MsgId)>| {
created_db_entries: Vec<(ChatId, MsgId)>| {
if let Some(create_event_to_send) = create_event_to_send {
for (chat_id, msg_id) in created_db_entries {
let event = match create_event_to_send {
CreateEvent::MsgsChanged => Event::MsgsChanged {
msg_id: *msg_id,
chat_id: *chat_id,
},
CreateEvent::IncomingMsg => Event::IncomingMsg {
msg_id: *msg_id,
chat_id: *chat_id,
},
CreateEvent::MsgsChanged => Event::MsgsChanged { msg_id, chat_id },
CreateEvent::IncomingMsg => Event::IncomingMsg { msg_id, chat_id },
};
context.call_cb(event);
}
@@ -100,41 +94,15 @@ pub fn dc_receive_imf(
// get From: (it can be an address list!) and check if it is known (for known From:'s we add
// the other To:/Cc: in the 3rd pass)
// or if From: is equal to SELF (in this case, it is any outgoing messages,
// we do not check Return-Path any more as this is unreliable, see issue #150)
let mut from_id = 0;
let mut from_id_blocked = false;
let mut incoming = true;
let mut incoming_origin = Origin::Unknown;
if let Some(field_from) = mime_parser.get(HeaderDef::From_) {
let from_ids = dc_add_or_lookup_contacts_by_address_list(
context,
&field_from,
Origin::IncomingUnknownFrom,
)?;
if from_ids.contains(&DC_CONTACT_ID_SELF) {
incoming = false;
from_id = DC_CONTACT_ID_SELF;
incoming_origin = Origin::OutgoingBcc;
} else if !from_ids.is_empty() {
if from_ids.len() > 1 {
warn!(
context,
"mail has more than one From address, only using first: {:?}", field_from
);
}
from_id = from_ids.get_index(0).cloned().unwrap_or_default();
if let Ok(contact) = Contact::load_from_db(context, from_id) {
incoming_origin = contact.origin;
from_id_blocked = contact.blocked;
}
// we do not check Return-Path any more as this is unreliable, see
// https://github.com/deltachat/deltachat-core/issues/150)
let (from_id, from_id_blocked, incoming_origin) =
if let Some(field_from) = mime_parser.get(HeaderDef::From_) {
from_field_to_contact_id(context, field_from)?
} else {
warn!(context, "mail has an empty From header: {:?}", field_from);
// if there is no from given, from_id stays 0 which is just fine. These messages
// are very rare, however, we have to add them to the database (they go to the
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
}
}
(0, false, Origin::Unknown)
};
let incoming = from_id != DC_CONTACT_ID_SELF;
let mut to_ids = ContactIds::new();
for header_def in &[HeaderDef::To, HeaderDef::Cc] {
@@ -191,7 +159,7 @@ pub fn dc_receive_imf(
&mut created_db_entries,
&mut create_event_to_send,
) {
cleanup(context, &create_event_to_send, &created_db_entries);
cleanup(context, &create_event_to_send, created_db_entries);
bail!("add_parts error: {:?}", err);
}
} else {
@@ -242,13 +210,54 @@ pub fn dc_receive_imf(
"received message {} has Message-Id: {}", server_uid, rfc724_mid
);
cleanup(context, &create_event_to_send, &created_db_entries);
cleanup(context, &create_event_to_send, created_db_entries);
mime_parser.handle_reports(from_id, sent_timestamp, &server_folder, server_uid);
mime_parser.handle_reports(context, from_id, sent_timestamp, &server_folder, server_uid);
Ok(())
}
/// Converts "From" field to contact id.
///
/// Also returns whether it is blocked or not and its origin.
pub fn from_field_to_contact_id(
context: &Context,
field_from: &str,
) -> Result<(u32, bool, Origin)> {
let from_ids = dc_add_or_lookup_contacts_by_address_list(
context,
&field_from,
Origin::IncomingUnknownFrom,
)?;
if from_ids.contains(&DC_CONTACT_ID_SELF) {
Ok((DC_CONTACT_ID_SELF, false, Origin::OutgoingBcc))
} else if !from_ids.is_empty() {
if from_ids.len() > 1 {
warn!(
context,
"mail has more than one From address, only using first: {:?}", field_from
);
}
let from_id = from_ids.get_index(0).cloned().unwrap_or_default();
let mut from_id_blocked = false;
let mut incoming_origin = Origin::Unknown;
if let Ok(contact) = Contact::load_from_db(context, from_id) {
from_id_blocked = contact.blocked;
incoming_origin = contact.origin;
}
Ok((from_id, from_id_blocked, incoming_origin))
} else {
warn!(context, "mail has an empty From header: {:?}", field_from);
// if there is no from given, from_id stays 0 which is just fine. These messages
// are very rare, however, we have to add them to the database (they go to the
// "deaddrop" chat) to avoid a re-download from the server. See also [**]
Ok((0, false, Origin::Unknown))
}
}
#[allow(clippy::too_many_arguments, clippy::cognitive_complexity)]
fn add_parts(
context: &Context,
@@ -299,8 +308,7 @@ fn add_parts(
} else {
MessengerMessage::No
};
// incoming non-chat messages may be discarded;
// maybe this can be optimized later, by checking the state before the message body is downloaded
// incoming non-chat messages may be discarded
let mut allow_creation = true;
let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails)).unwrap_or_default();
@@ -308,11 +316,13 @@ fn add_parts(
&& msgrmsg == MessengerMessage::No
{
// this message is a classic email not a chat-message nor a reply to one
if show_emails == ShowEmails::Off {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
allow_creation = false
} else if show_emails == ShowEmails::AcceptedContacts {
allow_creation = false
match show_emails {
ShowEmails::Off => {
*chat_id = ChatId::new(DC_CHAT_ID_TRASH);
allow_creation = false;
}
ShowEmails::AcceptedContacts => allow_creation = false,
ShowEmails::All => {}
}
}
@@ -390,7 +400,7 @@ fn add_parts(
&& chat_id_blocked != Blocked::Not
&& create_blocked == Blocked::Not
{
chat::unblock(context, new_chat_id);
new_chat_id.unblock(context);
chat_id_blocked = Blocked::Not;
}
}
@@ -423,7 +433,7 @@ fn add_parts(
}
if !chat_id.is_unset() && Blocked::Not != chat_id_blocked {
if Blocked::Not == create_blocked {
chat::unblock(context, *chat_id);
chat_id.unblock(context);
chat_id_blocked = Blocked::Not;
} else if is_reply_to_known_message(context, mime_parser) {
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
@@ -476,7 +486,7 @@ fn add_parts(
chat_id_blocked = new_chat_id_blocked;
// automatically unblock chat when the user sends a message
if !chat_id.is_unset() && chat_id_blocked != Blocked::Not {
chat::unblock(context, new_chat_id);
new_chat_id.unblock(context);
chat_id_blocked = Blocked::Not;
}
}
@@ -497,7 +507,7 @@ fn add_parts(
&& Blocked::Not != chat_id_blocked
&& Blocked::Not == create_blocked
{
chat::unblock(context, *chat_id);
chat_id.unblock(context);
chat_id_blocked = Blocked::Not;
}
}
@@ -508,7 +518,7 @@ fn add_parts(
if chat_id.is_unset() && self_sent {
// from_id==to_id==DC_CONTACT_ID_SELF - this is a self-sent messages,
// maybe an Autocrypt Setup Messag
// maybe an Autocrypt Setup Message
let (id, bl) =
chat::create_or_lookup_by_contact_id(context, DC_CONTACT_ID_SELF, Blocked::Not)
.unwrap_or_default();
@@ -516,7 +526,7 @@ fn add_parts(
chat_id_blocked = bl;
if !chat_id.is_unset() && Blocked::Not != chat_id_blocked {
chat::unblock(context, *chat_id);
chat_id.unblock(context);
chat_id_blocked = Blocked::Not;
}
}
@@ -538,7 +548,7 @@ fn add_parts(
);
// unarchive chat
chat::unarchive(context, *chat_id)?;
chat_id.unarchive(context)?;
// if the mime-headers should be saved, find out its size
// (the mime-header ends with an empty line)
@@ -1054,7 +1064,7 @@ fn create_or_lookup_group(
}
/// try extract a grpid from a message-id list header value
fn extract_grpid<'a>(mime_parser: &'a MimeMessage, headerdef: HeaderDef) -> Option<&'a str> {
fn extract_grpid(mime_parser: &MimeMessage, headerdef: HeaderDef) -> Option<&str> {
let header = mime_parser.get(headerdef)?;
let parts = header
.split(',')
@@ -1509,7 +1519,7 @@ fn is_reply_to_messenger_message(context: &Context, mime_parser: &MimeMessage) -
false
}
fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
pub(crate) fn is_msgrmsg_rfc724_mid_in_list(context: &Context, mid_list: &str) -> bool {
if let Ok(ids) = mailparse::addrparse(mid_list) {
for id in ids.iter() {
if is_msgrmsg_rfc724_mid(context, id) {

View File

@@ -222,43 +222,6 @@ pub(crate) fn dc_extract_grpid_from_rfc724_mid(mid: &str) -> Option<&str> {
None
}
// Function returns a sanitized basename that does not contain
// win/linux path separators and also not any non-ascii chars
fn get_safe_basename(filename: &str) -> String {
// return the (potentially mangled) basename of the input filename
// this might be a path that comes in from another operating system
let mut index: usize = 0;
if let Some(unix_index) = filename.rfind('/') {
index = unix_index + 1;
}
if let Some(win_index) = filename.rfind('\\') {
index = max(index, win_index + 1);
}
if index >= filename.len() {
"nobasename".to_string()
} else {
// we don't allow any non-ascii to be super-safe
filename[index..].replace(|c: char| !c.is_ascii() || c == ':', "-")
}
}
pub fn dc_derive_safe_stem_ext(filename: &str) -> (String, String) {
let basename = get_safe_basename(&filename);
let (mut stem, mut ext) = if let Some(index) = basename.rfind('.') {
(
basename[0..index].to_string(),
basename[index..].to_string(),
)
} else {
(basename, "".to_string())
};
// limit length of stem and ext
stem.truncate(32);
ext.truncate(32);
(stem, ext)
}
// the returned suffix is lower-case
pub fn dc_get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
Path::new(path_filename.as_ref())
@@ -505,7 +468,7 @@ pub(crate) fn time() -> i64 {
/// assert_eq!(&email.domain, "example.com");
/// assert_eq!(email.to_string(), "someone@example.com");
/// ```
#[derive(Debug, PartialEq, Clone)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct EmailAddress {
pub local: String,
pub domain: String,
@@ -707,29 +670,29 @@ mod tests {
#[test]
fn test_emailaddress_parse() {
assert_eq!(EmailAddress::new("").is_ok(), false);
assert_eq!("".parse::<EmailAddress>().is_ok(), false);
assert_eq!(
EmailAddress::new("user@domain.tld").unwrap(),
"user@domain.tld".parse::<EmailAddress>().unwrap(),
EmailAddress {
local: "user".into(),
domain: "domain.tld".into(),
}
);
assert_eq!(EmailAddress::new("uuu").is_ok(), false);
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
assert_eq!(EmailAddress::new("tt.dd@uu").is_ok(), false);
assert_eq!(EmailAddress::new("u@d").is_ok(), false);
assert_eq!(EmailAddress::new("u@d.").is_ok(), false);
assert_eq!(EmailAddress::new("u@d.t").is_ok(), false);
assert_eq!("uuu".parse::<EmailAddress>().is_ok(), false);
assert_eq!("dd.tt".parse::<EmailAddress>().is_ok(), false);
assert_eq!("tt.dd@uu".parse::<EmailAddress>().is_ok(), false);
assert_eq!("u@d".parse::<EmailAddress>().is_ok(), false);
assert_eq!("u@d.".parse::<EmailAddress>().is_ok(), false);
assert_eq!("u@d.t".parse::<EmailAddress>().is_ok(), false);
assert_eq!(
EmailAddress::new("u@d.tt").unwrap(),
"u@d.tt".parse::<EmailAddress>().unwrap(),
EmailAddress {
local: "u".into(),
domain: "d.tt".into(),
}
);
assert_eq!(EmailAddress::new("u@.tt").is_ok(), false);
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
assert_eq!("u@tt".parse::<EmailAddress>().is_ok(), false);
assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
}
use proptest::prelude::*;
@@ -765,19 +728,6 @@ mod tests {
}
}
#[test]
fn test_file_get_safe_basename() {
assert_eq!(get_safe_basename("12312/hello"), "hello");
assert_eq!(get_safe_basename("12312\\hello"), "hello");
assert_eq!(get_safe_basename("//12312\\hello"), "hello");
assert_eq!(get_safe_basename("//123:12\\hello"), "hello");
assert_eq!(get_safe_basename("//123:12/\\\\hello"), "hello");
assert_eq!(get_safe_basename("//123:12//hello"), "hello");
assert_eq!(get_safe_basename("//123:12//"), "nobasename");
assert_eq!(get_safe_basename("//123:12/"), "nobasename");
assert!(get_safe_basename("123\x012.hello").ends_with(".hello"));
}
#[test]
fn test_file_handling() {
let t = dummy_context();

View File

@@ -1,15 +1,18 @@
//! End-to-end encryption support.
use std::collections::HashSet;
use std::convert::TryFrom;
use mailparse::{MailHeaderMap, ParsedMail};
use mailparse::ParsedMail;
use num_traits::FromPrimitive;
use crate::aheader::*;
use crate::config::Config;
use crate::context::Context;
use crate::dc_tools::EmailAddress;
use crate::error::*;
use crate::key::*;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{self, Key, KeyPairUse, SignedPublicKey};
use crate::keyring::*;
use crate::peerstate::*;
use crate::pgp;
@@ -19,7 +22,7 @@ use crate::securejoin::handle_degrade_event;
pub struct EncryptHelper {
pub prefer_encrypt: EncryptPreference,
pub addr: String,
pub public_key: Key,
pub public_key: SignedPublicKey,
}
impl EncryptHelper {
@@ -102,8 +105,8 @@ impl EncryptHelper {
})?;
keyring.add_ref(key);
}
keyring.add_ref(&self.public_key);
let public_key = Key::from(self.public_key.clone());
keyring.add_ref(&public_key);
let sign_key = Key::from_self_private(context, self.addr.clone(), &context.sql)
.ok_or_else(|| format_err!("missing own private key"))?;
@@ -122,7 +125,7 @@ pub fn try_decrypt(
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
let from = mail
.headers
.get_first_value("From")?
.get_header_value(HeaderDef::From_)?
.and_then(|from_addr| mailparse::addrparse(&from_addr).ok())
.and_then(|from| from.extract_single_info())
.map(|from| from.addr)
@@ -191,15 +194,20 @@ pub fn try_decrypt(
/// storing a new one when one doesn't exist yet. Care is taken to
/// only generate one key per context even when multiple threads call
/// this function concurrently.
fn load_or_generate_self_public_key(context: &Context, self_addr: impl AsRef<str>) -> Result<Key> {
fn load_or_generate_self_public_key(
context: &Context,
self_addr: impl AsRef<str>,
) -> Result<SignedPublicKey> {
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
return Ok(key);
return SignedPublicKey::try_from(key)
.map_err(|_| Error::Message("Not a public key".into()));
}
let _guard = context.generating_key_mutex.lock().unwrap();
// Check again in case the key was generated while we were waiting for the lock.
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
return Ok(key);
return SignedPublicKey::try_from(key)
.map_err(|_| Error::Message("Not a public key".into()));
}
let start = std::time::Instant::now();
@@ -207,28 +215,14 @@ fn load_or_generate_self_public_key(context: &Context, self_addr: impl AsRef<str
context,
"Generating keypair with {} bits, e={} ...", 2048, 65537,
);
match pgp::create_keypair(&self_addr) {
Some((public_key, private_key)) => {
if dc_key_save_self_keypair(
context,
&public_key,
&private_key,
&self_addr,
true,
&context.sql,
) {
info!(
context,
"Keypair generated in {:.3}s.",
start.elapsed().as_secs()
);
Ok(public_key)
} else {
Err(format_err!("Failed to save keypair"))
}
}
None => Err(format_err!("Failed to generate keypair")),
}
let keypair = pgp::create_keypair(EmailAddress::new(self_addr.as_ref())?)?;
key::store_self_keypair(context, &keypair, KeyPairUse::Default)?;
info!(
context,
"Keypair generated in {:.3}s.",
start.elapsed().as_secs()
);
Ok(keypair.public)
}
/// Returns a reference to the encrypted payload and validates the autocrypt structure.

View File

@@ -16,6 +16,9 @@ pub enum Error {
#[fail(display = "{:?}", _0)]
Message(String),
#[fail(display = "{:?}", _0)]
MessageWithCause(String, #[cause] failure::Error, failure::Backtrace),
#[fail(display = "{:?}", _0)]
Image(image_meta::ImageError),
@@ -118,6 +121,18 @@ impl From<crate::message::InvalidMsgId> for Error {
}
}
impl From<crate::key::SaveKeyError> for Error {
fn from(err: crate::key::SaveKeyError) -> Error {
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
}
}
impl From<crate::pgp::PgpKeygenError> for Error {
fn from(err: crate::pgp::PgpKeygenError) -> Error {
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
}
}
impl From<mailparse::MailParseError> for Error {
fn from(err: mailparse::MailParseError) -> Error {
Error::MailParseError(err)

View File

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

View File

@@ -80,18 +80,21 @@ impl Imap {
} else {
info!(context, "Idle entering wait-on-remote state");
match idle_wait.await {
IdleResponse::NewData(_) => {
Ok(IdleResponse::NewData(_)) => {
info!(context, "Idle has NewData");
}
// TODO: idle_wait does not distinguish manual interrupts
// from Timeouts if we would know it's a Timeout we could bail
// directly and reconnect .
IdleResponse::Timeout => {
Ok(IdleResponse::Timeout) => {
info!(context, "Idle-wait timeout or interruption");
}
IdleResponse::ManualInterrupt => {
Ok(IdleResponse::ManualInterrupt) => {
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
}
}
}
// if we can't properly terminate the idle
@@ -134,18 +137,21 @@ impl Imap {
} else {
info!(context, "Idle entering wait-on-remote state");
match idle_wait.await {
IdleResponse::NewData(_) => {
Ok(IdleResponse::NewData(_)) => {
info!(context, "Idle has NewData");
}
// TODO: idle_wait does not distinguish manual interrupts
// from Timeouts if we would know it's a Timeout we could bail
// directly and reconnect .
IdleResponse::Timeout => {
Ok(IdleResponse::Timeout) => {
info!(context, "Idle-wait timeout or interruption");
}
IdleResponse::ManualInterrupt => {
Ok(IdleResponse::ManualInterrupt) => {
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
}
}
}
// if we can't properly terminate the idle

View File

@@ -5,6 +5,8 @@
use std::sync::atomic::{AtomicBool, Ordering};
use num_traits::FromPrimitive;
use async_imap::{
error::Result as ImapResult,
types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute},
@@ -12,10 +14,14 @@ use async_imap::{
use async_std::sync::{Mutex, RwLock};
use async_std::task;
use crate::config::*;
use crate::constants::*;
use crate::context::Context;
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_receive_imf::{
dc_receive_imf, from_field_to_contact_id, is_msgrmsg_rfc724_mid_in_list,
};
use crate::events::Event;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::imap_client::*;
use crate::job::{job_add, Action};
use crate::login_param::{CertificateChecks, LoginParam};
@@ -63,6 +69,9 @@ pub enum Error {
#[fail(display = "IMAP select folder error")]
SelectFolderError(#[cause] select_folder::Error),
#[fail(display = "Mail parse error")]
MailParseError(#[cause] mailparse::MailParseError),
#[fail(display = "No mailbox selected, folder: {:?}", _0)]
NoMailbox(String),
@@ -94,6 +103,12 @@ impl From<select_folder::Error> for Error {
}
}
impl From<mailparse::MailParseError> for Error {
fn from(err: mailparse::MailParseError) -> Error {
Error::MailParseError(err)
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq)]
pub enum ImapActionResult {
Failed,
@@ -102,7 +117,20 @@ pub enum ImapActionResult {
Success,
}
const PREFETCH_FLAGS: &str = "(UID ENVELOPE)";
/// Prefetch:
/// - Message-ID to check if we already have the message.
/// - In-Reply-To and References to check if message is a reply to chat message.
/// - Chat-Version to check if a message is a chat message
/// - Autocrypt-Setup-Message to check if a message is an autocrypt setup message,
/// not necessarily sent by Delta Chat.
const PREFETCH_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \
FROM \
IN-REPLY-TO REFERENCES \
CHAT-VERSION \
AUTOCRYPT-SETUP-MESSAGE\
)])";
const DELETE_CHECK_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])";
const JUST_UID: &str = "(UID)";
const BODY_FLAGS: &str = "(FLAGS BODY.PEEK[])";
const SELECT_ALL: &str = "1:*";
@@ -154,7 +182,6 @@ struct ImapConfig {
pub selected_mailbox: Option<Mailbox>,
pub selected_folder_needs_expunge: bool,
pub can_idle: bool,
pub has_xlist: bool,
pub imap_delimiter: char,
}
@@ -172,7 +199,6 @@ impl Default for ImapConfig {
selected_mailbox: None,
selected_folder_needs_expunge: false,
can_idle: false,
has_xlist: false,
imap_delimiter: '.',
}
}
@@ -326,7 +352,6 @@ impl Imap {
cfg.imap_port = 0;
cfg.can_idle = false;
cfg.has_xlist = false;
}
/// Connects to imap account using already-configured parameters.
@@ -387,7 +412,6 @@ impl Imap {
true
} else {
let can_idle = caps.has_str("IDLE");
let has_xlist = caps.has_str("XLIST");
let caps_list = caps.iter().fold(String::new(), |s, c| {
if let Capability::Atom(x) = c {
s + &format!(" {}", x)
@@ -397,7 +421,6 @@ impl Imap {
});
self.config.write().await.can_idle = can_idle;
self.config.write().await.has_xlist = has_xlist;
*self.connected.lock().await = true;
emit_event!(
context,
@@ -555,6 +578,9 @@ impl Imap {
context: &Context,
folder: S,
) -> Result<bool> {
let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails)).unwrap_or_default();
let (uid_validity, last_seen_uid) =
self.select_with_uidvalidity(context, folder.as_ref())?;
@@ -580,11 +606,16 @@ impl Imap {
list.sort_unstable_by_key(|msg| msg.uid.unwrap_or_default());
for msg in &list {
let cur_uid = msg.uid.unwrap_or_default();
for fetch in &list {
let cur_uid = fetch.uid.unwrap_or_default();
if cur_uid <= last_seen_uid {
// seems that at least dovecot sends the last available UID
// even if we asked for higher UID+N:*
// If the mailbox is not empty, results always include
// at least one UID, even if last_seen_uid+1 is past
// the last UID in the mailbox. It happens because
// uid+1:* is interpreted the same way as *:uid+1.
// See https://tools.ietf.org/html/rfc3501#page-61 for
// standard reference. Therefore, sometimes we receive
// already seen messages and have to filter them out.
info!(
context,
"fetch_new_messages: ignoring uid {}, last seen was {}", cur_uid, last_seen_uid
@@ -593,20 +624,9 @@ impl Imap {
}
read_cnt += 1;
let message_id = prefetch_get_message_id(msg).unwrap_or_default();
if !precheck_imf(context, &message_id, folder.as_ref(), cur_uid) {
// check passed, go fetch the rest
if self.fetch_single_msg(context, &folder, cur_uid).await == 0 {
info!(
context,
"Read error for message {} from \"{}\", trying over later.",
message_id,
folder.as_ref()
);
read_errors += 1;
}
} else {
let headers = get_fetch_headers(fetch)?;
let message_id = prefetch_get_message_id(&headers).unwrap_or_default();
if precheck_imf(context, &message_id, folder.as_ref(), cur_uid) {
// we know the message-id already or don't want the message otherwise.
info!(
context,
@@ -614,6 +634,34 @@ impl Imap {
message_id,
folder.as_ref(),
);
} else {
let show = prefetch_should_download(context, &headers, show_emails)
.map_err(|err| {
warn!(context, "prefetch_should_download error: {}", err);
err
})
.unwrap_or(true);
if !show {
info!(
context,
"Ignoring new message {} from \"{}\".",
message_id,
folder.as_ref(),
);
} else {
// check passed, go fetch the rest
if let Err(err) = self.fetch_single_msg(context, &folder, cur_uid).await {
info!(
context,
"Read error for message {} from \"{}\", trying over later: {}.",
message_id,
folder.as_ref(),
err
);
read_errors += 1;
}
}
}
if read_errors == 0 {
new_last_seen_uid = cur_uid;
@@ -657,17 +705,19 @@ impl Imap {
context.sql.set_raw_config(context, &key, Some(&val)).ok();
}
/// Fetches a single message by server UID.
///
/// If it succeeds, the message should be treated as received even
/// if no database entries are created. If the function returns an
/// error, the caller should try again later.
async fn fetch_single_msg<S: AsRef<str>>(
&self,
context: &Context,
folder: S,
server_uid: u32,
) -> usize {
// the function returns:
// 0 the caller should try over again later
// or 1 if the messages should be treated as received, the caller should not try to read the message again (even if no database entries are returned)
) -> Result<()> {
if !self.is_connected().await {
return 0;
return Err(Error::Other("Not connected".to_string()));
}
let set = format!("{}", server_uid);
@@ -686,13 +736,13 @@ impl Imap {
folder.as_ref(),
err
);
return 0;
return Err(Error::FetchFailed(err));
}
}
} else {
// we could not get a valid imap session, this should be retried
self.trigger_reconnect();
return 0;
return Err(Error::Other("Could not get IMAP session".to_string()));
};
if msgs.is_empty() {
@@ -733,7 +783,7 @@ impl Imap {
}
}
1
Ok(())
}
pub fn mv(
@@ -797,9 +847,23 @@ impl Imap {
Ok(_) => {
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid);
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} copied to {} (delete FAILED)",
display_folder_id, dest_folder
))
);
ImapActionResult::Failed
} else {
self.config.write().await.selected_folder_needs_expunge = true;
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} copied to {} (delete successfull)",
display_folder_id, dest_folder
))
);
ImapActionResult::Success
}
}
@@ -935,9 +999,11 @@ impl Imap {
// double-check that we are deleting the correct message-id
// this comes at the expense of another imap query
if let Some(ref mut session) = &mut *self.session.lock().await {
match session.uid_fetch(set, PREFETCH_FLAGS).await {
match session.uid_fetch(set, DELETE_CHECK_FLAGS).await {
Ok(msgs) => {
if msgs.is_empty() {
let fetch = if let Some(fetch) = msgs.first() {
fetch
} else {
warn!(
context,
"Cannot delete on IMAP, {}: imap entry gone '{}'",
@@ -945,9 +1011,11 @@ impl Imap {
message_id,
);
return ImapActionResult::Failed;
}
let remote_message_id =
prefetch_get_message_id(msgs.first().unwrap()).unwrap_or_default();
};
let remote_message_id = get_fetch_headers(fetch)
.and_then(|headers| prefetch_get_message_id(&headers))
.unwrap_or_default();
if remote_message_id != message_id {
warn!(
@@ -1099,7 +1167,6 @@ impl Imap {
}
async fn list_folders(&self, session: &mut Session, context: &Context) -> Option<Vec<Name>> {
// TODO: use xlist when available
match session.list(Some(""), Some("*")).await {
Ok(list) => {
if list.is_empty() {
@@ -1228,8 +1295,7 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
}
}
fn parse_message_id(message_id: &[u8]) -> crate::error::Result<String> {
let value = std::str::from_utf8(message_id)?;
fn parse_message_id(value: &str) -> crate::error::Result<String> {
let addrs = mailparse::addrparse(value)
.map_err(|err| format_err!("failed to parse message id {:?}", err))?;
@@ -1240,19 +1306,72 @@ fn parse_message_id(message_id: &[u8]) -> crate::error::Result<String> {
bail!("could not parse message_id: {}", value);
}
fn prefetch_get_message_id(prefetch_msg: &Fetch) -> Result<String> {
if prefetch_msg.envelope().is_none() {
return Err(Error::Other(
"prefectch: message has no envelope".to_string(),
));
fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>> {
let header_bytes = match prefetch_msg.header() {
Some(header_bytes) => header_bytes,
None => return Ok(Vec::new()),
};
let (headers, _) = mailparse::parse_headers(header_bytes)?;
Ok(headers)
}
fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String> {
if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId)? {
Ok(parse_message_id(&message_id)?)
} else {
Err(Error::Other("prefetch: No message ID found".to_string()))
}
}
fn prefetch_is_reply_to_chat_message(
context: &Context,
headers: &[mailparse::MailHeader],
) -> Result<bool> {
if let Some(value) = headers.get_header_value(HeaderDef::InReplyTo)? {
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
return Ok(true);
}
}
let message_id = prefetch_msg.envelope().unwrap().message_id;
if message_id.is_none() {
return Err(Error::Other("prefetch: No message ID found".to_string()));
if let Some(value) = headers.get_header_value(HeaderDef::References)? {
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
return Ok(true);
}
}
parse_message_id(&message_id.unwrap()).map_err(Into::into)
Ok(false)
}
fn prefetch_should_download(
context: &Context,
headers: &[mailparse::MailHeader],
show_emails: ShowEmails,
) -> Result<bool> {
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion)?.is_some();
let is_reply_to_chat_message = prefetch_is_reply_to_chat_message(context, &headers)?;
// Autocrypt Setup Message should be shown even if it is from non-chat client.
let is_autocrypt_setup_message = headers
.get_header_value(HeaderDef::AutocryptSetupMessage)?
.is_some();
let from_field = headers
.get_header_value(HeaderDef::From_)?
.unwrap_or_default();
let (_contact_id, blocked_contact, origin) = from_field_to_contact_id(context, &from_field)?;
let accepted_contact = origin.is_known();
let show = is_autocrypt_setup_message
|| match show_emails {
ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
ShowEmails::AcceptedContacts => {
is_chat_message || is_reply_to_chat_message || accepted_contact
}
ShowEmails::All => true,
};
let show = show && !blocked_contact;
Ok(show)
}
#[cfg(test)]
@@ -1262,11 +1381,11 @@ mod tests {
#[test]
fn test_parse_message_id() {
assert_eq!(
parse_message_id(b"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(),
parse_message_id("Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
assert_eq!(
parse_message_id(b"<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(),
parse_message_id("<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
}

View File

@@ -35,9 +35,8 @@ impl Client {
certificate_checks: CertificateChecks,
) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let tls = dc_build_tls(certificate_checks)?;
let tls_connector: async_native_tls::TlsConnector = tls.into();
let tls_stream = tls_connector.connect(domain.as_ref(), stream).await?;
let tls = dc_build_tls(certificate_checks);
let tls_stream = tls.connect(domain.as_ref(), stream).await?;
let mut client = ImapClient::new(tls_stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
client.debug = true;
@@ -73,10 +72,8 @@ impl Client {
) -> ImapResult<Client> {
match self {
Client::Insecure(client) => {
let tls = dc_build_tls(certificate_checks)?;
let tls_stream = tls.into();
let client_sec = client.secure(domain, &tls_stream).await?;
let tls = dc_build_tls(certificate_checks);
let client_sec = client.secure(domain, tls).await?;
Ok(Client::Secure(client_sec))
}

View File

@@ -18,7 +18,7 @@ use crate::e2ee;
use crate::error::*;
use crate::events::Event;
use crate::job::*;
use crate::key::*;
use crate::key::{self, Key};
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::*;
@@ -289,7 +289,6 @@ fn set_self_key(
.and_then(|(k, h)| k.split_key().map(|pub_key| (k, pub_key, h)));
ensure!(keys.is_some(), "Not a valid private key");
let (private_key, public_key, header) = keys.unwrap();
let preferencrypt = header.get("Autocrypt-Prefer-Encrypt");
match preferencrypt.map(|s| s.as_str()) {
@@ -314,34 +313,26 @@ fn set_self_key(
let self_addr = context.get_config(Config::ConfiguredAddr);
ensure!(self_addr.is_some(), "Missing self addr");
let addr = EmailAddress::new(&self_addr.unwrap_or_default())?;
// XXX maybe better make dc_key_save_self_keypair delete things
sql::execute(
let (public, secret) = match (public_key, private_key) {
(Key::Public(p), Key::Secret(s)) => (p, s),
_ => bail!("wrong keys unpacked"),
};
let keypair = pgp::KeyPair {
addr,
public,
secret,
};
key::store_self_keypair(
context,
&context.sql,
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
params![public_key.to_bytes(), private_key.to_bytes()],
&keypair,
if set_default {
key::KeyPairUse::Default
} else {
key::KeyPairUse::ReadOnly
},
)?;
if set_default {
sql::execute(
context,
&context.sql,
"UPDATE keypairs SET is_default=0;",
params![],
)?;
}
if !dc_key_save_self_keypair(
context,
&public_key,
&private_key,
self_addr.unwrap_or_default(),
set_default,
&context.sql,
) {
bail!("Cannot save keypair, internal key-state possibly corrupted now!");
}
Ok(())
}
@@ -746,7 +737,6 @@ fn export_key_to_asc_file(
#[cfg(test)]
mod tests {
use super::*;
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
use crate::test_utils::*;
use ::pgp::armor::BlockType;
@@ -801,8 +791,7 @@ mod tests {
#[test]
fn test_export_key_to_asc_file() {
let context = dummy_context();
let base64 = include_str!("../test-data/key/public.asc");
let key = Key::from_base64(base64, KeyType::Public).unwrap();
let key = Key::from(alice_keypair().public);
let blobdir = "$BLOBDIR";
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key).is_ok());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();

View File

@@ -369,7 +369,7 @@ impl Job {
}
self.smtp_send(context, recipients, body, self.job_id, || {
// Remove additional SendMdn jobs we have aggretated into this one.
// Remove additional SendMdn jobs we have aggregated into this one.
job_kill_ids(context, &additional_job_ids)?;
Ok(())
})
@@ -792,7 +792,32 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
};
let mimefactory = MimeFactory::from_msg(context, &msg, attach_selfavatar)?;
let mut rendered_msg = mimefactory.render().map_err(|err| {
let mut recipients = mimefactory.recipients();
let from = context
.get_config(Config::ConfiguredAddr)
.unwrap_or_default();
let lowercase_from = from.to_lowercase();
if context.get_config_bool(Config::BccSelf)
&& !recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
{
recipients.push(from);
}
if recipients.is_empty() {
// may happen eg. for groups with only SELF and bcc_self disabled
info!(
context,
"message {} has no recipient, skipping smtp-send", msg_id
);
set_delivered(context, msg_id);
return Ok(());
}
let rendered_msg = mimefactory.render().map_err(|err| {
message::set_msg_failed(context, msg_id, Some(err.to_string()));
err
})?;
@@ -811,26 +836,6 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
);
}
let lowercase_from = rendered_msg.from.to_lowercase();
if context.get_config_bool(Config::BccSelf)
&& !rendered_msg
.recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
{
rendered_msg.recipients.push(rendered_msg.from.clone());
}
if rendered_msg.recipients.is_empty() {
// may happen eg. for groups with only SELF and bcc_self disabled
info!(
context,
"message {} has no recipient, skipping smtp-send", msg_id
);
set_delivered(context, msg_id);
return Ok(());
}
if rendered_msg.is_gossiped {
chat::set_gossiped_timestamp(context, msg.chat_id, time())?;
}
@@ -849,7 +854,7 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
}
if attach_selfavatar {
if let Err(err) = chat::set_selfavatar_timestamp(context, msg.chat_id, time()) {
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()) {
error!(context, "Failed to set selfavatar timestamp: {:?}", err);
}
}
@@ -859,7 +864,13 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<()> {
msg.save_param_to_disk(context);
}
add_smtp_job(context, Action::SendMsgToSmtp, msg.id, &rendered_msg)?;
add_smtp_job(
context,
Action::SendMsgToSmtp,
msg.id,
recipients,
&rendered_msg,
)?;
Ok(())
}
@@ -889,8 +900,8 @@ fn job_perform(context: &Context, thread: Thread, probe_network: bool) {
// some configuration jobs are "exclusive":
// - they are always executed in the imap-thread and the smtp-thread is suspended during execution
// - they may change the database handle change the database handle; we do not keep old pointers therefore
// - they can be re-executed one time AT_ONCE, but they are not save in the database for later execution
// - they may change the database handle; we do not keep old pointers therefore
// - they can be re-executed one time AT_ONCE, but they are not saved in the database for later execution
if Action::ConfigureImap == job.action || Action::ImexImap == job.action {
job_kill_action(context, job.action);
context
@@ -1080,17 +1091,15 @@ fn add_smtp_job(
context: &Context,
action: Action,
msg_id: MsgId,
recipients: Vec<String>,
rendered_msg: &RenderedEmail,
) -> Result<()> {
ensure!(
!rendered_msg.recipients.is_empty(),
"no recipients for smtp job set"
);
ensure!(!recipients.is_empty(), "no recipients for smtp job set");
let mut param = Params::new();
let bytes = &rendered_msg.message;
let blob = BlobObject::create(context, &rendered_msg.rfc724_mid, bytes)?;
let recipients = rendered_msg.recipients.join("\x1e");
let recipients = recipients.join("\x1e");
param.set(Param::File, blob.as_name());
param.set(Param::Recipients, &recipients);
@@ -1153,7 +1162,7 @@ pub fn interrupt_smtp_idle(context: &Context) {
///
/// Load jobs for this "[Thread]", i.e. either load SMTP jobs or load
/// IMAP jobs. The `probe_network` parameter decides how to query
/// jobs, this is tricky and probably wrong currently. Look at the
/// jobs, this is tricky and probably wrong currently. Look at the
/// SQL queries for details.
fn load_next_job(context: &Context, thread: Thread, probe_network: bool) -> Option<Job> {
let query = if !probe_network {

View File

@@ -143,7 +143,7 @@ impl JobThread {
if state.jobs_needed {
info!(
context,
"{}-IDLE will not be started as it was interrupted while not ideling.",
"{}-IDLE will not be started as it was interrupted while not idling.",
self.name,
);
state.jobs_needed = false;

View File

@@ -4,14 +4,84 @@ use std::collections::BTreeMap;
use std::io::Cursor;
use std::path::Path;
use pgp::composed::{Deserializable, SignedPublicKey, SignedSecretKey};
use pgp::composed::Deserializable;
use pgp::ser::Serialize;
use pgp::types::{KeyTrait, SecretKeyTrait};
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::sql::{self, Sql};
use crate::sql::Sql;
// Re-export key types
pub use crate::pgp::KeyPair;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
/// Error type for deltachat key handling.
#[derive(Fail, Debug)]
pub enum Error {
#[fail(display = "Could not decode base64")]
Base64Decode(#[cause] base64::DecodeError, failure::Backtrace),
#[fail(display = "rPGP error: {}", _0)]
PgpError(#[cause] pgp::errors::Error, failure::Backtrace),
}
impl From<base64::DecodeError> for Error {
fn from(err: base64::DecodeError) -> Error {
Error::Base64Decode(err, failure::Backtrace::new())
}
}
impl From<pgp::errors::Error> for Error {
fn from(err: pgp::errors::Error) -> Error {
Error::PgpError(err, failure::Backtrace::new())
}
}
pub type Result<T> = std::result::Result<T, Error>;
/// Convenience trait for working with keys.
///
/// This trait is implemented for rPGP's [SignedPublicKey] and
/// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world.
pub trait DcKey: Serialize + Deserializable {
type KeyType: Serialize + Deserializable;
/// Create a key from some bytes.
fn from_slice(bytes: &[u8]) -> Result<Self::KeyType> {
Ok(<Self::KeyType as Deserializable>::from_bytes(Cursor::new(
bytes,
))?)
}
/// Create a key from a base64 string.
fn from_base64(data: &str) -> Result<Self::KeyType> {
// strip newlines and other whitespace
let cleaned: String = data.trim().split_whitespace().collect();
let bytes = base64::decode(cleaned.as_bytes())?;
Self::from_slice(&bytes)
}
/// Serialise the key to a base64 string.
fn to_base64(&self) -> String {
// Not using Serialize::to_bytes() to make clear *why* it is
// safe to ignore this error.
// Because we write to a Vec<u8> the io::Write impls never
// fail and we can hide this error.
let mut buf = Vec::new();
self.to_writer(&mut buf).unwrap();
base64::encode(&buf)
}
}
impl DcKey for SignedPublicKey {
type KeyType = SignedPublicKey;
}
impl DcKey for SignedSecretKey {
type KeyType = SignedSecretKey;
}
/// Cryptographic key
#[derive(Debug, PartialEq, Eq, Clone)]
@@ -35,7 +105,7 @@ impl From<SignedSecretKey> for Key {
impl std::convert::TryFrom<Key> for SignedSecretKey {
type Error = ();
fn try_from(value: Key) -> Result<Self, Self::Error> {
fn try_from(value: Key) -> std::result::Result<Self, Self::Error> {
match value {
Key::Public(_) => Err(()),
Key::Secret(key) => Ok(key),
@@ -46,7 +116,7 @@ impl std::convert::TryFrom<Key> for SignedSecretKey {
impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedSecretKey {
type Error = ();
fn try_from(value: &'a Key) -> Result<Self, Self::Error> {
fn try_from(value: &'a Key) -> std::result::Result<Self, Self::Error> {
match value {
Key::Public(_) => Err(()),
Key::Secret(key) => Ok(key),
@@ -57,7 +127,7 @@ impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedSecretKey {
impl std::convert::TryFrom<Key> for SignedPublicKey {
type Error = ();
fn try_from(value: Key) -> Result<Self, Self::Error> {
fn try_from(value: Key) -> std::result::Result<Self, Self::Error> {
match value {
Key::Public(key) => Ok(key),
Key::Secret(_) => Err(()),
@@ -68,7 +138,7 @@ impl std::convert::TryFrom<Key> for SignedPublicKey {
impl<'a> std::convert::TryFrom<&'a Key> for &'a SignedPublicKey {
type Error = ();
fn try_from(value: &'a Key) -> Result<Self, Self::Error> {
fn try_from(value: &'a Key) -> std::result::Result<Self, Self::Error> {
match value {
Key::Public(key) => Ok(key),
Key::Secret(_) => Err(()),
@@ -92,7 +162,7 @@ impl Key {
if bytes.is_empty() {
return None;
}
let res: Result<Key, _> = match key_type {
let res: std::result::Result<Key, _> = match key_type {
KeyType::Public => SignedPublicKey::from_bytes(Cursor::new(bytes)).map(Into::into),
KeyType::Private => SignedSecretKey::from_bytes(Cursor::new(bytes)).map(Into::into),
};
@@ -111,7 +181,7 @@ impl Key {
key_type: KeyType,
) -> Option<(Self, BTreeMap<String, String>)> {
let bytes = data.as_bytes();
let res: Result<(Key, _), _> = match key_type {
let res: std::result::Result<(Key, _), _> = match key_type {
KeyType::Public => SignedPublicKey::from_armor_single(Cursor::new(bytes))
.map(|(k, h)| (Into::into(k), h)),
KeyType::Private => SignedSecretKey::from_armor_single(Cursor::new(bytes))
@@ -127,15 +197,6 @@ impl Key {
}
}
pub fn from_base64(encoded_data: &str, key_type: KeyType) -> Option<Self> {
// strip newlines and other whitespace
let cleaned: String = encoded_data.trim().split_whitespace().collect();
let bytes = cleaned.as_bytes();
base64::decode(bytes)
.ok()
.and_then(|decoded| Self::from_slice(&decoded, key_type))
}
pub fn from_self_public(
context: &Context,
self_addr: impl AsRef<str>,
@@ -242,20 +303,97 @@ impl Key {
}
}
pub fn dc_key_save_self_keypair(
/// Use of a [KeyPair] for encryption or decryption.
///
/// This is used by [store_self_keypair] to know what kind of key is
/// being saved.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum KeyPairUse {
/// The default key used to encrypt new messages.
Default,
/// Only used to decrypt existing message.
ReadOnly,
}
/// Error saving a keypair to the database.
#[derive(Fail, Debug)]
#[fail(display = "SaveKeyError: {}", message)]
pub struct SaveKeyError {
message: String,
#[cause]
cause: failure::Error,
backtrace: failure::Backtrace,
}
impl SaveKeyError {
fn new(message: impl Into<String>, cause: impl Into<failure::Error>) -> Self {
Self {
message: message.into(),
cause: cause.into(),
backtrace: failure::Backtrace::new(),
}
}
}
/// Store the keypair as an owned keypair for addr in the database.
///
/// This will save the keypair as keys for the given address. The
/// "self" here refers to the fact that this DC instance owns the
/// keypair. Usually `addr` will be [Config::ConfiguredAddr].
///
/// If either the public or private keys are already present in the
/// database, this entry will be removed first regardless of the
/// address associated with it. Practically this means saving the
/// same key again overwrites it.
///
/// [Config::ConfiguredAddr]: crate::config::Config::ConfiguredAddr
pub fn store_self_keypair(
context: &Context,
public_key: &Key,
private_key: &Key,
addr: impl AsRef<str>,
is_default: bool,
sql: &Sql,
) -> bool {
sql::execute(
context,
sql,
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created) VALUES (?,?,?,?,?);",
params![addr.as_ref(), is_default as i32, public_key.to_bytes(), private_key.to_bytes(), time()],
).is_ok()
keypair: &KeyPair,
default: KeyPairUse,
) -> std::result::Result<(), SaveKeyError> {
// Everything should really be one transaction, more refactoring
// is needed for that.
let public_key = keypair
.public
.to_bytes()
.map_err(|err| SaveKeyError::new("failed to serialise public key", err))?;
let secret_key = keypair
.secret
.to_bytes()
.map_err(|err| SaveKeyError::new("failed to serialise secret key", err))?;
context
.sql
.execute(
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
params![public_key, secret_key],
)
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
if default == KeyPairUse::Default {
context
.sql
.execute("UPDATE keypairs SET is_default=0;", params![])
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
}
let is_default = match default {
KeyPairUse::Default => true,
KeyPairUse::ReadOnly => false,
};
context
.sql
.execute(
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
VALUES (?,?,?,?,?);",
params![
keypair.addr.to_string(),
is_default as i32,
public_key,
secret_key,
time()
],
)
.map(|_| ())
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))
}
/// Make a fingerprint human-readable, in hex format.
@@ -287,6 +425,14 @@ pub fn dc_normalize_fingerprint(fp: &str) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use std::convert::TryFrom;
use lazy_static::lazy_static;
lazy_static! {
static ref KEYPAIR: KeyPair = alice_keypair();
}
#[test]
fn test_normalize_fingerprint() {
@@ -373,9 +519,9 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
}
#[test]
#[ignore] // is too expensive
fn test_from_slice_roundtrip() {
let (public_key, private_key) = crate::pgp::create_keypair("hello").unwrap();
let public_key = Key::from(KEYPAIR.public.clone());
let private_key = Key::from(KEYPAIR.secret.clone());
let binary = public_key.to_bytes();
let public_key2 = Key::from_slice(&binary, KeyType::Public).expect("invalid public key");
@@ -408,9 +554,9 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
}
#[test]
#[ignore] // is too expensive
fn test_ascii_roundtrip() {
let (public_key, private_key) = crate::pgp::create_keypair("hello").unwrap();
let public_key = Key::from(KEYPAIR.public.clone());
let private_key = Key::from(KEYPAIR.secret.clone());
let s = public_key.to_armored_string(None).unwrap();
let (public_key2, _) =
@@ -423,4 +569,51 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
Key::from_armored_string(&s, KeyType::Private).expect("invalid private key");
assert_eq!(private_key, private_key2);
}
#[test]
fn test_split_key() {
let private_key = Key::from(KEYPAIR.secret.clone());
let public_wrapped = private_key.split_key().unwrap();
let public = SignedPublicKey::try_from(public_wrapped).unwrap();
assert_eq!(public.primary_key, KEYPAIR.public.primary_key);
}
#[test]
fn test_save_self_key_twice() {
// Saving the same key twice should result in only one row in
// the keypairs table.
let t = dummy_context();
let nrows = || {
t.ctx
.sql
.query_get_value::<_, u32>(&t.ctx, "SELECT COUNT(*) FROM keypairs;", params![])
.unwrap()
};
assert_eq!(nrows(), 0);
store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap();
assert_eq!(nrows(), 1);
store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap();
assert_eq!(nrows(), 1);
}
// Convenient way to create a new key if you need one, run with
// `cargo test key::tests::gen_key`.
// #[test]
// fn gen_key() {
// let name = "fiona";
// let keypair = crate::pgp::create_keypair(
// EmailAddress::new(&format!("{}@example.net", name)).unwrap(),
// )
// .unwrap();
// std::fs::write(
// format!("test-data/key/{}-public.asc", name),
// keypair.public.to_base64(),
// )
// .unwrap();
// std::fs::write(
// format!("test-data/key/{}-secret.asc", name),
// keypair.secret.to_base64(),
// )
// .unwrap();
// }
}

View File

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

View File

@@ -54,6 +54,7 @@ pub mod oauth2;
mod param;
pub mod peerstate;
pub mod pgp;
pub mod provider;
pub mod qr;
pub mod securejoin;
mod simplify;

View File

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

View File

@@ -86,6 +86,9 @@ pub enum LotState {
/// test1=formatted fingerprint
QrFprWithoutAddr = 230,
/// text1=domain
QrAccount = 250,
/// id=contact
QrAddr = 320,

View File

@@ -47,10 +47,7 @@ impl MsgId {
///
/// This kind of message ID can not be used for real messages.
pub fn is_special(self) -> bool {
match self.0 {
0..=DC_MSG_ID_LAST_SPECIAL => true,
_ => false,
}
self.0 <= DC_MSG_ID_LAST_SPECIAL
}
/// Whether the message ID is unset.

View File

@@ -17,6 +17,13 @@ use crate::param::*;
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::stock::StockMessage;
// attachments of 25 mb brutto should work on the majority of providers
// (brutto examples: web.de=50, 1&1=40, t-online.de=32, gmail=25, posteo=50, yahoo=25, all-inkl=100).
// as an upper limit, we double the size; the core won't send messages larger than this
// to get the netto sizes, we subtract 1 mb header-overhead and the base64-overhead.
pub const RECOMMENDED_FILE_SIZE: u64 = 24 * 1024 * 1024 / 4 * 3;
const UPPER_LIMIT_FILE_SIZE: u64 = 49 * 1024 * 1024 / 4 * 3;
#[derive(Debug, Clone)]
pub enum Loaded {
Message { chat: Chat },
@@ -53,9 +60,6 @@ pub struct RenderedEmail {
pub is_gossiped: bool,
pub last_added_location_id: u32,
pub from: String,
pub recipients: Vec<String>,
/// Message ID (Message in the sense of Email)
pub rfc724_mid: String,
}
@@ -342,6 +346,13 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
}
pub fn recipients(&self) -> Vec<String> {
self.recipients
.iter()
.map(|(_, addr)| addr.clone())
.collect()
}
pub fn render(mut self) -> Result<RenderedEmail, Error> {
// Headers that are encrypted
// - Chat-*, except Chat-Version
@@ -558,8 +569,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
};
let MimeFactory {
recipients,
from_addr,
last_added_location_id,
..
} = self;
@@ -570,8 +579,6 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
is_encrypted,
is_gossiped,
last_added_location_id,
recipients: recipients.into_iter().map(|(_, addr)| addr).collect(),
from: from_addr,
rfc724_mid,
})
}
@@ -595,6 +602,27 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
Some(part)
}
fn get_location_kml_part(&mut self) -> Result<PartBuilder, Error> {
let (kml_content, last_added_location_id) =
location::get_kml(self.context, self.msg.chat_id)?;
let part = PartBuilder::new()
.content_type(
&"application/vnd.google-earth.kml+xml"
.parse::<mime::Mime>()
.unwrap(),
)
.header((
"Content-Disposition",
"attachment; filename=\"location.kml\"",
))
.body(kml_content);
if !self.msg.param.exists(Param::SetLatitude) {
// otherwise, the independent location is already filed
self.last_added_location_id = last_added_location_id;
}
Ok(part)
}
#[allow(clippy::cognitive_complexity)]
fn render_message(
&mut self,
@@ -825,7 +853,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
if !is_file_size_okay(context, &self.msg) {
bail!(
"Message exceeds the recommended {} MB.",
24 * 1024 * 1024 / 4 * 3 / 1000 / 1000,
RECOMMENDED_FILE_SIZE / 1_000_000,
);
} else {
let (file_part, _) = build_body_file(context, &self.msg, "")?;
@@ -842,28 +870,10 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
if location::is_sending_locations_to_chat(context, self.msg.chat_id) {
match location::get_kml(context, self.msg.chat_id) {
Ok((kml_content, last_added_location_id)) => {
parts.push(
PartBuilder::new()
.content_type(
&"application/vnd.google-earth.kml+xml"
.parse::<mime::Mime>()
.unwrap(),
)
.header((
"Content-Disposition",
"attachment; filename=\"location.kml\"",
))
.body(kml_content),
);
if !self.msg.param.exists(Param::SetLatitude) {
// otherwise, the independent location is already filed
self.last_added_location_id = last_added_location_id;
}
}
match self.get_location_kml_part() {
Ok(part) => parts.push(part),
Err(err) => {
warn!(context, "mimefactory: could not get location: {}", err);
warn!(context, "mimefactory: could not send location: {}", err);
}
}
}
@@ -1101,7 +1111,7 @@ fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
match msg.param.get_path(Param::File, context).unwrap_or(None) {
Some(path) => {
let bytes = dc_get_filebytes(context, &path);
bytes <= (49 * 1024 * 1024 / 4 * 3)
bytes <= UPPER_LIMIT_FILE_SIZE
}
None => false,
}

View File

@@ -15,7 +15,7 @@ use crate::dehtml::dehtml;
use crate::e2ee;
use crate::error::Result;
use crate::events::Event;
use crate::headerdef::HeaderDef;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job::{job_add, Action};
use crate::location;
use crate::message;
@@ -36,8 +36,7 @@ use crate::{bail, ensure};
/// It is created by parsing the raw data of an actual MIME message
/// using the [MimeMessage::from_bytes] constructor.
#[derive(Debug)]
pub struct MimeMessage<'a> {
pub context: &'a Context,
pub struct MimeMessage {
pub parts: Vec<Part>,
header: HashMap<String, String>,
pub decrypting_failed: bool,
@@ -91,46 +90,29 @@ impl Default for SystemMessage {
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
impl<'a> MimeMessage<'a> {
pub fn from_bytes(context: &'a Context, body: &[u8]) -> Result<Self> {
impl MimeMessage {
pub fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
let mail = mailparse::parse_mail(body)?;
let mut parser = MimeMessage {
parts: Vec::new(),
header: Default::default(),
decrypting_failed: false,
// only non-empty if it was a valid autocrypt message
signatures: Default::default(),
gossipped_addr: Default::default(),
is_forwarded: false,
context,
reports: Vec::new(),
is_system_message: SystemMessage::Unknown,
location_kml: None,
message_kml: None,
user_avatar: AvatarAction::None,
group_avatar: AvatarAction::None,
};
let message_time = mail
.headers
.get_first_value("Date")?
.get_header_value(HeaderDef::Date)?
.and_then(|v| mailparse::dateparse(&v).ok())
.unwrap_or_default();
let mut headers = Default::default();
// init known headers with what mailparse provided us
parser.merge_headers(&mail.headers);
MimeMessage::merge_headers(&mut headers, &mail.headers);
// Memory location for a possible decrypted message.
let mail_raw;
let mut gossipped_addr = Default::default();
let mail = match e2ee::try_decrypt(parser.context, &mail, message_time) {
let (mail, signatures) = match e2ee::try_decrypt(context, &mail, message_time) {
Ok((raw, signatures)) => {
// Valid autocrypt message, encrypted
parser.signatures = signatures;
if let Some(raw) = raw {
// Valid autocrypt message, encrypted
mail_raw = raw;
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
@@ -142,17 +124,17 @@ impl<'a> MimeMessage<'a> {
// "3.6 Key Gossip" of https://autocrypt.org/autocrypt-spec-1.1.0.pdf
let gossip_headers =
decrypted_mail.headers.get_all_values("Autocrypt-Gossip")?;
parser.gossipped_addr =
gossipped_addr =
update_gossip_peerstates(context, message_time, &mail, gossip_headers)?;
// let known protected headers from the decrypted
// part override the unencrypted top-level
parser.merge_headers(&decrypted_mail.headers);
MimeMessage::merge_headers(&mut headers, &decrypted_mail.headers);
decrypted_mail
(decrypted_mail, signatures)
} else {
// Message was not encrypted
mail
(mail, signatures)
}
}
Err(err) => {
@@ -163,20 +145,35 @@ impl<'a> MimeMessage<'a> {
// if we just return here, the header is missing
// and the caller cannot display the message
// and try to assign the message to a chat
warn!(parser.context, "decryption failed: {}", err);
mail
warn!(context, "decryption failed: {}", err);
(mail, Default::default())
}
};
parser.parse_mime_recursive(&mail)?;
let mut parser = MimeMessage {
parts: Vec::new(),
header: headers,
decrypting_failed: false,
parser.parse_headers()?;
// only non-empty if it was a valid autocrypt message
signatures,
gossipped_addr,
is_forwarded: false,
reports: Vec::new(),
is_system_message: SystemMessage::Unknown,
location_kml: None,
message_kml: None,
user_avatar: AvatarAction::None,
group_avatar: AvatarAction::None,
};
parser.parse_mime_recursive(context, &mail)?;
parser.parse_headers(context)?;
Ok(parser)
}
#[allow(clippy::cognitive_complexity)]
fn parse_headers(&mut self) -> Result<()> {
/// Parses system messages.
fn parse_system_message_headers(&mut self, context: &Context) -> Result<()> {
if self.get(HeaderDef::AutocryptSetupMessage).is_some() {
self.parts = self
.parts
@@ -191,14 +188,18 @@ impl<'a> MimeMessage<'a> {
if self.parts.len() == 1 {
self.is_system_message = SystemMessage::AutocryptSetupMessage;
} else {
warn!(self.context, "could not determine ASM mime-part");
warn!(context, "could not determine ASM mime-part");
}
} else if let Some(value) = self.get(HeaderDef::ChatContent) {
if value == "location-streaming-enabled" {
self.is_system_message = SystemMessage::LocationStreamingEnabled;
}
}
Ok(())
}
/// Parses avatar action headers.
fn parse_avatar_headers(&mut self) {
if let Some(header_value) = self.get(HeaderDef::ChatGroupAvatar).cloned() {
self.group_avatar = self.avatar_action_from_header(header_value);
} else if let Some(header_value) = self.get(HeaderDef::ChatGroupImage).cloned() {
@@ -210,7 +211,13 @@ impl<'a> MimeMessage<'a> {
if let Some(header_value) = self.get(HeaderDef::ChatUserAvatar).cloned() {
self.user_avatar = self.avatar_action_from_header(header_value);
}
}
/// Squashes mutlipart chat messages with attachment into single-part messages.
///
/// Delta Chat sends attachments, such as images, in two-part messages, with the first message
/// containing an explanation. If such a message is detected, first part can be safely dropped.
fn squash_attachment_parts(&mut self) {
if self.has_chat_version() && self.parts.len() == 2 {
let need_drop = {
let textpart = &self.parts[0];
@@ -238,6 +245,48 @@ impl<'a> MimeMessage<'a> {
std::mem::replace(&mut self.parts[0], filepart);
}
}
}
/// Processes chat messages with attachments.
fn parse_attachments(&mut self) {
// Attachment messages should be squashed into a single part
// before calling this function.
if self.parts.len() == 1 {
if self.parts[0].typ == Viewtype::Audio
&& self.get(HeaderDef::ChatVoiceMessage).is_some()
{
let part_mut = &mut self.parts[0];
part_mut.typ = Viewtype::Voice;
}
if self.parts[0].typ == Viewtype::Image {
if let Some(value) = self.get(HeaderDef::ChatContent) {
if value == "sticker" {
let part_mut = &mut self.parts[0];
part_mut.typ = Viewtype::Sticker;
}
}
}
let part = &self.parts[0];
if part.typ == Viewtype::Audio
|| part.typ == Viewtype::Voice
|| part.typ == Viewtype::Video
{
if let Some(field_0) = self.get(HeaderDef::ChatDuration) {
let duration_ms = field_0.parse().unwrap_or_default();
if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
let part_mut = &mut self.parts[0];
part_mut.param.set_int(Param::Duration, duration_ms);
}
}
}
}
}
fn parse_headers(&mut self, context: &Context) -> Result<()> {
self.parse_system_message_headers(context)?;
self.parse_avatar_headers();
self.squash_attachment_parts();
if let Some(ref subject) = self.get_subject() {
let mut prepend_subject = true;
if !self.decrypting_failed {
@@ -273,42 +322,15 @@ impl<'a> MimeMessage<'a> {
part.param.set_int(Param::Forwarded, 1);
}
}
if self.parts.len() == 1 {
if self.parts[0].typ == Viewtype::Audio
&& self.get(HeaderDef::ChatVoiceMessage).is_some()
{
let part_mut = &mut self.parts[0];
part_mut.typ = Viewtype::Voice;
}
if self.parts[0].typ == Viewtype::Image {
if let Some(value) = self.get(HeaderDef::ChatContent) {
if value == "sticker" {
let part_mut = &mut self.parts[0];
part_mut.typ = Viewtype::Sticker;
}
}
}
let part = &self.parts[0];
if part.typ == Viewtype::Audio
|| part.typ == Viewtype::Voice
|| part.typ == Viewtype::Video
{
if let Some(field_0) = self.get(HeaderDef::ChatDuration) {
let duration_ms = field_0.parse().unwrap_or_default();
if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
let part_mut = &mut self.parts[0];
part_mut.param.set_int(Param::Duration, duration_ms);
}
}
}
}
self.parse_attachments();
// See if an MDN is requested from the other side
if !self.decrypting_failed && !self.parts.is_empty() {
if let Some(ref dn_to_addr) =
self.parse_first_addr(HeaderDef::ChatDispositionNotificationTo)
self.parse_first_addr(context, HeaderDef::ChatDispositionNotificationTo)
{
if let Some(ref from_addr) = self.parse_first_addr(HeaderDef::From_) {
if let Some(ref from_addr) = self.parse_first_addr(context, HeaderDef::From_) {
if compare_addrs(from_addr, dn_to_addr) {
if let Some(part) = self.parts.last_mut() {
part.param.set_int(Param::WantsMdn, 1);
@@ -380,34 +402,38 @@ impl<'a> MimeMessage<'a> {
}
pub fn get(&self, headerdef: HeaderDef) -> Option<&String> {
self.header.get(&headerdef.get_headername())
self.header.get(headerdef.get_headername())
}
fn parse_first_addr(&self, headerdef: HeaderDef) -> Option<MailAddr> {
fn parse_first_addr(&self, context: &Context, headerdef: HeaderDef) -> Option<MailAddr> {
if let Some(value) = self.get(headerdef.clone()) {
match mailparse::addrparse(&value) {
Ok(ref addrs) => {
return addrs.first().cloned();
}
Err(err) => {
warn!(self.context, "header {} parse error: {:?}", headerdef, err);
warn!(context, "header {} parse error: {:?}", headerdef, err);
}
}
}
None
}
fn parse_mime_recursive(&mut self, mail: &mailparse::ParsedMail<'_>) -> Result<bool> {
fn parse_mime_recursive(
&mut self,
context: &Context,
mail: &mailparse::ParsedMail<'_>,
) -> Result<bool> {
if mail.ctype.params.get("protected-headers").is_some() {
if mail.ctype.mimetype == "text/rfc822-headers" {
warn!(
self.context,
context,
"Protected headers found in text/rfc822-headers attachment: Will be ignored.",
);
return Ok(false);
}
warn!(self.context, "Ignoring nested protected headers");
warn!(context, "Ignoring nested protected headers");
}
enum MimeS {
@@ -435,7 +461,7 @@ impl<'a> MimeMessage<'a> {
};
match m {
MimeS::Multiple => self.handle_multiple(mail),
MimeS::Multiple => self.handle_multiple(context, mail),
MimeS::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
@@ -443,13 +469,17 @@ impl<'a> MimeMessage<'a> {
}
let mail = mailparse::parse_mail(&raw).unwrap();
self.parse_mime_recursive(&mail)
self.parse_mime_recursive(context, &mail)
}
MimeS::Single => self.add_single_part_if_known(mail),
MimeS::Single => self.add_single_part_if_known(context, mail),
}
}
fn handle_multiple(&mut self, mail: &mailparse::ParsedMail<'_>) -> Result<bool> {
fn handle_multiple(
&mut self,
context: &Context,
mail: &mailparse::ParsedMail<'_>,
) -> Result<bool> {
let mut any_part_added = false;
let mimetype = get_mime_type(mail)?.0;
match (mimetype.type_(), mimetype.subtype().as_str()) {
@@ -460,7 +490,7 @@ impl<'a> MimeMessage<'a> {
(mime::MULTIPART, "alternative") => {
for cur_data in &mail.subparts {
if get_mime_type(cur_data)?.0 == "multipart/mixed" {
any_part_added = self.parse_mime_recursive(cur_data)?;
any_part_added = self.parse_mime_recursive(context, cur_data)?;
break;
}
}
@@ -468,7 +498,7 @@ impl<'a> MimeMessage<'a> {
/* search for text/plain and add this */
for cur_data in &mail.subparts {
if get_mime_type(cur_data)?.0.type_() == mime::TEXT {
any_part_added = self.parse_mime_recursive(cur_data)?;
any_part_added = self.parse_mime_recursive(context, cur_data)?;
break;
}
}
@@ -476,7 +506,7 @@ impl<'a> MimeMessage<'a> {
if !any_part_added {
/* `text/plain` not found - use the first part */
for cur_part in &mail.subparts {
if self.parse_mime_recursive(cur_part)? {
if self.parse_mime_recursive(context, cur_part)? {
any_part_added = true;
break;
}
@@ -489,14 +519,14 @@ impl<'a> MimeMessage<'a> {
being the first one, which may not be always true ...
however, most times it seems okay. */
if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(first)?;
any_part_added = self.parse_mime_recursive(context, first)?;
}
}
(mime::MULTIPART, "encrypted") => {
// we currently do not try to decrypt non-autocrypt messages
// at all. If we see an encrypted part, we set
// decrypting_failed.
let msg_body = self.context.stock_str(StockMessage::CantDecryptMsgBody);
let msg_body = context.stock_str(StockMessage::CantDecryptMsgBody);
let txt = format!("[{}]", msg_body);
let mut part = Part::default();
@@ -519,7 +549,7 @@ impl<'a> MimeMessage<'a> {
https://k9mail.github.io/2016/11/24/OpenPGP-Considerations-Part-I.html
for background information why we use encrypted+signed) */
if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(first)?;
any_part_added = self.parse_mime_recursive(context, first)?;
}
}
(mime::MULTIPART, "report") => {
@@ -527,14 +557,14 @@ impl<'a> MimeMessage<'a> {
if mail.subparts.len() >= 2 {
if let Some(report_type) = mail.ctype.params.get("report-type") {
if report_type == "disposition-notification" {
if let Some(report) = self.process_report(mail)? {
if let Some(report) = self.process_report(context, mail)? {
self.reports.push(report);
}
} else {
/* eg. `report-type=delivery-status`;
maybe we should show them as a little error icon */
if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(first)?;
any_part_added = self.parse_mime_recursive(context, first)?;
}
}
}
@@ -544,7 +574,7 @@ impl<'a> MimeMessage<'a> {
// Add all parts (in fact, AddSinglePartIfKnown() later check if
// the parts are really supported)
for cur_data in mail.subparts.iter() {
if self.parse_mime_recursive(cur_data)? {
if self.parse_mime_recursive(context, cur_data)? {
any_part_added = true;
}
}
@@ -554,7 +584,11 @@ impl<'a> MimeMessage<'a> {
Ok(any_part_added)
}
fn add_single_part_if_known(&mut self, mail: &mailparse::ParsedMail<'_>) -> Result<bool> {
fn add_single_part_if_known(
&mut self,
context: &Context,
mail: &mailparse::ParsedMail<'_>,
) -> Result<bool> {
// return true if a part was added
let (mime_type, msg_type) = get_mime_type(mail)?;
let raw_mime = mail.ctype.mimetype.to_lowercase();
@@ -565,6 +599,7 @@ impl<'a> MimeMessage<'a> {
if let Ok(filename) = filename {
self.do_add_single_file_part(
context,
msg_type,
mime_type,
&raw_mime,
@@ -580,7 +615,7 @@ impl<'a> MimeMessage<'a> {
let decoded_data = match mail.get_body() {
Ok(decoded_data) => decoded_data,
Err(err) => {
warn!(self.context, "Invalid body parsed {:?}", err);
warn!(context, "Invalid body parsed {:?}", err);
// Note that it's not always an error - might be no data
return Ok(false);
}
@@ -621,6 +656,7 @@ impl<'a> MimeMessage<'a> {
fn do_add_single_file_part(
&mut self,
context: &Context,
msg_type: Viewtype,
mime_type: Mime,
raw_mime: &str,
@@ -635,9 +671,9 @@ impl<'a> MimeMessage<'a> {
// XXX what if somebody sends eg an "location-highlights.kml"
// attachment unrelated to location streaming?
if filename.starts_with("location") || filename.starts_with("message") {
let parsed = location::Kml::parse(self.context, decoded_data)
let parsed = location::Kml::parse(context, decoded_data)
.map_err(|err| {
warn!(self.context, "failed to parse kml part: {}", err);
warn!(context, "failed to parse kml part: {}", err);
})
.ok();
if filename.starts_with("location") {
@@ -651,17 +687,17 @@ impl<'a> MimeMessage<'a> {
/* we have a regular file attachment,
write decoded data to new blob object */
let blob = match BlobObject::create(self.context, filename, decoded_data) {
let blob = match BlobObject::create(context, filename, decoded_data) {
Ok(blob) => blob,
Err(err) => {
error!(
self.context,
context,
"Could not add blob for mime part {}, error {}", filename, err
);
return;
}
};
info!(self.context, "added blobfile: {:?}", blob.as_name());
info!(context, "added blobfile: {:?}", blob.as_name());
/* create and register Mime part referencing the new Blob object */
let mut part = Part::default();
@@ -719,38 +755,45 @@ impl<'a> MimeMessage<'a> {
.and_then(|msgid| parse_message_id(msgid))
}
fn merge_headers(&mut self, fields: &[mailparse::MailHeader<'_>]) {
fn merge_headers(headers: &mut HashMap<String, String>, fields: &[mailparse::MailHeader<'_>]) {
for field in fields {
if let Ok(key) = field.get_key() {
// lowercasing all headers is technically not correct, but makes things work better
let key = key.to_lowercase();
if !self.header.contains_key(&key) || // key already exists, only overwrite known types (protected headers)
if !headers.contains_key(&key) || // key already exists, only overwrite known types (protected headers)
is_known(&key) || key.starts_with("chat-")
{
if let Ok(value) = field.get_value() {
self.header.insert(key, value);
headers.insert(key, value);
}
}
}
}
}
fn process_report(&self, report: &mailparse::ParsedMail<'_>) -> Result<Option<Report>> {
fn process_report(
&self,
context: &Context,
report: &mailparse::ParsedMail<'_>,
) -> Result<Option<Report>> {
// parse as mailheaders
let report_body = report.subparts[1].get_body_raw()?;
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
// must be present
let disp = HeaderDef::Disposition.get_headername();
if let Some(_disposition) = report_fields.get_first_value(&disp).ok().flatten() {
if let Some(_disposition) = report_fields
.get_header_value(HeaderDef::Disposition)
.ok()
.flatten()
{
if let Some(original_message_id) = report_fields
.get_first_value(&HeaderDef::OriginalMessageId.get_headername())
.get_header_value(HeaderDef::OriginalMessageId)
.ok()
.flatten()
.and_then(|v| parse_message_id(&v))
{
let additional_message_ids = report_fields
.get_first_value(&HeaderDef::AdditionalMessageIds.get_headername())
.get_header_value(HeaderDef::AdditionalMessageIds)
.ok()
.flatten()
.map_or_else(Vec::new, |v| {
@@ -764,9 +807,9 @@ impl<'a> MimeMessage<'a> {
}
}
warn!(
self.context,
context,
"ignoring unknown disposition-notification, Message-Id: {:?}",
report_fields.get_first_value("Message-ID").ok()
report_fields.get_header_value(HeaderDef::MessageId).ok()
);
Ok(None)
@@ -775,6 +818,7 @@ impl<'a> MimeMessage<'a> {
/// Handle reports (only MDNs for now)
pub fn handle_reports(
&self,
context: &Context,
from_id: u32,
sent_timestamp: i64,
server_folder: impl AsRef<str>,
@@ -789,13 +833,10 @@ impl<'a> MimeMessage<'a> {
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(
self.context,
from_id,
original_message_id,
sent_timestamp,
) {
self.context.call_cb(Event::MsgRead { chat_id, msg_id });
if let Some((chat_id, msg_id)) =
message::mdn_from_ext(context, from_id, original_message_id, sent_timestamp)
{
context.call_cb(Event::MsgRead { chat_id, msg_id });
mdn_recognized = true;
}
}
@@ -805,10 +846,10 @@ impl<'a> MimeMessage<'a> {
let mut param = Params::new();
param.set(Param::ServerFolder, server_folder.as_ref());
param.set_int(Param::ServerUid, server_uid as i32);
if self.has_chat_version() && self.context.get_config_bool(Config::MvboxMove) {
if self.has_chat_version() && context.get_config_bool(Config::MvboxMove) {
param.set_int(Param::AlsoMove, 1);
}
job_add(self.context, Action::MarkseenMdnOnImap, 0, param, 0);
job_add(context, Action::MarkseenMdnOnImap, 0, param, 0);
}
}
}
@@ -1142,10 +1183,13 @@ mod tests {
let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]).unwrap();
let of = mimeparser.parse_first_addr(HeaderDef::From_).unwrap();
let of = mimeparser
.parse_first_addr(&context.ctx, HeaderDef::From_)
.unwrap();
assert_eq!(of, mailparse::addrparse("hello@one.org").unwrap()[0]);
let of = mimeparser.parse_first_addr(HeaderDef::ChatDispositionNotificationTo);
let of =
mimeparser.parse_first_addr(&context.ctx, HeaderDef::ChatDispositionNotificationTo);
assert!(of.is_none());
}
@@ -1445,4 +1489,16 @@ Additional-Message-IDs: <foo@example.com> <foo@example.net>\n\
&["foo@example.com", "foo@example.net"]
);
}
#[test]
fn mailparse_test() {
let body = b"From: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= <foobar@example.com>";
let mail = mailparse::parse_mail(body).unwrap();
let from = mail.headers[0].get_value().unwrap();
assert_eq!(&from, "Имя, Фамилия <foobar@example.com>");
let parsed = mailparse::addrparse(&from).unwrap();
assert_eq!(parsed.len(), 1);
}
}

View File

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

View File

@@ -1,5 +1,6 @@
//! # [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;
@@ -7,7 +8,7 @@ use num_traits::FromPrimitive;
use crate::aheader::*;
use crate::constants::*;
use crate::context::Context;
use crate::key::*;
use crate::key::{Key, SignedPublicKey};
use crate::sql::{self, Sql};
#[derive(Debug)]
@@ -126,7 +127,7 @@ impl<'a> Peerstate<'a> {
res.last_seen_autocrypt = message_time;
res.to_save = Some(ToSave::All);
res.prefer_encrypt = header.prefer_encrypt;
res.public_key = Some(header.public_key.clone());
res.public_key = Some(Key::from(header.public_key.clone()));
res.recalc_fingerprint();
res
@@ -137,7 +138,7 @@ impl<'a> Peerstate<'a> {
res.gossip_timestamp = message_time;
res.to_save = Some(ToSave::All);
res.gossip_key = Some(gossip_header.public_key.clone());
res.gossip_key = Some(Key::from(gossip_header.public_key.clone()));
res.recalc_fingerprint();
res
@@ -293,8 +294,8 @@ impl<'a> Peerstate<'a> {
self.to_save = Some(ToSave::All)
}
if self.public_key.as_ref() != Some(&header.public_key) {
self.public_key = Some(header.public_key.clone());
if self.public_key.as_ref() != Some(&Key::from(header.public_key.clone())) {
self.public_key = Some(Key::from(header.public_key.clone()));
self.recalc_fingerprint();
self.to_save = Some(ToSave::All);
}
@@ -309,8 +310,9 @@ impl<'a> Peerstate<'a> {
if message_time > self.gossip_timestamp {
self.gossip_timestamp = message_time;
self.to_save = Some(ToSave::Timestamps);
if self.gossip_key.as_ref() != Some(&gossip_header.public_key) {
self.gossip_key = Some(gossip_header.public_key.clone());
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);
self.recalc_fingerprint();
self.to_save = Some(ToSave::All)
}
@@ -320,9 +322,10 @@ 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(),
key.clone(),
public_key,
EncryptPreference::NoPreference,
);
Some(header.to_string())
@@ -332,18 +335,13 @@ impl<'a> Peerstate<'a> {
}
pub fn peek_key(&self, min_verified: PeerstateVerifiedStatus) -> Option<&Key> {
if self.public_key.is_none() && self.gossip_key.is_none() && self.verified_key.is_none() {
return None;
match min_verified {
PeerstateVerifiedStatus::BidirectVerified => self.verified_key.as_ref(),
PeerstateVerifiedStatus::Unverified => self
.public_key
.as_ref()
.or_else(|| self.gossip_key.as_ref()),
}
if min_verified != PeerstateVerifiedStatus::Unverified {
return self.verified_key.as_ref();
}
if self.public_key.is_some() {
return self.public_key.as_ref();
}
self.gossip_key.as_ref()
}
pub fn set_verified(
@@ -450,8 +448,8 @@ impl<'a> Peerstate<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[test]
@@ -459,11 +457,7 @@ mod tests {
let ctx = crate::test_utils::dummy_context();
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from_base64(
include_str!("../test-data/key/public.asc"),
KeyType::Public,
)
.unwrap();
let pub_key = crate::key::Key::from(alice_keypair().public);
let mut peerstate = Peerstate {
context: &ctx.ctx,
@@ -503,12 +497,7 @@ mod tests {
fn test_peerstate_double_create() {
let ctx = crate::test_utils::dummy_context();
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from_base64(
include_str!("../test-data/key/public.asc"),
KeyType::Public,
)
.unwrap();
let pub_key = crate::key::Key::from(alice_keypair().public);
let peerstate = Peerstate {
context: &ctx.ctx,
@@ -542,11 +531,7 @@ mod tests {
let ctx = crate::test_utils::dummy_context();
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from_base64(
include_str!("../test-data/key/public.asc"),
KeyType::Public,
)
.unwrap();
let pub_key = crate::key::Key::from(alice_keypair().public);
let mut peerstate = Peerstate {
context: &ctx.ctx,

View File

@@ -16,6 +16,7 @@ use pgp::types::{
};
use rand::{thread_rng, CryptoRng, Rng};
use crate::dc_tools::EmailAddress;
use crate::error::Result;
use crate::key::*;
use crate::keyring::*;
@@ -111,10 +112,43 @@ pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, Str
Ok((typ, headers, bytes))
}
/// Create a new key pair.
pub fn create_keypair(addr: impl AsRef<str>) -> Option<(Key, Key)> {
let user_id = format!("<{}>", addr.as_ref());
/// Error with generating a PGP keypair.
///
/// Most of these are likely coding errors rather than user errors
/// since all variability is hardcoded.
#[derive(Fail, Debug)]
#[fail(display = "PgpKeygenError: {}", message)]
pub(crate) struct PgpKeygenError {
message: String,
#[cause]
cause: failure::Error,
backtrace: failure::Backtrace,
}
impl PgpKeygenError {
fn new(message: impl Into<String>, cause: impl Into<failure::Error>) -> Self {
Self {
message: message.into(),
cause: cause.into(),
backtrace: failure::Backtrace::new(),
}
}
}
/// A PGP keypair.
///
/// This has it's own struct to be able to keep the public and secret
/// keys together as they are one unit.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct KeyPair {
pub addr: EmailAddress,
pub public: SignedPublicKey,
pub secret: SignedSecretKey,
}
/// Create a new key pair.
pub(crate) fn create_keypair(addr: EmailAddress) -> std::result::Result<KeyPair, PgpKeygenError> {
let user_id = format!("<{}>", addr);
let key_params = SecretKeyParamsBuilder::default()
.key_type(PgpKeyType::Rsa(2048))
.can_create_certificates(true)
@@ -146,20 +180,29 @@ pub fn create_keypair(addr: impl AsRef<str>) -> Option<(Key, Key)> {
.unwrap(),
)
.build()
.expect("invalid key params");
let key = key_params.generate().expect("invalid params");
.map_err(|err| PgpKeygenError::new("invalid key params", failure::err_msg(err)))?;
let key = key_params
.generate()
.map_err(|err| PgpKeygenError::new("invalid params", err))?;
let private_key = key.sign(|| "".into()).expect("failed to sign secret key");
let public_key = private_key.public_key();
let public_key = public_key
.sign(&private_key, || "".into())
.expect("failed to sign public key");
.map_err(|err| PgpKeygenError::new("failed to sign public key", err))?;
private_key.verify().expect("invalid private key generated");
public_key.verify().expect("invalid public key generated");
private_key
.verify()
.map_err(|err| PgpKeygenError::new("invalid private key generated", err))?;
public_key
.verify()
.map_err(|err| PgpKeygenError::new("invalid public key generated", err))?;
Some((Key::Public(public_key), Key::Secret(private_key)))
Ok(KeyPair {
addr,
public: public_key,
secret: private_key,
})
}
/// Select public key or subkey to use for encryption.
@@ -311,6 +354,8 @@ pub fn symm_decrypt<T: std::io::Read + std::io::Seek>(
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use lazy_static::lazy_static;
#[test]
fn test_split_armored_data_1() {
@@ -338,4 +383,177 @@ mod tests {
assert!(!base64.is_empty());
assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
}
#[test]
#[ignore] // is too expensive
fn test_create_keypair() {
let keypair0 = create_keypair(EmailAddress::new("foo@bar.de").unwrap()).unwrap();
let keypair1 = create_keypair(EmailAddress::new("two@zwo.de").unwrap()).unwrap();
assert_ne!(keypair0.public, keypair1.public);
}
/// [Key] objects to use in tests.
struct TestKeys {
alice_secret: Key,
alice_public: Key,
bob_secret: Key,
bob_public: Key,
}
impl TestKeys {
fn new() -> TestKeys {
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()),
}
}
}
/// The original text of [CTEXT_SIGNED]
static CLEARTEXT: &[u8] = b"This is a test";
lazy_static! {
/// Initialised [TestKeys] for tests.
static ref KEYS: TestKeys = TestKeys::new();
/// A cyphertext encrypted to Alice & Bob, signed by Alice.
static ref CTEXT_SIGNED: String = {
let mut keyring = Keyring::default();
keyring.add_owned(KEYS.alice_public.clone());
keyring.add_ref(&KEYS.bob_public);
pk_encrypt(CLEARTEXT, &keyring, Some(&KEYS.alice_secret)).unwrap()
};
/// A cyphertext encrypted to Alice & Bob, not signed.
static ref CTEXT_UNSIGNED: String = {
let mut keyring = Keyring::default();
keyring.add_owned(KEYS.alice_public.clone());
keyring.add_ref(&KEYS.bob_public);
pk_encrypt(CLEARTEXT, &keyring, None).unwrap()
};
}
#[test]
fn test_encrypt_signed() {
assert!(!CTEXT_SIGNED.is_empty());
assert!(CTEXT_SIGNED.starts_with("-----BEGIN PGP MESSAGE-----"));
}
#[test]
fn test_encrypt_unsigned() {
assert!(!CTEXT_UNSIGNED.is_empty());
assert!(CTEXT_UNSIGNED.starts_with("-----BEGIN PGP MESSAGE-----"));
}
#[test]
fn test_decrypt_singed() {
// Check decrypting as Alice
let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.alice_secret);
let mut sig_check_keyring = Keyring::default();
sig_check_keyring.add_ref(&KEYS.alice_public);
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
Some(&mut valid_signatures),
)
.map_err(|err| println!("{:?}", err))
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
// Check decrypting as Bob
let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.bob_secret);
let mut sig_check_keyring = Keyring::default();
sig_check_keyring.add_ref(&KEYS.alice_public);
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
Some(&mut valid_signatures),
)
.map_err(|err| println!("{:?}", err))
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 1);
}
#[test]
fn test_decrypt_no_sig_check() {
let mut keyring = Keyring::default();
keyring.add_ref(&KEYS.alice_secret);
let empty_keyring = Keyring::default();
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(),
&keyring,
&empty_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
#[test]
fn test_decrypt_signed_no_key() {
// The validation does not have the public key of the signer.
let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.bob_secret);
let mut sig_check_keyring = Keyring::default();
sig_check_keyring.add_ref(&KEYS.bob_public);
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
#[test]
fn test_decrypt_unsigned() {
let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.bob_secret);
let sig_check_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.alice_public);
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pk_decrypt(
CTEXT_UNSIGNED.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
#[test]
fn test_decrypt_signed_no_sigret() {
// Check decrypting signed cyphertext without providing the HashSet for signatures.
let mut decrypt_keyring = Keyring::default();
decrypt_keyring.add_ref(&KEYS.bob_secret);
let mut sig_check_keyring = Keyring::default();
sig_check_keyring.add_ref(&KEYS.alice_public);
let plain = pk_decrypt(
CTEXT_SIGNED.as_bytes(),
&decrypt_keyring,
&sig_check_keyring,
None,
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
}
}

308
src/provider/data.rs Normal file
View File

@@ -0,0 +1,308 @@
// file generated by src/provider/update.py
use crate::provider::Protocol::*;
use crate::provider::Socket::*;
use crate::provider::UsernamePattern::*;
use crate::provider::*;
use std::collections::HashMap;
lazy_static::lazy_static! {
// aktivix.org.md: aktivix.org
static ref P_AKTIVIX_ORG: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/aktivix-org",
server: vec![
Server { protocol: IMAP, socket: STARTTLS, hostname: "newyear.aktivix.org", port: 143, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "newyear.aktivix.org", port: 25, username_pattern: EMAIL },
],
};
// autistici.org.md: autistici.org
static ref P_AUTISTICI_ORG: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/autistici-org",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "mail.autistici.org", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "smtp.autistici.org", port: 465, username_pattern: EMAIL },
],
};
// bluewin.ch.md: bluewin.ch
static ref P_BLUEWIN_CH: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/bluewin-ch",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imaps.bluewin.ch", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "smtpauths.bluewin.ch", port: 465, username_pattern: EMAIL },
],
};
// comcast.md: xfinity.com, comcast.net
// - skipping provider with status OK and no special things to do
// dismail.de.md: dismail.de
// - skipping provider with status OK and no special things to do
// disroot.md: disroot.org
// - skipping provider with status OK and no special things to do
// example.com.md: example.com, example.org
static ref P_EXAMPLE_COM: Provider = Provider {
status: Status::BROKEN,
before_login_hint: "Hush this provider doesn't exist!",
after_login_hint: "This provider doesn't really exist, so you can't use it :/ If you need an email provider for Delta Chat, take a look at providers.delta.chat!",
overview_page: "https://providers.delta.chat/example-com",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.example.com", port: 1337, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.example.com", port: 1337, username_pattern: EMAIL },
],
};
// freenet.de.md: freenet.de
static ref P_FREENET_DE: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/freenet-de",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "mx.freenet.de", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "mx.freenet.de", port: 587, username_pattern: EMAIL },
],
};
// gmail.md: gmail.com, googlemail.com
static ref P_GMAIL: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "For Gmail accounts, you need to create an app-password if you have \"2-Step Verification\" enabled. If this setting is not available, you need to enable \"less secure apps\".",
after_login_hint: "",
overview_page: "https://providers.delta.chat/gmail",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.gmail.com", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "smtp.gmail.com", port: 465, username_pattern: EMAIL },
],
};
// gmx.net.md: gmx.net, gmx.de, gmx.at, gmx.ch, gmx.org, gmx.eu, gmx.info, gmx.biz, gmx.com
static ref P_GMX_NET: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "You must allow IMAP access to your account before you can login.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/gmx-net",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.gmx.net", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "mail.gmx.net", port: 465, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "mail.gmx.net", port: 587, username_pattern: EMAIL },
],
};
// i.ua.md: i.ua
// - skipping provider with status OK and no special things to do
// icloud.md: icloud.com, me.com, mac.com
static ref P_ICLOUD: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "You must create an app-specific password for Delta Chat before you can login.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/icloud",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.mail.me.com", port: 993, username_pattern: EMAILLOCALPART },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.mail.me.com", port: 587, username_pattern: EMAIL },
],
};
// kolst.com.md: kolst.com
// - skipping provider with status OK and no special things to do
// kontent.com.md: kontent.com
// - skipping provider with status OK and no special things to do
// mail.ru.md: mail.ru, inbox.ru, bk.ru, list.ru
// - skipping provider with status OK and no special things to do
// mailbox.org.md: mailbox.org, secure.mailbox.org
// - skipping provider with status OK and no special things to do
// nauta.cu.md: nauta.cu
static ref P_NAUTA_CU: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "Atención - con nauta.cu, puede enviar mensajes sólo a un máximo de 20 personas a la vez. En grupos más grandes, no puede enviar mensajes o abandonar el grupo.",
overview_page: "https://providers.delta.chat/nauta-cu",
server: vec![
Server { protocol: IMAP, socket: STARTTLS, hostname: "imap.nauta.cu", port: 143, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.nauta.cu", port: 25, username_pattern: EMAIL },
],
};
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com
static ref P_OUTLOOK_COM: Provider = Provider {
status: Status::BROKEN,
before_login_hint: "Outlook.com email addresses will not work as expected as these servers remove some important transport information. Hopefully sooner or later there will be a fix, for now we suggest to use another email address.",
after_login_hint: "Outlook.com email addresses will not work as expected as these servers remove some important transport information. Unencrypted 1-on-1 chats kind of work, but groups and encryption don't. Hopefully sooner or later there will be a fix, for now we suggest to use another email address.",
overview_page: "https://providers.delta.chat/outlook-com",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap-mail.outlook.com", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp-mail.outlook.com", port: 587, username_pattern: EMAIL },
],
};
// posteo.md: posteo.de
static ref P_POSTEO: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/posteo",
server: vec![
Server { protocol: IMAP, socket: STARTTLS, hostname: "posteo.de", port: 143, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "posteo.de", port: 587, username_pattern: EMAIL },
],
};
// riseup.net.md: riseup.net
// - skipping provider with status OK and no special things to do
// rogers.com.md: rogers.com
// - skipping provider with status OK and no special things to do
// tiscali.it.md: tiscali.it
static ref P_TISCALI_IT: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/tiscali-it",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.tiscali.it", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "smtp.tiscali.it", port: 465, username_pattern: EMAIL },
],
};
// ukr.net.md: ukr.net
// - skipping provider with status OK and no special things to do
// vfemail.md: vfemail.net
// - skipping provider with status OK and no special things to do
// 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 {
status: Status::PREPARATION,
before_login_hint: "You must allow IMAP access to your account before you can login.",
after_login_hint: "Note: if you have your web.de spam settings too strict, you won't receive contact requests from new people. If you want to receive contact requests, you should disable the \"3-Wege-Spamschutz\" in the web.de settings. Read how: https://hilfe.web.de/email/spam-und-viren/spamschutz-einstellungen.html",
overview_page: "https://providers.delta.chat/web-de",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.web.de", port: 993, username_pattern: EMAILLOCALPART },
Server { protocol: IMAP, socket: STARTTLS, hostname: "imap.web.de", port: 143, username_pattern: EMAILLOCALPART },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.web.de", port: 587, username_pattern: EMAILLOCALPART },
],
};
// 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.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/yahoo",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.mail.yahoo.com", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: SSL, hostname: "smtp.mail.yahoo.com", port: 465, username_pattern: EMAIL },
],
};
// yandex.ru.md: yandex.ru, yandex.com
// - skipping provider with status OK and no special things to do
// ziggo.nl.md: ziggo.nl
static ref P_ZIGGO_NL: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/ziggo-nl",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "imap.ziggo.nl", port: 993, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "smtp.ziggo.nl", port: 587, username_pattern: EMAIL },
],
};
// zoho.com.md: zoho.com
// - skipping provider with status OK and no special things to do
pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [
("aktivix.org", &*P_AKTIVIX_ORG),
("autistici.org", &*P_AUTISTICI_ORG),
("bluewin.ch", &*P_BLUEWIN_CH),
("example.com", &*P_EXAMPLE_COM),
("example.org", &*P_EXAMPLE_COM),
("freenet.de", &*P_FREENET_DE),
("gmail.com", &*P_GMAIL),
("googlemail.com", &*P_GMAIL),
("gmx.net", &*P_GMX_NET),
("gmx.de", &*P_GMX_NET),
("gmx.at", &*P_GMX_NET),
("gmx.ch", &*P_GMX_NET),
("gmx.org", &*P_GMX_NET),
("gmx.eu", &*P_GMX_NET),
("gmx.info", &*P_GMX_NET),
("gmx.biz", &*P_GMX_NET),
("gmx.com", &*P_GMX_NET),
("icloud.com", &*P_ICLOUD),
("me.com", &*P_ICLOUD),
("mac.com", &*P_ICLOUD),
("nauta.cu", &*P_NAUTA_CU),
("hotmail.com", &*P_OUTLOOK_COM),
("outlook.com", &*P_OUTLOOK_COM),
("office365.com", &*P_OUTLOOK_COM),
("outlook.com.tr", &*P_OUTLOOK_COM),
("live.com", &*P_OUTLOOK_COM),
("posteo.de", &*P_POSTEO),
("tiscali.it", &*P_TISCALI_IT),
("web.de", &*P_WEB_DE),
("email.de", &*P_WEB_DE),
("flirt.ms", &*P_WEB_DE),
("hallo.ms", &*P_WEB_DE),
("kuss.ms", &*P_WEB_DE),
("love.ms", &*P_WEB_DE),
("magic.ms", &*P_WEB_DE),
("singles.ms", &*P_WEB_DE),
("cool.ms", &*P_WEB_DE),
("kanzler.ms", &*P_WEB_DE),
("okay.ms", &*P_WEB_DE),
("party.ms", &*P_WEB_DE),
("pop.ms", &*P_WEB_DE),
("stars.ms", &*P_WEB_DE),
("techno.ms", &*P_WEB_DE),
("clever.ms", &*P_WEB_DE),
("deutschland.ms", &*P_WEB_DE),
("genial.ms", &*P_WEB_DE),
("ich.ms", &*P_WEB_DE),
("online.ms", &*P_WEB_DE),
("smart.ms", &*P_WEB_DE),
("wichtig.ms", &*P_WEB_DE),
("action.ms", &*P_WEB_DE),
("fussball.ms", &*P_WEB_DE),
("joker.ms", &*P_WEB_DE),
("planet.ms", &*P_WEB_DE),
("power.ms", &*P_WEB_DE),
("yahoo.com", &*P_YAHOO),
("yahoo.de", &*P_YAHOO),
("yahoo.it", &*P_YAHOO),
("yahoo.fr", &*P_YAHOO),
("yahoo.es", &*P_YAHOO),
("yahoo.se", &*P_YAHOO),
("yahoo.co.uk", &*P_YAHOO),
("yahoo.co.nz", &*P_YAHOO),
("yahoo.com.au", &*P_YAHOO),
("yahoo.com.ar", &*P_YAHOO),
("yahoo.com.br", &*P_YAHOO),
("yahoo.com.mx", &*P_YAHOO),
("ymail.com", &*P_YAHOO),
("rocketmail.com", &*P_YAHOO),
("yahoodns.net", &*P_YAHOO),
("ziggo.nl", &*P_ZIGGO_NL),
].iter().copied().collect();
}

144
src/provider/mod.rs Normal file
View File

@@ -0,0 +1,144 @@
mod data;
use crate::dc_tools::EmailAddress;
use crate::provider::data::PROVIDER_DATA;
#[derive(Debug, Copy, Clone, PartialEq, ToPrimitive)]
#[repr(u8)]
pub enum Status {
OK = 1,
PREPARATION = 2,
BROKEN = 3,
}
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum Protocol {
SMTP = 1,
IMAP = 2,
}
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum Socket {
STARTTLS = 1,
SSL = 2,
}
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum UsernamePattern {
EMAIL = 1,
EMAILLOCALPART = 2,
}
#[derive(Debug)]
pub struct Server {
pub protocol: Protocol,
pub socket: Socket,
pub hostname: &'static str,
pub port: u16,
pub username_pattern: UsernamePattern,
}
impl Server {
pub fn apply_username_pattern(&self, addr: String) -> String {
match self.username_pattern {
UsernamePattern::EMAIL => addr,
UsernamePattern::EMAILLOCALPART => {
if let Some(at) = addr.find('@') {
return addr.split_at(at).0.to_string();
}
addr
}
}
}
}
#[derive(Debug)]
pub struct Provider {
pub status: Status,
pub before_login_hint: &'static str,
pub after_login_hint: &'static str,
pub overview_page: &'static str,
pub server: Vec<Server>,
}
impl Provider {
pub fn get_server(&self, protocol: Protocol) -> Option<&Server> {
for record in self.server.iter() {
if record.protocol == protocol {
return Some(record);
}
}
None
}
pub fn get_imap_server(&self) -> Option<&Server> {
self.get_server(Protocol::IMAP)
}
pub fn get_smtp_server(&self) -> Option<&Server> {
self.get_server(Protocol::SMTP)
}
}
pub fn get_provider_info(addr: &str) -> Option<&Provider> {
let domain = match addr.parse::<EmailAddress>() {
Ok(addr) => addr.domain,
Err(_err) => return None,
}
.to_lowercase();
if let Some(provider) = PROVIDER_DATA.get(domain.as_str()) {
return Some(*provider);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_provider_info_unexistant() {
let provider = get_provider_info("user@unexistant.org");
assert!(provider.is_none());
}
#[test]
fn test_get_provider_info_mixed_case() {
let provider = get_provider_info("uSer@nAUta.Cu").unwrap();
assert!(provider.status == Status::OK);
}
#[test]
fn test_get_provider_info() {
let provider = get_provider_info("nauta.cu"); // this is no email address
assert!(provider.is_none());
let provider = get_provider_info("user@nauta.cu").unwrap();
assert!(provider.status == Status::OK);
let server = provider.get_imap_server().unwrap();
assert_eq!(server.protocol, Protocol::IMAP);
assert_eq!(server.socket, Socket::STARTTLS);
assert_eq!(server.hostname, "imap.nauta.cu");
assert_eq!(server.port, 143);
assert_eq!(server.username_pattern, UsernamePattern::EMAIL);
let server = provider.get_smtp_server().unwrap();
assert_eq!(server.protocol, Protocol::SMTP);
assert_eq!(server.socket, Socket::STARTTLS);
assert_eq!(server.hostname, "smtp.nauta.cu");
assert_eq!(server.port, 25);
assert_eq!(server.username_pattern, UsernamePattern::EMAIL);
let provider = get_provider_info("user@gmail.com").unwrap();
assert!(provider.status == Status::PREPARATION);
assert!(!provider.before_login_hint.is_empty());
assert!(!provider.overview_page.is_empty());
let provider = get_provider_info("user@googlemail.com").unwrap();
assert!(provider.status == Status::PREPARATION);
}
}

148
src/provider/update.py Executable file
View File

@@ -0,0 +1,148 @@
#!/usr/bin/env python3
# if the yaml import fails, run "pip install pyyaml"
import sys
import os
import yaml
out_all = ""
out_domains = ""
domains_dict = {}
def cleanstr(s):
s = s.strip()
s = s.replace("\n", " ")
s = s.replace("\\", "\\\\")
s = s.replace("\"", "\\\"")
return s
def file2varname(f):
f = f[f.rindex("/")+1:].replace(".md", "")
f = f.replace(".", "_")
f = f.replace("-", "_")
return "P_" + f.upper()
def file2url(f):
f = f[f.rindex("/")+1:].replace(".md", "")
f = f.replace(".", "-")
return "https://providers.delta.chat/" + f
def process_data(data, file):
status = data.get("status", "")
if status != "OK" and status != "PREPARATION" and status != "BROKEN":
raise TypeError("bad status")
comment = ""
domains = ""
if not "domains" in data:
raise TypeError("no domains found")
for domain in data["domains"]:
domain = cleanstr(domain)
if domain == "" or domain.count(".") < 1 or domain.lower() != domain:
raise TypeError("bad domain: " + domain)
global domains_dict
if domains_dict.get(domain, False):
raise TypeError("domain used twice: " + domain)
domains_dict[domain] = True
domains += " (\"" + domain + "\", &*" + file2varname(file) + "),\n"
comment += domain + ", "
server = ""
has_imap = False
has_smtp = False
if "server" in data:
for s in data["server"]:
hostname = cleanstr(s.get("hostname", ""))
port = int(s.get("port", ""))
if hostname == "" or hostname.count(".") < 1 or port <= 0:
raise TypeError("bad hostname or port")
protocol = s.get("type", "").upper()
if protocol == "IMAP":
has_imap = True
elif protocol == "SMTP":
has_smtp = True
else:
raise TypeError("bad protocol")
socket = s.get("socket", "").upper()
if socket != "STARTTLS" and socket != "SSL":
raise TypeError("bad socket")
username_pattern = s.get("username_pattern", "EMAIL").upper()
if username_pattern != "EMAIL" and username_pattern != "EMAILLOCALPART":
raise TypeError("bad username pattern")
server += (" Server { protocol: " + protocol + ", socket: " + socket + ", hostname: \""
+ hostname + "\", port: " + str(port) + ", username_pattern: " + username_pattern + " },\n")
provider = ""
before_login_hint = cleanstr(data.get("before_login_hint", ""))
after_login_hint = cleanstr(data.get("after_login_hint", ""))
if (not has_imap and not has_smtp) or (has_imap and has_smtp):
provider += " static ref " + file2varname(file) + ": Provider = Provider {\n"
provider += " status: Status::" + status + ",\n"
provider += " before_login_hint: \"" + before_login_hint + "\",\n"
provider += " after_login_hint: \"" + after_login_hint + "\",\n"
provider += " overview_page: \"" + file2url(file) + "\",\n"
provider += " server: vec![\n" + server + " ],\n"
provider += " };\n\n"
else:
raise TypeError("SMTP and IMAP must be specified together or left out both")
if status != "OK" and before_login_hint == "":
raise TypeError("status PREPARATION or BROKEN requires before_login_hint: " + 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 == "":
out_all += " // - skipping provider with status OK and no special things to do\n\n"
else:
out_all += provider
out_domains += domains
def process_file(file):
print("processing file: " + file, file=sys.stderr)
with open(file) as f:
# load_all() loads "---"-separated yamls -
# by coincidence, this is also the frontmatter separator :)
data = next(yaml.load_all(f, Loader=yaml.SafeLoader))
process_data(data, file)
def process_dir(dir):
print("processing directory: " + dir, file=sys.stderr)
files = [f for f in os.listdir(dir) if f.endswith(".md")]
files.sort()
for f in files:
process_file(os.path.join(dir, f))
if __name__ == "__main__":
if len(sys.argv) < 2:
raise SystemExit("usage: update.py DIR_WITH_MD_FILES > data.rs")
out_all = ("// file generated by src/provider/update.py\n\n"
"use crate::provider::Protocol::*;\n"
"use crate::provider::Socket::*;\n"
"use crate::provider::UsernamePattern::*;\n"
"use crate::provider::*;\n"
"use std::collections::HashMap;\n\n"
"lazy_static::lazy_static! {\n\n")
process_dir(sys.argv[1])
out_all += " pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [\n"
out_all += out_domains;
out_all += " ].iter().copied().collect();\n}"
print(out_all)

View File

@@ -4,17 +4,21 @@ use lazy_static::lazy_static;
use percent_encoding::percent_decode_str;
use crate::chat;
use crate::config::*;
use crate::constants::Blocked;
use crate::contact::*;
use crate::context::Context;
use crate::error::Error;
use crate::key::dc_format_fingerprint;
use crate::key::*;
use crate::key::dc_normalize_fingerprint;
use crate::lot::{Lot, LotState};
use crate::param::*;
use crate::peerstate::*;
use reqwest::Url;
use serde::Deserialize;
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
const MAILTO_SCHEME: &str = "mailto:";
const MATMSG_SCHEME: &str = "MATMSG:";
const VCARD_SCHEME: &str = "BEGIN:VCARD";
@@ -43,6 +47,8 @@ pub fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
if qr.starts_with(OPENPGP4FPR_SCHEME) {
decode_openpgp(context, qr)
} else if qr.starts_with(DCACCOUNT_SCHEME) {
decode_account(context, qr)
} else if qr.starts_with(MAILTO_SCHEME) {
decode_mailto(context, qr)
} else if qr.starts_with(SMTP_SCHEME) {
@@ -179,6 +185,73 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
lot
}
/// scheme: `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3`
fn decode_account(_context: &Context, qr: &str) -> Lot {
let payload = &qr[DCACCOUNT_SCHEME.len()..];
let mut lot = Lot::new();
if let Ok(url) = Url::parse(payload) {
if url.scheme() == "https" {
lot.state = LotState::QrAccount;
lot.text1 = url.host_str().map(|x| x.to_string());
} else {
lot.state = LotState::QrError;
lot.text1 = Some(format!("Bad scheme for account url: {}", payload));
}
} else {
lot.state = LotState::QrError;
lot.text1 = Some(format!("Invalid account url: {}", payload));
}
lot
}
#[derive(Debug, Deserialize)]
struct CreateAccountResponse {
email: String,
password: String,
}
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
/// download additional information from the contained url and set the parameters.
/// on success, a configure::configure() should be able to log in to the account
pub fn set_config_from_qr(context: &Context, qr: &str) -> Result<(), Error> {
let url_str = &qr[DCACCOUNT_SCHEME.len()..];
let response = reqwest::blocking::Client::new().post(url_str).send();
if response.is_err() {
return Err(format_err!(
"Cannot create account, request to {} failed",
url_str
));
}
let response = response.unwrap();
if !response.status().is_success() {
return Err(format_err!(
"Request to {} unsuccessful: {:?}",
url_str,
response
));
}
let parsed: reqwest::Result<CreateAccountResponse> = response.json();
if parsed.is_err() {
return Err(format_err!(
"Failed to parse JSON response from {}: error: {:?}",
url_str,
parsed
));
}
println!("response: {:?}", &parsed);
let parsed = parsed.unwrap();
context.set_config(Config::Addr, Some(&parsed.email))?;
context.set_config(Config::MailPw, Some(&parsed.password))?;
Ok(())
}
/// Extract address for the mailto scheme.
///
/// Scheme: `mailto:addr...?subject=...&body=..`
@@ -493,4 +566,28 @@ mod tests {
assert_eq!(res.get_state(), LotState::QrError);
assert_eq!(res.get_id(), 0);
}
#[test]
fn test_decode_account() {
let ctx = dummy_context();
let res = check_qr(
&ctx.ctx,
"DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
);
assert_eq!(res.get_state(), LotState::QrAccount);
assert_eq!(res.get_text1().unwrap(), "example.org");
}
#[test]
fn test_decode_account_bad_scheme() {
let ctx = dummy_context();
let res = check_qr(
&ctx.ctx,
"DCACCOUNT:http://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
);
assert_eq!(res.get_state(), LotState::QrError);
assert!(res.get_text1().is_some());
}
}

View File

@@ -12,7 +12,7 @@ use crate::e2ee::*;
use crate::error::Error;
use crate::events::Event;
use crate::headerdef::HeaderDef;
use crate::key::*;
use crate::key::{dc_normalize_fingerprint, Key};
use crate::lot::LotState;
use crate::message::Message;
use crate::mimeparser::*;
@@ -143,6 +143,8 @@ fn get_self_fingerprint(context: &Context) -> Option<String> {
None
}
/// Take a scanned QR-code and do the setup-contact/join-group handshake.
/// See the ffi-documentation for more details.
pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
let cleanup =
|context: &Context, contact_chat_id: ChatId, ongoing_allocated: bool, join_vg: bool| {
@@ -257,11 +259,18 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> ChatId {
);
}
while !context.shall_stop_ongoing() {
// Don't sleep too long, the user is waiting.
std::thread::sleep(std::time::Duration::from_millis(200));
if join_vg {
// for a group-join, wait until the secure-join is done and the group is created
while !context.shall_stop_ongoing() {
std::thread::sleep(std::time::Duration::from_millis(200));
}
cleanup(&context, contact_chat_id, true, join_vg)
} else {
// for a one-to-one-chat, the chat is already known, return the chat-id,
// the verification runs in background
context.free_ongoing();
contact_chat_id
}
cleanup(&context, contact_chat_id, true, join_vg)
}
fn send_handshake_msg(
@@ -402,7 +411,7 @@ pub(crate) fn handle_securejoin_handshake(
match chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not) {
Ok((chat_id, blocked)) => {
if blocked != Blocked::Not {
chat::unblock(context, chat_id);
chat_id.unblock(context);
}
chat_id
}
@@ -476,7 +485,7 @@ pub(crate) fn handle_securejoin_handshake(
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
let auth = get_qr_attr!(context, auth).to_string();
if !encrypted_and_signed(mime_message, &scanned_fingerprint_of_alice) {
if !encrypted_and_signed(context, mime_message, &scanned_fingerprint_of_alice) {
could_not_establish_secure_connection(
context,
contact_chat_id,
@@ -539,7 +548,7 @@ pub(crate) fn handle_securejoin_handshake(
return Ok(HandshakeMessage::Ignore);
}
};
if !encrypted_and_signed(mime_message, &fingerprint) {
if !encrypted_and_signed(context, mime_message, &fingerprint) {
could_not_establish_secure_connection(
context,
contact_chat_id,
@@ -623,22 +632,27 @@ pub(crate) fn handle_securejoin_handshake(
==== Bob - the joiner's side ====
==== Step 7 in "Setup verified contact" protocol ====
=======================================================*/
let abort_retval = if join_vg {
HandshakeMessage::Propagate
} else {
HandshakeMessage::Ignore
};
if context.bob.read().unwrap().expects != DC_VC_CONTACT_CONFIRM {
info!(context, "Message belongs to a different handshake.",);
return Ok(HandshakeMessage::Propagate);
return Ok(abort_retval);
}
let cond = {
let bob = context.bob.read().unwrap();
let scan = bob.qr_scan.as_ref();
scan.is_none() || join_vg && scan.unwrap().state != LotState::QrAskVerifyGroup
scan.is_none() || (join_vg && scan.unwrap().state != LotState::QrAskVerifyGroup)
};
if cond {
warn!(
context,
"Message out of sync or belongs to a different handshake.",
);
return Ok(HandshakeMessage::Propagate);
return Ok(abort_retval);
}
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
@@ -661,7 +675,7 @@ pub(crate) fn handle_securejoin_handshake(
true
};
if vg_expect_encrypted
&& !encrypted_and_signed(mime_message, &scanned_fingerprint_of_alice)
&& !encrypted_and_signed(context, mime_message, &scanned_fingerprint_of_alice)
{
could_not_establish_secure_connection(
context,
@@ -669,7 +683,7 @@ pub(crate) fn handle_securejoin_handshake(
"Contact confirm message not encrypted.",
);
context.bob.write().unwrap().status = 0;
return Ok(HandshakeMessage::Propagate);
return Ok(abort_retval);
}
if mark_peer_as_verified(context, &scanned_fingerprint_of_alice).is_err() {
@@ -678,7 +692,7 @@ pub(crate) fn handle_securejoin_handshake(
contact_chat_id,
"Fingerprint mismatch on joiner-side.",
);
return Ok(HandshakeMessage::Propagate);
return Ok(abort_retval);
}
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinJoined);
emit_event!(context, Event::ContactsChanged(None));
@@ -692,7 +706,7 @@ pub(crate) fn handle_securejoin_handshake(
.map_err(|_| HandshakeError::NoSelfAddr)?
{
info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group).");
return Ok(HandshakeMessage::Propagate);
return Ok(abort_retval);
}
secure_connection_established(context, contact_chat_id);
context.bob.write().unwrap().expects = 0;
@@ -709,7 +723,11 @@ pub(crate) fn handle_securejoin_handshake(
}
context.bob.write().unwrap().status = 1;
context.stop_ongoing();
Ok(HandshakeMessage::Propagate)
Ok(if join_vg {
HandshakeMessage::Propagate
} else {
HandshakeMessage::Done
})
}
"vg-member-added-received" => {
/*==========================================================
@@ -812,22 +830,26 @@ fn mark_peer_as_verified(context: &Context, fingerprint: impl AsRef<str>) -> Res
* Tools: Misc.
******************************************************************************/
fn encrypted_and_signed(mimeparser: &MimeMessage, expected_fingerprint: impl AsRef<str>) -> bool {
fn encrypted_and_signed(
context: &Context,
mimeparser: &MimeMessage,
expected_fingerprint: impl AsRef<str>,
) -> bool {
if !mimeparser.was_encrypted() {
warn!(mimeparser.context, "Message not encrypted.",);
warn!(context, "Message not encrypted.",);
false
} else if mimeparser.signatures.is_empty() {
warn!(mimeparser.context, "Message not signed.",);
warn!(context, "Message not signed.",);
false
} else if expected_fingerprint.as_ref().is_empty() {
warn!(mimeparser.context, "Fingerprint for comparison missing.",);
warn!(context, "Fingerprint for comparison missing.",);
false
} else if !mimeparser
.signatures
.contains(expected_fingerprint.as_ref())
{
warn!(
mimeparser.context,
context,
"Message does not match expected fingerprint {}.",
expected_fingerprint.as_ref(),
);

View File

@@ -38,11 +38,11 @@ pub enum Error {
Oauth2Error { address: String },
#[fail(display = "TLS error")]
Tls(#[cause] native_tls::Error),
Tls(#[cause] async_native_tls::Error),
}
impl From<native_tls::Error> for Error {
fn from(err: native_tls::Error) -> Error {
impl From<async_native_tls::Error> for Error {
fn from(err: async_native_tls::Error) -> Error {
Error::Tls(err)
}
}
@@ -104,7 +104,7 @@ impl Smtp {
let domain = &lp.send_server;
let port = lp.send_port as u16;
let tls_config = dc_build_tls(lp.smtp_certificate_checks)?.into();
let tls_config = dc_build_tls(lp.smtp_certificate_checks);
let tls_parameters = ClientTlsParameters::new(domain.to_string(), tls_config);
let (creds, mechanism) = if 0 != lp.server_flags & (DC_LP_AUTH_OAUTH2 as i32) {

View File

@@ -217,20 +217,37 @@ impl Sql {
.unwrap_or_default()
}
pub fn query_get_value<P, T>(&self, context: &Context, query: &str, params: P) -> Option<T>
/// Executes a query which is expected to return one row and one
/// column. If the query does not return a value or returns SQL
/// `NULL`, returns `Ok(None)`.
pub fn query_get_value_result<P, T>(&self, query: &str, params: P) -> Result<Option<T>>
where
P: IntoIterator,
P::Item: rusqlite::ToSql,
T: rusqlite::types::FromSql,
{
match self.query_row(query, params, |row| row.get::<_, T>(0)) {
Ok(res) => Some(res),
Err(Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => None,
Ok(res) => Ok(Some(res)),
Err(Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
Err(Error::Sql(rusqlite::Error::InvalidColumnType(
_,
_,
rusqlite::types::Type::Null,
))) => None,
))) => Ok(None),
Err(err) => Err(err),
}
}
/// Not resultified version of `query_get_value_result`. Returns
/// `None` on error.
pub fn query_get_value<P, T>(&self, context: &Context, query: &str, params: P) -> Option<T>
where
P: IntoIterator,
P::Item: rusqlite::ToSql,
T: rusqlite::types::FromSql,
{
match self.query_get_value_result(query, params) {
Ok(res) => res,
Err(err) => {
error!(context, "sql: Failed query_row: {}", err);
None
@@ -868,6 +885,14 @@ fn open(
update_icons = true;
sql.set_raw_config_int(context, "dbversion", 61)?;
}
if dbversion < 62 {
info!(context, "[migration] v62");
sql.execute(
"ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;",
NO_PARAMS,
)?;
sql.set_raw_config_int(context, "dbversion", 62)?;
}
// (2) updates that require high-level objects
// (the structure is complete now and all objects are usable)

View File

@@ -547,8 +547,8 @@ mod tests {
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.len(), 2);
chat::delete(&t.ctx, chats.get_chat_id(0)).ok();
chat::delete(&t.ctx, chats.get_chat_id(1)).ok();
chats.get_chat_id(0).delete(&t.ctx).ok();
chats.get_chat_id(1).delete(&t.ctx).ok();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
assert_eq!(chats.len(), 0);

View File

@@ -5,16 +5,16 @@
use tempfile::{tempdir, TempDir};
use crate::config::Config;
use crate::constants::KeyType;
use crate::context::{Context, ContextCallback};
use crate::dc_tools::EmailAddress;
use crate::events::Event;
use crate::key;
use crate::key::{self, DcKey};
/// A Context and temporary directory.
///
/// The temporary directory can be used to store the SQLite database,
/// see e.g. [test_context] which does this.
pub struct TestContext {
pub(crate) struct TestContext {
pub ctx: Context,
pub dir: TempDir,
}
@@ -25,7 +25,7 @@ pub struct TestContext {
/// "db.sqlite" in the [TestContext.dir] directory.
///
/// [Context]: crate::context::Context
pub fn test_context(callback: Option<Box<ContextCallback>>) -> TestContext {
pub(crate) fn test_context(callback: Option<Box<ContextCallback>>) -> TestContext {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let cb: Box<ContextCallback> = match callback {
@@ -41,11 +41,11 @@ pub fn test_context(callback: Option<Box<ContextCallback>>) -> 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 fn dummy_context() -> TestContext {
pub(crate) fn dummy_context() -> TestContext {
test_context(None)
}
pub fn logging_cb(_ctx: &Context, evt: Event) {
pub(crate) fn logging_cb(_ctx: &Context, evt: Event) {
match evt {
Event::Info(msg) => println!("I: {}", msg),
Event::Warning(msg) => println!("W: {}", msg),
@@ -54,27 +54,50 @@ pub fn logging_cb(_ctx: &Context, evt: Event) {
}
}
/// Load a pre-generated keypair for alice@example.com from disk.
///
/// This saves CPU cycles by avoiding having to generate a key.
///
/// The keypair was created using the crate::key::tests::gen_key test.
pub(crate) fn alice_keypair() -> key::KeyPair {
let addr = EmailAddress::new("alice@example.com").unwrap();
let public =
key::SignedPublicKey::from_base64(include_str!("../test-data/key/alice-public.asc"))
.unwrap();
let secret =
key::SignedSecretKey::from_base64(include_str!("../test-data/key/alice-secret.asc"))
.unwrap();
key::KeyPair {
addr,
public,
secret,
}
}
/// Creates Alice with a pre-generated keypair.
///
/// Returns the address of the keypair created (alice@example.org).
pub fn configure_alice_keypair(ctx: &Context) -> String {
let addr = String::from("alice@example.org");
ctx.set_config(Config::ConfiguredAddr, Some(&addr)).unwrap();
// The keypair was created using:
// let (public, private) = crate::pgp::dc_pgp_create_keypair("alice@example.com")
// .unwrap();
// println!("{}", public.to_base64(64));
// println!("{}", private.to_base64(64));
let public =
key::Key::from_base64(include_str!("../test-data/key/public.asc"), KeyType::Public)
.unwrap();
let private = key::Key::from_base64(
include_str!("../test-data/key/private.asc"),
KeyType::Private,
)
.unwrap();
let saved = key::dc_key_save_self_keypair(&ctx, &public, &private, &addr, true, &ctx.sql);
assert_eq!(saved, true, "Failed to save Alice's key");
addr
/// Returns the address of the keypair created (alice@example.com).
pub(crate) fn configure_alice_keypair(ctx: &Context) -> String {
let keypair = alice_keypair();
ctx.set_config(Config::ConfiguredAddr, Some(&keypair.addr.to_string()))
.unwrap();
key::store_self_keypair(&ctx, &keypair, key::KeyPairUse::Default)
.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.
pub(crate) fn bob_keypair() -> key::KeyPair {
let addr = EmailAddress::new("bob@example.net").unwrap();
let public =
key::SignedPublicKey::from_base64(include_str!("../test-data/key/bob-public.asc")).unwrap();
let secret =
key::SignedSecretKey::from_base64(include_str!("../test-data/key/bob-secret.asc")).unwrap();
key::KeyPair {
addr,
public,
secret,
}
}

View File

@@ -0,0 +1 @@
xsBNBF425W8BCADLIbltPzG1vk/V2ov2+eBeJJJnRu1kJHdo6e3oNB+HTIxVde5+7Uq8tTEDZB1O7m9NBUFrXr7UYQsA/86G2jmsyWKTzIu1O/t5kdcNDqsNcTVZAhBu2ixYsYVc3ws6kJONjpXLtD2u3P7vEXU3INiOb2JrBQDT8/ubEm1xas/UirYnP5DMaH068IHRdVEYs9ULFaD5scw1m/94buXYZ1CRt/2hT8iRrtBi6ki8kArnhsZC2Xr0+jRQNMUnG5k7Bwi6saCqVmd7IlqSM6MbfYank30Gi/UyDmyIrOk7daTg6WIqgiVOTHav65EK/aUvvjlr+awM+C+u35rQytzyTitZABEBAAHNEzxhbGljZUBleGFtcGxlLmNvbT7CwIkEEAEIADMCGQEFAl425ZQCGwMECwkIBwYVCAkKCwIDFgIBFiEEsBJRVVptIGB7DRLzYuJiDHjRb8EACgkQYuJiDHjRb8HiZQf+PLDxzWchkHAdQFbxxtoXj66aiknofjlRWHDWvUG4nULZ15tjDjnv3z22Meldr8kSV4r1+ejhLFHou9gTzAYk7eAxiybDd8AJOdK+ZgK/Nn7xjdO+HTZLhNdi+R7EektDyf8WDNktEaS8pZc74VKu4984ESi4PoqVxqGHRiSisH4cw4b2pQYxp32BkIdil7sWnqRUEoCpMoKdw2h0N7/lm+rS7/JR9cdjXaVzy1dYTqAVsTL1FTGy4osOKGOyQbkP+Cm6uNq7kC/Bt+fefsb+c2JycmI1uwdvnG7PoFslKv3lRnfkNSmrcIYlJHUl5z0yAgliophr5fqMfzQpO4zMc87ATQReNuVvAQgAuNjE1i+g4v25UNDPIMgXODU4WztE30074gQs5sZa0DQnDUMsdWc2g1o060YZDojMYJQAtBjlW1Dz8FEE7WsLNohGtRyUWmIgNxE5CpodjpwIZ0MdO4Aji0YM+g+WsOSS8kiHMs+dMFfQJuNKjujGFaMIciSaMMrUmPtzkQ/o8NEJs2Aftw90fpVR+M7Mue3++rcEX09ntbgqkgm8SV6OIrOY2kfILudtybocgYkCTeNVqz5VFXuxrnT4ceyFQ64JkwsZxb+X/pCm4V5Q2TbKRwtdonU8HfAz0nAd5tsNeGmf/dPLOKBCxlNEme399YmzWrT+kJBp7CIH5jlWQKyuLwARAQABwsB2BBgBCAAgBQJeNuWUAhsMFiEEsBJRVVptIGB7DRLzYuJiDHjRb8EACgkQYuJiDHjRb8HrEgf/Xu8eRPPdskwtyd98y64teidBpkHuIjuZKJpNyy2HhdGXQwYbNIzwINg0EJ+u2nkreNF/h2Lu+/saqI8Dai02dpYXjvxJIlCgP2os7sNhVaZSaS4XmmJjkHCfZuIKblZypKDJVc5AceZxrtvUbgG+94+H3zeRWVAA30S5ep6YPvxigvhmQah/sdzY7708/jd9uXcCbkP47PBaXCpuPiYLb3t7z8mOteJb7LOZUmSI1efiLDLTGj7ofkdDfA7E6/nF/1+nq+UIDWqljwiUzeNIJsFlZRa/9/uDEjcQbaDe9/knBs7k9pEDZX5u8SSwSED75L+OvRpFWenp4SSKvd2BUw==

View File

@@ -0,0 +1 @@
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

View File

@@ -0,0 +1 @@
xsBNBF4wx1cBCADOwLS/xCd8iKDWUsyTfVzWby+ZGKPpamPTvdj0GFgnf0B1EBaA5//PjAzbK5iKio6QNEmZagzJPkXPByJcAIRUm0T16tqDtCvxm+H93YEXpHi/XWOeJw9kohATSqUtsRO0pFJeDvPiMTmQrEmHYoWDSQBfCrowZdvnMAlbJ9JjYOngcMeTxc0jxmPs5s17yFC+1OWu4fwWCyUM3wy1JzdKTcDWryrSkvmgFdUqJ7pJDk1HFTt+x9tvQlK3un9BXiRwv0u0zDSuI8eDH/dRLA4UL9Pq6vmJmBame1BPsE1PA7VzeTSJR2ooJXMT6o2AmH8PPUfRkv3OiWuh7LM5FSpHABEBAAHNETxib2JAZXhhbXBsZS5uZXQ+wsCJBBABCAAzAhkBBQJeMMduAhsDBAsJCAcGFQgJCgsCAxYCARYhBMzLWqn24RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSH9oIIALbpmicuVghM3CloiCgJhPEFLFMaQZRDV/KCVVtBcHAhw6d42q8T50mhs+W3Va5E37DN+wcenj8CgeGPQY3kPp2cnZruYtLhLkZ1+VEay5BQFUMb8kY21XrNTQQET8vc0L8cCLQ7RCgm1tGiFVp1nqbjmGUdoru90ksoufWfoqVPjNrW+9eHFvY/Z7PqchCdMnbKOJiwwv4E3NDTySZ1UVZnDztGy95Aa8OZ3cntvbq4JVi7S+N38rRPPPzpZKx+M4DUGfDAoaq7O/Xemyk1sP6C/NgQvS8rri54PgkMgKSS4TyyEzdM+fzeNYFPXFGTbgj4p0pSueQV7/JUfYHRfe3OwE0EXjDHVwEIAKIHgS2yI2niSCN1tqcbLvkhLrEJCVcpGxmA7asl1flwWYrGOBhNJE2sCuZqkofqw6qrgsQ4GFgUU5xmcBCqIZ49jRu+aY38lT4WDFHSbe/mGtaIhb2ZYK6zo9W7Y3r6ud8hbUKJTDfl9qEvJpX/Y0syMjwng8SZNTdYMWgAE4NwcgMgdU3dMA3RT6ePJ4vKs38hmXmInLyZce+GJzmo2tpZyP8viPS7JpqojoCPB3G5h9aHeakp1Y4XKQaExANeWCyBJEhNwtNEOVEpQ0txFYPyDrtxV5y5e79IUP418r/PHsnH6UnxXGzB6LfVbSeEyDyKEl+w0PrlNklySomTZFUAEQEAAcLAdgQYAQgAIAUCXjDHbgIbDBYhBMzLWqn24RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSHVCIIAIH694HkQLXRAJlXmi8K/xMVP96ywJovL/B5l4S/vk/iR4P1lYsF55A3Z2PK/iFtwAgVsppcBIPBlqSI0GPDMvEIxj7UFOQfQzVpDes29wG8grHJEJqI/4TlRjOacxTGaJ5fIMsLXJD6nLBuoN5Z6zm3LjqIyOx4ZGrwradPO95OMGT2Xll3YNzUqSWe33RJLqNQ5ea9I7+qvKnW5Z9Yt5nQwOo8yD+f5fql8904B3eAyLqxgkdLmngAWmYhc7KOaKdAsx7TXBAKsoeHk51OPk59u7EbX35HWD6snl/phJdUYDXiddyYN/n2ZY9g80ycle2JfgpfrQGlh7oJqgCjZuk=

View File

@@ -0,0 +1 @@
xcLYBF4wx1cBCADOwLS/xCd8iKDWUsyTfVzWby+ZGKPpamPTvdj0GFgnf0B1EBaA5//PjAzbK5iKio6QNEmZagzJPkXPByJcAIRUm0T16tqDtCvxm+H93YEXpHi/XWOeJw9kohATSqUtsRO0pFJeDvPiMTmQrEmHYoWDSQBfCrowZdvnMAlbJ9JjYOngcMeTxc0jxmPs5s17yFC+1OWu4fwWCyUM3wy1JzdKTcDWryrSkvmgFdUqJ7pJDk1HFTt+x9tvQlK3un9BXiRwv0u0zDSuI8eDH/dRLA4UL9Pq6vmJmBame1BPsE1PA7VzeTSJR2ooJXMT6o2AmH8PPUfRkv3OiWuh7LM5FSpHABEBAAEACACcqjFMTlqNZwpY3QzfhdLfOgkbPSyXJmLWg7jt3bSO2UICclpa+3E/16O2P+aqtCsq4jQS5+UgaOuE4KcMh+e+JJmwrnE98zyJK9GnCD1VqO9GMoHVyUtEufjsZVecs912uD0hwLrU3u/7zFE7IVCCFsMNQZesLMLg/+lXBWnKmrty5XwRMxoxM0yweiJ2b2wdfntQbur7pSdWrwrdBdU1Vprj2VZ/fG6ASMXQ34QTrkq9dYzRGq4T5r6etC5wi8BpAfQX/eD1ktLc/535JmfRwEXFkbmRKwYbMxVk6hrfE+N9xlg+xmUUIlJv29qrB4Q2UjO9FPG9XetrCfExQQShBADVOrBYlkzDFprBC+m1d+RABPo7D5oCiBtrhX+v1UE8By78DCP8jm2VLqmW0hKfEyQDYfiGcnCmjvDciVzZjaB1+K8ov/1YPZ0I29PlolFROyl49H/uEtLn5UwDExD49koQGbqad16M4lDM+MG9pzsfYOV5luXn1fTblvQHhVDhbQQA+DlzEmQ6RkLLw1ta5pCigCHeqaNU8qy4wadst3rAqwM4FtONtHVUr1T9xSvTGJiSGqfD93kNk3Kn2OyC0dNcboBMhwlrCKGqWoXMEGaO2ayL6jjrwri17WsW19NGubrniIPSKPZY75yahx+PckJ5sHDh3jFkvE1p5ThULpp+3gMEAK3buTiGWap0cKcAQYYCNBtt1mcDhgYnXWSaKDDf+e+yTX+Ts3YdrofhwRmBsTbWLYeE4oLO+0vRhGk5b/DcfoleiiwT61LubJmmtlg6EpPp/aWWq7c1+unzulrYjhfLhaoMBrGkudEw7q+pd/Xd3I0UKrPWHfzGGnmiGYUq1K2sTQPNETxib2JAZXhhbXBsZS5uZXQ+wsCJBBABCAAzAhkBBQJeMMdtAhsDBAsJCAcGFQgJCgsCAxYCARYhBMzLWqn24RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSH8ggH+QHLm+gAwetZs7C8NVcdMXLeKCQGLCn4QOeC0HNcPr94rUROlYSxhWGaZWfLiNwte01Lufj4d/Blz5gn+VHKx6lRGw69vxogZI++ikOgdbZIRYAdhmEun0SXtTm6ha9GvuH9ux8UNlP6IQuR6za2uFEeg33TUCgCh2uuQsYkheeOQ2vDjBZvhE2JVEn53uamAkjDDeDw2d71HNXmYGzJfp6MUhAEV8M/aZMcMgeW5sbzp5c6Xs0I1OQATBPI/wheZS3n5Ar/qWCF2HMvoL7Oy3eBMYgOBBXp9z4UKqFACS5XcKjhZO9mZ0Lt4UTqTprv+L4zvGFeuDmPKlKF2PV6AiTHwtgEXjDHVwEIAKIHgS2yI2niSCN1tqcbLvkhLrEJCVcpGxmA7asl1flwWYrGOBhNJE2sCuZqkofqw6qrgsQ4GFgUU5xmcBCqIZ49jRu+aY38lT4WDFHSbe/mGtaIhb2ZYK6zo9W7Y3r6ud8hbUKJTDfl9qEvJpX/Y0syMjwng8SZNTdYMWgAE4NwcgMgdU3dMA3RT6ePJ4vKs38hmXmInLyZce+GJzmo2tpZyP8viPS7JpqojoCPB3G5h9aHeakp1Y4XKQaExANeWCyBJEhNwtNEOVEpQ0txFYPyDrtxV5y5e79IUP418r/PHsnH6UnxXGzB6LfVbSeEyDyKEl+w0PrlNklySomTZFUAEQEAAQAH/jFUmZbBApktJItvPlH4K7/7w0xxFN/tiuuj3jhaR6Au/YQLv25epivjslnenog1CKeAmkqFTZwbbC1U3s+kDKIx2TFWMqrg+MszSULsDz6Xzxn77MQB23a1CK984tfBWC+/7JTyWjs2j3UZduT6IU/2k2bPHQYRIyubdUdVpptAd+GcuBq1DERJVuSJxcAzHgUydJpj5Ao4v+oznZmKgtzTJiuhz1o+1TEUCojw3etE0RCHZ66yhFaRp4crq89BnltMIDHSf2cEYVhiPblYcBx84c6msMczKU4pJzFnvX+I4h6JEhYdxshVv5JnaLKC3Zrum9IjMCWPZ1Act0hD1m0EAMrLfD8wXEIHsbz2MPAif7Au/g9pGhOYW56DF9ABuP9uttLqhe+7YYsagpJbzOzQRFrdL15LwqKRikjnZoRTmV5JGFeCKUkTXy/5Arpc9MBizwVgA8DnvghJWMNYuCtcSkY5O2WJIGd/HnP/iuv8TTK9FcuZo2hEP0IFphwiTwSfBADMigqzx/aLSqd7Aox3Jt9+UxIhoPUEDY9876ann4Aggapw+IAk/ra/q3+bxsxBJSMO9bhUj3NzldHCYi3GBOrreyvJRaO/b7WCTBUpVuU2kahJZf5lqKCojDdBqLz7PqPi86I6Zpv06fzosI4AsE9UwnALsa2QbQ9utYyg4xbeiwQApV9wOUrAV2SUINxEp0I92aT7DvrQtDwUw8DKJXiX90e7DUjPjDjPdUc/WgRMWmVAvxmeN2UOd3nN1EELeOYVsvITipqO65U8B1GjLZHx2gAYvCY96TAEV7qm57uedh5ciwStFGdSrGxY4rVQ9lFgRGUFsRFwp8E0f7LRDMberMA33sLAdgQYAQgAIAUCXjDHbQIbDBYhBMzLWqn24RQclDFl8dsYsYy89wSHAAoJENsYsYy89wSHrKcH/0icTs9x4JKHU6+TvCjBxzgZIP0eRsm71F/tnDi3oLrIDaOu/9y+c1qbWdoJfEqIWjXSJzRCpChvn2eGU8Vv4V6G/Fpv7XsOCzsWwyFbbJ9MoyTfGBDcywOhStHA47pqBgFCeAu/fBefriBP8iue64ZMc48kYA2mHyoUCfqgMgD65XooePgPiQ1R1TVHskoY3uVAYe0JBElkegjGc6+OBeOWo/cnP0LlkDlmooaUTgA36ept53sjLu5YBt5bsi21owfH6RTm8+azAcxQZB53qERP8oS/7V2dJEt0CBG7+dyHqURLIcEginVboKszq4J8mfOF7wy7sMVvFBX3zWfjxDU=

View File

@@ -0,0 +1 @@
xsBNBF48nc4BCADSWr9fE2K1FcE7eSW/Z0MOuzdozKmQJsrmkb7Abssd1yARZuf3+YYh5WqeKJrSTUFD+mJNUhqtodBqBxFH+JzITMG5qGcAdBwXaeJYFMquezLAuVZsZ2b+Njk9Fw3XF4cUZB58ItO/ViFgDi4r5lqsdiiMvGrLmQD2m2BOB8U52ibFhnSVvvYi6rlsZ1HfqB+efD8InPKfiMKDqu909fgVchGJ7OwtTKanaF3v+rzQvXsnVal1yfc0YsmOM0ekWDhIR5EoSg8pJlBHVc/yBrxQWq9h2e2PUntLK29/qp/k/xsQHN3BAj/kuQQPzMARcUUvxo9Aq8n4CMzoFmrK2G+bABEBAAHNFTxjaGFybGllQGV4YW1wbGUubmV0PsLAiQQQAQgAMwIZAQUCXjyd5AIbAwQLCQgHBhUICQoLAgMWAgEWIQRmPL1krPNFp4Tiz1fFiWgx7MdV7wAKCRDFiWgx7MdV7/tMCACSeaPalMt/EwsCGxnWNNEWPX7fMSiAZx1bbPYULIroEBVgmObOkDqoB6y9tWQB/HUq44ZIYdhdw7wlzZp9d81dtxGA+QcRkTy8fr/P2KmQhwjV9m22xBCGABPSpFG+/iONEJADEA8Oe3SogI/iPksepLs9Gg9Ix9Qb1ZMt6+GErE7u0aAamW03NQW7SlgLruAKhjKLjP1wxryK4h0Mdac6CuGZQH/G3yZX3OQPkE2BGjEefhzj0yEKOGhYM716uswqecjB8HoYh5RaDcE+Shcze/OhSQesqj0RxoCpeN3/6QGcC6DT70Qrqeri0RtA495SS3YTqb2p9U2WISOEJIdXzsBNBF48nc4BCADKwwUPt5jp4yIdVdVdLyXGuJS/pC6t5Q64QlcEzHH03eVJHYH42shyWPiTE7HU3giqLhnPXjYN8/piONBQ7dQqTkYJtrx0aUBAoQ9p0p6eMGoY4pMxrclwxnvGE+2YplFNzXkcqxkSPy4n5kOPeq55tVHkWG+gTblZdlT9sKM8Ksr8gF728eaWgt+TQCnhuwa9h9BVzLAZquaATm7PvPKamMd7jIVoISXrZKuPBVrSDbx+DElg0HKj8sOxh3lNz2rmRTGWDbURPhqvVyQ2tXryEVfMZRauR9B9YCYEeedRwDrOSME2LaoU483w0j2Vnb4GkOLpg8Wrw+fnIcM32GXxABEBAAHCwHYEGAEIACAFAl48neQCGwwWIQRmPL1krPNFp4Tiz1fFiWgx7MdV7wAKCRDFiWgx7MdV78HXCACyxjrXpr7GKqQOMJGvNKytuVf7gHUbwLq7oAXoM6PixPkfZIArH/EHnt0GaBWq2r08REj2IRQ8t/8zfkWa9L2RxITez3dRkOUjf49i+J9g3oyleJDZVrAhoU2Uzwb+40tTGaErGpD6m3GqIe5wE7gXFBAf0IwQmIjic64ULCE5j2qaWMxwfvKIDuSD/bN+mSqlQbmekeX6bud4rMoGIUnkQthNAQwBQHOjkPZvdjXiDGFxmpDlcJv5e9LYv8kb141JmYIon9iIGWF3L52+SbvYXFAoPi4qovsdgUXezTRoaiR8Mgft4KUZZIeXUhC7PRuut8PsbN2P0WCh1CjIJph9

View File

@@ -0,0 +1 @@
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

View File

@@ -0,0 +1 @@
xsBNBF48no8BCADwN4PxXewE4iP3HHn/FE5r0x8x8byo9mJIIEOrjL1k0JWIcz3KvC5evfZ3M/ZK2QPBLhFYOck0+gCAR/eH1zgDZeYXUB1CWUoCKlZ9jH1UbfzE34ZxxiwCZy2ZdMnKxDF/ezyXsVvoEhGQE1+8A9Xy/ZXlplUbyYVacgdK9fQlh50d8FOU+7eplUts/SzXx52T83tB98oS9QVgJ4qIC+fu0xMgqdH7e0ithSc/owfYkKjH+isbD4n9U8KSqb4zzf2Y2gz7h6jdM+PPuWqTSARbH83j7qG8q4IjC7cO3CgSryk3f/MLkquKD8z9ybvrYKXSF04RMEuhgBxXnE0vA3ZpABEBAAHNETxkb21AZXhhbXBsZS5uZXQ+wsCJBBABCAAzAhkBBQJePJ6rAhsDBAsJCAcGFQgJCgsCAxYCARYhBJaMlJFdjIE/mXE7aD6NVnuTBaaoAAoJED6NVnuTBaaoXGQIAM6KRSMBLKOb0IlXqSCy1Ve9MF02fDNmGFw7xC5v2gKpdDZZJctRLxH3ZFgQmpcSIZ4P/A7XcYnlIxxGxwzyhMmas9eJ1w3sPbvX5vJ4ZYGzooZSFUQXgO6+bm7qQlf7gFcsuTkF5n6cx9tDPcyxy01b7eaexTXoWpYDX110TFbpklprub2QpI2w4gy5y1d/pfOmJYGHgMEALgAEKQCWjQ/jUS6pOVEH+pT1ddlv9vq37CmTp00bKbj+Z59mqeUBjE5+RmNWaLwkdZSR1o+LKNw/Gl07gzayRjJyh0Txj4LgHtVSKvGj6GexhddEgqp8qwMMfNbbqWATh9zHNJ+gjSzOwE0EXjyejwEIAKbISj1O986suBXKzHPlEzqetkGEWNV4OHHfhphIxsoW/f3IOdqv0uqwz9AhzXE6YbsFzrc6ZwP+pYTH51T1ugtG0LBMFikh4tyyYjJV0v9gUq2JqixxHfJ/XfVryKO1OWqs2GtBxaQP5FVf+vQqSPpAz4B/6hqwUvx2XdI6bbGn1fQlTQyfGR6Uclm5kIaY8/VBzuVySaAycfOdBhjoyYhl2eMxdxz4NCb6pwNkl6KbjSfGHVpfR6fbuTihIbZFL1qAfYi6BnvrB9HgnfYItIA9VV2SlLIRfBgxgG9vxKmD83j9r5Dn/e1KC5PoMndMZ5I410H06kjR6dLJbTuy7+MAEQEAAcLAdgQYAQgAIAUCXjyeqwIbDBYhBJaMlJFdjIE/mXE7aD6NVnuTBaaoAAoJED6NVnuTBaaon5UIAMJLU7EKjCsJ4ldIYYRYE7wzVO2DpiFY5yEVu/Jq2O2C+go7b2oBgUYYB3ExwMUwPnU8H6j+jbzukxOltKeRZ1QU1d9iwzYVHsP3kw02DceoyMaab0j1DLHPSVPSmsP5/U0eFK++vFvsY8KuwRUPLK+m7+qFET2gidyRSJGqyiJlEz+wyR3b44Ff1C3RDdRx8EzPVYx+gXRGLIBqsBNuSLhimiwAekMgnpAdsQu8WTCPQSO2Yrz6I6uFVNAr8FnnFoeclwLHbQ++aWhBTuhUp9oJ/388ySkEuMAZrd4YU/RwthM4zsW/VAf936FBkvi8FBJQ0Vdps1xksQeXK7hQrVw=

View File

@@ -0,0 +1 @@
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

View File

@@ -0,0 +1 @@
xsBNBF48nxEBCADRa0HRyoj7KdbchkVycHq5jYxYN4NJsRf1bojhuifuLYlJQu7I7Rp862sbvSPN9tM/3dQF6fTyUllFkhoS50fUaf/bJWwi5XWVsLa1+DdOW4As2zkJslu6Hp/hUFM2FXthRYIwg3c2jparkNtsLyQvtDsG62Acg7dr+NwZ5mxepyJ5WkXciOPLp9egcrTVoBbcnn4gzj2Wx6MkUByY+bENtUrqsZerWOt+F75DIWkHwzo5VwfpvH6RrIJabu8KLMLoUTZoovQcDKfk0yv8bdBLEQf84SLY+0BCZ6aZHrqnwEmDBeFGfTcIMFsBxUZfvsMYlAiG6khbZzhQxJ44+YaxABEBAAHNEzxlbGVuYUBleGFtcGxlLm5ldD7CwIkEEAEIADMCGQEFAl48ny0CGwMECwkIBwYVCAkKCwIDFgIBFiEEuGWGtt70N9Z0v6/AKmsuvGM7noIACgkQKmsuvGM7noIpsAgAiP+E48xEhCvEHVIMiQigQvC3kucg2TRk0yrp0ydDeS19l8jXkN8lA9byXVq5VDg8Bs4tN9WR/Gy8x9Xmd6/VyFiquBqGTObO/1u1kegHR0+sp5FxxffZhGoCRBIW4iXww1BzcGUd+TeRlInl5adGpA6vQAGifLsKQQU9/I0bSRBAMGbo0PKZ+EllTLqDeo4D4ELNO6CKyh65FExUIZTtbWZTyLn95ekypUIhgEAKgEs/qE74nJBqcrzkXn3xmDJetYxw2xQzwS+rIwR12ONub01nm/74Z30zbMViwQF4yTV7VB9uklY6WayCBpT3BkFQiYRPt7lnipBI2PplYXvYIs7ATQRePJ8RAQgAwvvNWB4eABzpUylyhI7q8WNK8GHCGLfqprSLFiAv14psluRW9MezLEFM1N8Az5yqzs3hsuEMiIBPiLrkW8ZkCledlYENorM/6G5+xK9TI4iWnP1LP+qESGGNSF7pXciZE7/XrE/CPL06nXuJRd1qOHvIUnaCQRiqLFPkUG3KhtC+/Ayvz6l2RvSqoTTayxYckgigkEneS/RXMSYnG/A5RJB0fOACp39HzHN/XU7JFLz8WlXgLfWJi7v9uVft9QBCtVseWF+ElKJ5NkVNRrV/SQwFYB91Y+LHqKz6CTQq2Di0vGHjY9ecai16D/CGUqkJg9t5jnLxZvQeR70o13JqlQARAQABwsB2BBgBCAAgBQJePJ8uAhsMFiEEuGWGtt70N9Z0v6/AKmsuvGM7noIACgkQKmsuvGM7noLj2gf9EuLnhKUv0rWW2HShJlKF5oqKmx6tSoNMOd5hbJzL1Z9d7ZjFQJS25jAzl9V6858heAm3HYTlJfg2RkzHte12k0hr9Svbpadf/OYB2UVsAwGRFGbdq/3H/ZaP5EbCdv8Egcx6pjdgvXb5n6gaMj7LJG4/YvokGLOx3VgNNRdqB0gtKWwN0tzdM/6YfDJTEVDbMUR3aFqYLTSB8KCjNtWcnWO2a17P1Qf6fDhcxpyELkLb1T4sPD4Tz9bFPTJzL87uS4+Ba+Xq2YE7aWa6e+C2HMgH6OghDLqoGpeNy01N3XSikyoFNdEmPYY/RdmuJNSuuyptQHgzh1vIOV7AOoQ+Rw==

View File

@@ -0,0 +1 @@
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

View File

@@ -0,0 +1 @@
xsBNBF48n2MBCAC741/YKzU76pWNijBCYC+hGSKZvBdanm4eJzi/eKPevUypdW2GJzFFJxWjx2wIcV8cLwQvY2Z+ieJaPPwGbUr0nH2S9lmghkCCKjGxWcmrx3sQr7x9431KYOM6+/SAZNjHYjWwcwy2QhE6J7qfC74LjTF4pv+ZRgSZDC+O70NpTWdNdwi/0Kn9gdj5diSwJmnBxQBGp/tnZuu3XGZrgKXlGlPspRMF3Ug2WmvyBhxF8KKoosiaXlumc5gFFuFRnoAVfp/6yehO44l9bYz0zwYWahxmLhShQVhfcH3YBg0l7hMkfC2Fp8fDF7Hr1PUOJcBY50VDN0zlV9Bi1iiPofAtABEBAAHNEzxmaW9uYUBleGFtcGxlLm5ldD7CwIkEEAEIADMCGQEFAl48n3wCGwMECwkIBwYVCAkKCwIDFgIBFiEEyLpQv0rBL6841/ZX3fyOnzx5kZUACgkQ3fyOnzx5kZUGWQgApCPQYmfa76xcscYBWg3DvMAm0Exk/LvbN74MIqADHIgaNFUHoThTPDVAPeh5ogra/kg4QaLctivC81S2VOPIc/4LGEFJekvythXUgSLNjhlR63Va5ObMV4UegUH9c0O8MGndkQFxiwy98D1bzNyl3mCVOUECVZiJJbxUZwBKj4iowa7kgX/FrdjZIJrz50NMExcDuxvc+MGyDr4YQHrZTbCjcrJkxufklwsWme0qneuPTxwW2+TnDjHkDaYDdwEnoKRkwlXEy3Lu9ZhswlaWTgdtClpf2geb7tCAPQ0cd2V5m6NnKMzXrnFuJXB6Xkvp32Tyuq4ebILsbCfpLV/HzM7ATQRePJ9jAQgAzQGckmcFjCViSkwGLROhi2Gvb85ABXbkKPMg1x27LraP9YWNJoq2GOy2vv8r8m2q+FpCR25a0NdWlbyiQpPuEWh0udJnNUm+6j5j6PSAmlRRDMhDH4QwzJC+B+lM6NxvSRhxBBtwFAvMy0qeNhv1UCBaeQUHoFCODqJRYOywj7ZGXdQyttIAlC5PtVw1cV+J8/TGXNrE9DNgFGAm1BPgw/lE1OjVbF8l6NbDdi9YJoOm9mpffUPlUZMZh3+a5+J6F3KjYwNyi4wi3Y3Pt4avXEo+ib15XNhJcwMslc49La2T8PMnpf3nLClxynJjYDiMuzujcbBWbfVF1383P0KikQARAQABwsB2BBgBCAAgBQJePJ98AhsMFiEEyLpQv0rBL6841/ZX3fyOnzx5kZUACgkQ3fyOnzx5kZX1eAf9GnrA9nCds3OmtCUpRmVEDar29DfS79B4vBfoYXYb5kRIOxJV6yjfGYRh2IJ0CTfNYkp4AuRC/jEHPXlVUD92Vcb0wSfwO32mMw75FRzIS7/IcwWAauWOtpao3J/tsxHWkSxfh5Zo6vzODQY4k8eHnitxpXT0xSIjnYmVRMuqfb3NELk3PkF52+Fte4zKfBCP80HqO3BdVCBlQNpZiOMW+yFO/8VqLmB9442nGGhuSfCeBystI3Er0SYSDqNbg5uetBaP1MOomIZuuxhv3T5jD9DeEDHGOqDjPZ8dMdQYCnYmiVaMb8ECsKG5eMvS8Imss1ho2iz4sjYdVzODl8T0zQ==

View File

@@ -0,0 +1 @@
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

View File

@@ -1 +0,0 @@
xcLYBF086ewBCACmJKuoxIO6T87yi4Q3MyNpMch3Y8KrtHDQyUszU36eqM3Pmd1lFrbcCd8ZWo2pq6OJSwsM/jjRGn1zo2YOaQeJRRrC+KrKGqSUtRSYQBPrPjE2YjSXAMbu8jBI6VVUhHeghriBkK79PY9O/oRhIJC0p14IJe6CQ6OI2fTmTUHF9i/nJ3G4Wb3/K1bU3yVfyPZjTZQPYPvvh03vxHULKurtYkX5DTEMSWsF4qzLMps+l87VuLV9iQnbN7YMRLHHx2KkX5Ivi7JCefhCa54M0K3bDCCxuVWAM5wjQwNZjzR+WL+dYchwoFvuF8NrlzjM9dSv+2rn+J7f99ijSXarzPC7ABEBAAEACAChqzVOuErmVRqvcYtqm1xt1H+ZjX20z5Sn1fhTLYAcq236AWMqJvwxCXoKlc8bt2UfB+Ls9cQb1YcVq353r0QiExiDeK3YlCxqd/peXJwFYTNKFC3QcnUhtpG9oS/jWjN+BRotGbjtu6Vj3M68JJAq+mHJ0/9OyrqrREvGfo7uLZt7iMGemDlrDakvrbIyZrPLgay+nZ3dEFKeOQ6FFrU05jyUVdoHBy0Tqx/6VpFUX9+IHcMHL2lTJB0nynBj+XZ/G4aX3WYoo3YlixHbIu35fGFA0TChoGaGPzqcI/kg2Z+b/BryG9NM3LA2cO8iGrGXAE1nPFp91jmCrQ3VWushBADERP+uojjjfdO5J+RkmcFe9mFYDdtkhN+kV+LdePjiNNtcXMBhasstio0Sut0GKnE7DFRhX7mkN9w2apJ2ooeFeVVWot18eSdp6Rzh6/1Z7TmhYFJ3oUxxLbnQsWIXIec1SzqWBFJUCn3IP0mCnJktFg/uGW6yLs01r5ds52uSBQQA2LSWiTwk9tEmdr9mz3tHnmrkyGiyKhKGM1Z7Rch63D5yQc1s4kUMBlyuLL2QtM/e4dtaz2JAkO8kQrYCnNgJ+2roTAK3kDZgYtymjdvK3HpQNtjVo7dds5RJVb6U618phZwU5WNFAEJWyyImmycGfjLv+18cW/3mq0QVZejkM78D/2kHaIeJAowtBOFY2zDrKyDRoBHaUSgj5BjGoviRC5rYihWDEyYDQ6mBJQstAD0Ty3MYzyUxl6ruB/BMWnMDFq5+TqtdBzu3jCtZ8OEyH8A5Kdo68Wzo/PGxzMtusOdNj9+3PBmSq4yibJxbLSrn59aVUYpGLjeGKyvm9OTKkrOGN27NEzxhbGljZUBleGFtcGxlLmNvbT7CwIkEEAEIADMCGQEFAl086fgCGwMECwkIBwYVCAkKCwIDFgIBFiEE+iaix4d0doj87Q0ek6DcNkbrcegACgkQk6DcNkbrcei3ogf/cruUmQ+th52TFHTHdkw9OHUl3MrXtZ7QmHyOAFvbXE/6n5Eeh+eZoF8MWWV72m14Wbs+vTcNQkFVTdOPptkKA8e4cJqwDOHsyAnvQXZ7WNje9+BMzcoipIUawHP4ORFaPDsKLZQ0b4wBbKn8ziea6zjGF0/qljTdoxTtsYpv5wXYuhwbYklrLOqgSa5M7LXUe7E3g9mbg+9iX1GuB8m6GkquJN814Y+xny4xhZzGOfue6SeP12jJMNSjSP7416dRq7794VGnkkW9V/7oFEUKu5wO9FFbgDySOSlEjByGejSGuBmho0iJSjcPjZ7EY/j3M3orq4dpza5C82OeSvxDFcfC2ARdPOnsAQgA5oLxXRLnyugzOmNCy7dxV3JrDZscA6JNlJlDWIShT0YSs+zG9JzDeQql+sYXgUSxOoIayItuXtnFn7tstwGoOnYvadm/e5/7V5fKAQRtCtdN51av62n18Venlm0yNKpROPcZ6M/sc4m6uU6YRZ/a1idal8VGY0wLKlghjIBuIiBoVQ/RnoW+/fhmwIg08dQ5m8hQe3GEOZEeLrTWL/9awPlXK7Y+DmJOoR4qbHWEcRfbzS6q4zW8vk2ztB8ngwbnqYy8zrN1DCICN1gYdlU++uVw6Bb1XfY8Cdldh1VLKpF35mAmjxLZfVDcoObFH3Cv2GB7BEYxv86KC2Y6T74Q/wARAQABAAgAhSvFEYZoj1sSrXrHDjZOrryViGjCCH9t3pmkxLDrGIddKsFyN8ORUo6KUZS745yx3yFnI9EZ1IZvm9aF+jxk2lGJFtgLvfoxFOvGckwCSy8T/MCiJZkz01hWo5s2VCLJheWL/GqTKjS5wXDcm+y8Wtilh+UawycdlDsSNr/D4MZLj3Chq9K03l5UIR8DcC7SavNi55R2oGOfboXsdvwOlrNZdCkZOlXDI4ZKFwbDHCtpDo5FS30hnJi2TecUPZWB1CaGFWnevINd4ikugVjcAoZj/QAIvfrOCgqisF/Ylg9uRMUPBapmcJUueILwd0iQqvGG0aCqtchvSmlg15/lQQQA9G1NNjNAH+NQrXvDJFJe/V1U3F3pz7jCjQa69c0dxSBUeNX1pG8XXD6tSkkd4Ni1mzZGcZXOmVUM6cA9I7RH95RqV+QIfnXVneCRrlCjV8m6OBlkivkESXc3nW5wtCIfw7oKg9w1xuVNUaAlbCt9QVLaxXJiY7ad0f5U9XJ1+w8EAPFs+M/+GZK1wOZYBL1vo7x0gL9ZggmjC4B+viBJ8Q60mqTrphYFsbXHuwKV0g9aIoZMucKyEE0QLR7imttiLEz1nD8bfEScbGy9ZG//wRfyJmCVAjA0pQ6LtB93d70PSVzzJrMHgbLKrDuSd6RChl7n9BIEdVyk7LEph0Yg9UsRBADm6DvpKL+P3lQ0eLTfAgcQTOqLZDYmI3PvqqSkHb1kHChqOXXs8hGOSSwKGjcd4CZeNOGWR42rZyRhVgtkt6iYviIaVAWUfme6K+sLQBCeyMlmEGtykAA+LmPBf4zdyUNADfoxgZF3EKHf6I3nlVn5cdT+o/9vjdY2XAOwcls1RzaFwsB2BBgBCAAgBQJdPOn4AhsMFiEE+iaix4d0doj87Q0ek6DcNkbrcegACgkQk6DcNkbrcegLxwf/dXshJnoWqEnRsf6rVb9/Mc66ti+NVQLfNd275dybh/QIJdK3NmSxdnTPIEJRVspJywJoupwXFNrnHG2Ff6QPvHqI+/oNu986r9d7Z1oQibbLHKt8t6kpOfg/xGxotagJuiCQvR9mMjD1DqsdB37SjDxGupZOOJSXWi6KX60IE+uM+QOBfeOZziQwuFmA5wV6

View File

@@ -1 +0,0 @@
xsBNBF086ewBCACmJKuoxIO6T87yi4Q3MyNpMch3Y8KrtHDQyUszU36eqM3Pmd1lFrbcCd8ZWo2pq6OJSwsM/jjRGn1zo2YOaQeJRRrC+KrKGqSUtRSYQBPrPjE2YjSXAMbu8jBI6VVUhHeghriBkK79PY9O/oRhIJC0p14IJe6CQ6OI2fTmTUHF9i/nJ3G4Wb3/K1bU3yVfyPZjTZQPYPvvh03vxHULKurtYkX5DTEMSWsF4qzLMps+l87VuLV9iQnbN7YMRLHHx2KkX5Ivi7JCefhCa54M0K3bDCCxuVWAM5wjQwNZjzR+WL+dYchwoFvuF8NrlzjM9dSv+2rn+J7f99ijSXarzPC7ABEBAAHNEzxhbGljZUBleGFtcGxlLmNvbT7CwIkEEAEIADMCGQEFAl086fgCGwMECwkIBwYVCAkKCwIDFgIBFiEE+iaix4d0doj87Q0ek6DcNkbrcegACgkQk6DcNkbrcei3ogf/cruUmQ+th52TFHTHdkw9OHUl3MrXtZ7QmHyOAFvbXE/6n5Eeh+eZoF8MWWV72m14Wbs+vTcNQkFVTdOPptkKA8e4cJqwDOHsyAnvQXZ7WNje9+BMzcoipIUawHP4ORFaPDsKLZQ0b4wBbKn8ziea6zjGF0/qljTdoxTtsYpv5wXYuhwbYklrLOqgSa5M7LXUe7E3g9mbg+9iX1GuB8m6GkquJN814Y+xny4xhZzGOfue6SeP12jJMNSjSP7416dRq7794VGnkkW9V/7oFEUKu5wO9FFbgDySOSlEjByGejSGuBmho0iJSjcPjZ7EY/j3M3orq4dpza5C82OeSvxDFc7ATQRdPOnsAQgA5oLxXRLnyugzOmNCy7dxV3JrDZscA6JNlJlDWIShT0YSs+zG9JzDeQql+sYXgUSxOoIayItuXtnFn7tstwGoOnYvadm/e5/7V5fKAQRtCtdN51av62n18Venlm0yNKpROPcZ6M/sc4m6uU6YRZ/a1idal8VGY0wLKlghjIBuIiBoVQ/RnoW+/fhmwIg08dQ5m8hQe3GEOZEeLrTWL/9awPlXK7Y+DmJOoR4qbHWEcRfbzS6q4zW8vk2ztB8ngwbnqYy8zrN1DCICN1gYdlU++uVw6Bb1XfY8Cdldh1VLKpF35mAmjxLZfVDcoObFH3Cv2GB7BEYxv86KC2Y6T74Q/wARAQABwsB2BBgBCAAgBQJdPOn4AhsMFiEE+iaix4d0doj87Q0ek6DcNkbrcegACgkQk6DcNkbrcegLxwf/dXshJnoWqEnRsf6rVb9/Mc66ti+NVQLfNd275dybh/QIJdK3NmSxdnTPIEJRVspJywJoupwXFNrnHG2Ff6QPvHqI+/oNu986r9d7Z1oQibbLHKt8t6kpOfg/xGxotagJuiCQvR9mMjD1DqsdB37SjDxGupZOOJSXWi6KX60IE+uM+QOBfeOZziQwuFmA5wV6RDXIeYJfqrcbeXeR1d0nfNpPHQR1gBiqmxNb6KBbdXD2+EXW60axC7D2b1APmzMlammDliPwsK9U1rc9nuquEBvGDOJf4K+Dzn+mDCqRpP6uAuQ7RKHyim4uyN0wwKObzPqgJCGwjTglkixw+aSTXw==

View File

@@ -1,11 +1,7 @@
//! Stress some functions for testing; if used as a lib, this file is obsolete.
use std::collections::HashSet;
use deltachat::config;
use deltachat::context::*;
use deltachat::keyring::*;
use deltachat::pgp;
use deltachat::Event;
use tempfile::{tempdir, TempDir};
@@ -94,115 +90,6 @@ fn stress_functions(context: &Context) {
// free(qr.cast());
}
#[test]
#[ignore] // is too expensive
fn test_encryption_decryption() {
let (public_key, private_key) = pgp::create_keypair("foo@bar.de").unwrap();
private_key.split_key().unwrap();
let (public_key2, private_key2) = pgp::create_keypair("two@zwo.de").unwrap();
assert_ne!(public_key, public_key2);
let original_text = b"This is a test";
let mut keyring = Keyring::default();
keyring.add_owned(public_key.clone());
keyring.add_ref(&public_key2);
let ctext_signed = pgp::pk_encrypt(original_text, &keyring, Some(&private_key)).unwrap();
assert!(!ctext_signed.is_empty());
assert!(ctext_signed.starts_with("-----BEGIN PGP MESSAGE-----"));
let ctext_unsigned = pgp::pk_encrypt(original_text, &keyring, None).unwrap();
assert!(!ctext_unsigned.is_empty());
assert!(ctext_unsigned.starts_with("-----BEGIN PGP MESSAGE-----"));
let mut keyring = Keyring::default();
keyring.add_owned(private_key);
let mut public_keyring = Keyring::default();
public_keyring.add_ref(&public_key);
let mut public_keyring2 = Keyring::default();
public_keyring2.add_owned(public_key2);
let mut valid_signatures: HashSet<String> = Default::default();
let plain = pgp::pk_decrypt(
ctext_signed.as_bytes(),
&keyring,
&public_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text,);
assert_eq!(valid_signatures.len(), 1);
valid_signatures.clear();
let empty_keyring = Keyring::default();
let plain = pgp::pk_decrypt(
ctext_signed.as_bytes(),
&keyring,
&empty_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text);
assert_eq!(valid_signatures.len(), 0);
valid_signatures.clear();
let plain = pgp::pk_decrypt(
ctext_signed.as_bytes(),
&keyring,
&public_keyring2,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text);
assert_eq!(valid_signatures.len(), 0);
valid_signatures.clear();
public_keyring2.add_ref(&public_key);
let plain = pgp::pk_decrypt(
ctext_signed.as_bytes(),
&keyring,
&public_keyring2,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text);
assert_eq!(valid_signatures.len(), 1);
valid_signatures.clear();
let plain = pgp::pk_decrypt(
ctext_unsigned.as_bytes(),
&keyring,
&public_keyring,
Some(&mut valid_signatures),
)
.unwrap();
assert_eq!(plain, original_text);
valid_signatures.clear();
let mut keyring = Keyring::default();
keyring.add_ref(&private_key2);
let mut public_keyring = Keyring::default();
public_keyring.add_ref(&public_key);
let plain = pgp::pk_decrypt(ctext_signed.as_bytes(), &keyring, &public_keyring, None).unwrap();
assert_eq!(plain, original_text);
}
fn cb(_context: &Context, _event: Event) {}
#[allow(dead_code)]