Compare commits

...

556 Commits

Author SHA1 Message Date
bjoern
783c7ee4c5 Merge pull request #1303 from deltachat/prep-core25
prepare 1.25.0
2020-02-21 10:41:07 +01:00
B. Petersen
a0b2a692d0 bump version to 1.25.0 2020-02-20 23:49:49 +01:00
B. Petersen
a59d368101 changelog 2020-02-20 23:47:43 +01:00
B. Petersen
5c36fb29ed update python version examples 2020-02-20 23:47:23 +01:00
Alexander Krotov
508b8ef2e2 Improve documentation, mostly by hiding behind pub(crate) 2020-02-20 23:37:13 +03:00
holger krekel
e94c62e5b3 Update src/contact.rs
fix typo
2020-02-20 02:10:56 +01:00
B. Petersen
b65a6c2829 target comment of @hpk42 2020-02-20 02:10:56 +01:00
B. Petersen
c4a20d0798 fix updating names from incoming mails
- if a manual name was never given, always update names from incoming mails
- if a manual name is cleared, fall back to names from incoming mails
2020-02-20 02:10:56 +01:00
B. Petersen
9cb7ea524e add failing test where a contact-name is not updated as expected 2020-02-20 02:10:56 +01:00
holger krekel
0ac0eeda34 better naming, less code 2020-02-20 01:30:21 +01:00
holger krekel
4d066b4fd2 refine processing of errors and result handling 2020-02-20 01:30:21 +01:00
Alexander Krotov
840e321dd9 Process Permanent and Transient SMTP errors 2020-02-20 01:30:21 +01:00
holger krekel
4b6963122b Update src/smtp/mod.rs
Co-Authored-By: Alexander Krotov <ilabdsf@gmail.com>
2020-02-20 01:30:21 +01:00
holger krekel
d5d662bc41 fix ordering error 2020-02-20 01:30:21 +01:00
holger krekel
0b0ed56901 directly attempt to re-connect if the smtp connection is maybe stale
also refactor performing the job-action into own function
2020-02-20 01:30:21 +01:00
B. Petersen
3a25d6b275 target comment of @link2xt 2020-02-18 17:51:28 +01:00
B. Petersen
66e2f51233 adapt spec 2020-02-18 17:51:28 +01:00
B. Petersen
8fdb048b6a alter the memberlist more carefully
up to now, Chat-Group-Member-Added and -Removed commands
result in a complete recreation of the memberlist
by collecting all addresses from the From: and To: headers.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

all this works even when both devices are offline.

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

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

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

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

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

the Serde-thing is not obvious to me:
while serde_json::Value.as_str() removes the quotes,
serde_json::Value.to_string() does not.
2020-01-26 12:28:42 +00:00
Alexander Krotov
033a44580c Turn get_[fresh_]msg_cnt() into ChatId members 2020-01-25 22:11:13 +00:00
Alexander Krotov
4734bcfbb4 Update Chat.unarchive() comment 2020-01-25 22:11:13 +00:00
Alexander Krotov
099fe6f477 Rename Chat.set_blocking() into set_blocked() 2020-01-25 22:11:13 +00:00
Alexander Krotov
889327b5f6 Rename Chat.archive() into Chat.set_archived() 2020-01-25 22:11:13 +00:00
Alexander Krotov
a2845f44ab Turn ChatId-related functions into methods 2020-01-25 22:11:13 +00:00
Alexander Krotov
a7477516d1 mimefactory: factor out get_location_kml_part 2020-01-25 17:44:19 +00:00
bjoern
938d5828fc Merge pull request #1212 from deltachat/handshake-ignore
do not delete handshake messages possibly belonging to secure-joins on other devices
2020-01-25 15:41:20 +01:00
Alexander Krotov
bf3eab453c Avoid panic in sanitise_name()
.truncate() is not safe because it panics if string length does not
lie on character boundary. We convert the string to characters and take
first n character instead.
2020-01-25 13:04:56 +01:00
Alexander Krotov
ebab893330 dc_tools: remove unused dc_derive_safe_stem_ext()
It had the same .truncate() bug as BlobObject::sanitise_name()
2020-01-25 13:04:56 +01:00
Alexander Krotov
4c67b3a118 Add failing test_sanitise_name() test 2020-01-25 13:04:56 +01:00
B. Petersen
d74b06f8bf target comment of @flub 2020-01-24 21:22:12 +01:00
B. Petersen
c54e211147 do not delete handshake messages maybe belonging to secure-joins on other devices 2020-01-24 12:50:26 +01:00
B. Petersen
8817cf5116 don't make me think 2020-01-24 12:32:45 +01:00
Floris Bruynooghe
fb568513b2 When handling vc-contact-confirm the message is Done
The HandshaeMessage::Propagate return should only occur for
vg-member-added messages since it is for those that we still need to
handle the group add.
2020-01-24 11:26:27 +01:00
Floris Bruynooghe
b4ebfdb84a Fix out of sync cargo.lock
This should have been updated when the version of deltachat_derive was
changed.  Oops.
2020-01-24 11:25:08 +01:00
Floris Bruynooghe
7040bd804a Change to normal sematic version number for this sub-crate
Having a number that resembles the core version number is confusing.
This doesn't matter at all, it just needs to be a version number.
2020-01-22 23:49:01 +01:00
holger krekel
2f2fc17bd8 another try 2020-01-22 15:12:48 +01:00
holger krekel
d5d4b49aaf try to get on-tag-builds to work 2020-01-22 15:11:19 +01:00
holger krekel
042c4efddf update Python README 2020-01-22 14:00:55 +01:00
holger krekel
01251d162c fix wheel upload target, and change python versioning 2020-01-22 14:00:55 +01:00
Alexander Krotov
64026fde7c Store main part of the message outside of "parts" vector 2020-01-22 04:28:35 +00:00
Alexander Krotov
adcdae4abe Factor get_message_kml_part out of render_message 2020-01-22 04:28:35 +00:00
holger krekel
88d138b925 only upload manylinux1 packages as others are rejected by pypi anyway 2020-01-21 23:28:11 +01:00
holger krekel
2773b89815 prep beta.23 2020-01-21 21:40:32 +01:00
holger krekel
949d93fdaa - actually also upload py docs, not only c docs
- delete unused files on remote
- show a project number in doxygen
2020-01-21 19:14:16 +01:00
Alexander Krotov
90a4303c8e Make it possible to compile with stable Rust 2020-01-21 19:50:42 +03:00
holger krekel
d3b1972505 fix master build 2020-01-21 17:27:21 +01:00
holger krekel
3807d5fbd0 bringing back wheel building for linux, and fixing c.delta.chat 2020-01-21 17:25:01 +01:00
holger krekel
e49e2021e5 i don't think this fuzzy test is very useful -- the mime-parser will end very early on random-input and DC's parsing machinery probably almost never runs. 2020-01-21 16:00:59 +01:00
Alexander Krotov
84a5276ab0 Fix build
0f52f63863 rebase broke the build
2020-01-21 17:17:51 +03:00
Alexander Krotov
1732c3b350 Reduce the scope of clippy exceptions
This prevents creation of new overly complex functions.
2020-01-21 14:49:53 +01:00
Alexander Krotov
0f52f63863 Add "Auto-Submitted: auto-replied" header to MDNs
This header is specified in RFC 3834.
2020-01-21 16:29:18 +03:00
B. Petersen
0043e95ba7 fix message deletion on server
before a message is actually deleted,
we verify that server_folder+server_uid matches the Message-ID,
so that we do not delete the wrong message by accident.
the reason for that is that, to my knowledge, server_folder+server_uid
are not necessarily stable but Message-ID is.

anyway, this check was broken and set the server_uid to 0 in all cases -
where it should be set to 0 _only_ on mismatches.
2020-01-21 16:27:21 +03:00
Alexander Krotov
a3f2088046 Fix the message printed on job success
"Cannot be retried" does not make it clear if the job succeeded or failed.
2020-01-20 10:47:32 +01:00
Floris Bruynooghe
186f5553b8 Introduce a ChatId newtype
This doesn't try and change the way ChatId is used.  It still allows
creating them with 0 and lets some function use a ChatId(0) as error
return.
2020-01-19 23:42:08 +01:00
Alexander Krotov
d8454d9da5 Deduplicate SendMdn and SendMsgToSmtp code 2020-01-20 00:35:11 +03:00
Alexander Krotov
318194a216 Rename Param::MessageId into Param::MsgId
MsgId refers to database ID, same as crate::message::MsgId newtype.
2020-01-20 00:35:11 +03:00
Alexander Krotov
b50d2358d3 Rename X-Additional-Message-IDs into Additional-Message-IDs
X- prefixes are deprecated in https://tools.ietf.org/html/rfc6648
2020-01-20 00:35:11 +03:00
Alexander Krotov
763587ffb4 Load only one job at a time
As a result of job, other jobs can be added or deleted. To avoid
processing deleted jobs and to process other jobs in the correct order,
we should reload them after performing each job.

However, we don't use "LIMIT 1" in SQL queries, because we want to be
able to skip over invalid jobs, and checking if job is invalid can only
be done in Rust.
2020-01-20 00:35:11 +03:00
Alexander Krotov
8a375c12e9 Mark two messages as seen in test_send_and_receive_message_markseen
This may or may not send a combined MDN out.  We don't test for it,
but the test ensures that *if combined MDNs are sent in this case*,
then we receive them correctly.
2020-01-20 00:35:11 +03:00
Alexander Krotov
11afdb51f3 Log it when MDNs are combined 2020-01-20 00:35:11 +03:00
Alexander Krotov
e4353f4650 Aggregate SendMdn jobs 2020-01-20 00:35:11 +03:00
Alexander Krotov
9d4437a7f5 Make it possible to add X-Additional-Message-IDs to MDNs 2020-01-20 00:35:11 +03:00
Alexander Krotov
493bf5ed08 Parse additional message IDs in MDNs 2020-01-20 00:35:11 +03:00
Alexander Krotov
93800fd834 Construct list of recipients in SendMdn
There is always one recipient for MDNs, so it is easier to only handle
this case.
2020-01-20 00:35:11 +03:00
Alexander Krotov
79bc34ed76 Move check for blocked contact from mimefactory to SendMdn job 2020-01-20 00:35:11 +03:00
Alexander Krotov
b40ad7e87e Store contact ID in SendMdn job foreign_id
This change makes it possible to find all pending MDNs for the contact
with an SQL query.
2020-01-20 00:35:11 +03:00
Alexander Krotov
1ab3fba212 Move check for enabled MDNs from message rendering to MDN job 2020-01-20 00:35:11 +03:00
Alexander Krotov
3cd0bbc0f4 Move foreign_id handling out of mimefactory 2020-01-20 00:35:11 +03:00
Alexander Krotov
a806728e43 Make a new job type for MDN sending
Now MDN is generated on every try instead of being stored in a blob.
2020-01-20 00:35:11 +03:00
Alexander Krotov
683037ca69 Remove no-op jobs SendMdnOld and SendMsgToSmtpOld 2020-01-20 00:35:11 +03:00
Alexander Krotov
a113127df1 Implement Display for MessageState 2020-01-20 00:35:11 +03:00
Alexander Krotov
fecfaaf812 Test MDN parsing 2020-01-20 00:35:11 +03:00
Alexander Krotov
8df8f1f6f7 Update deltachat_derive metadata
Same as core and FFI.
2020-01-19 21:40:22 +01:00
Alexander Krotov
5502ca5f58 Update deltachat_derive dependencies 2020-01-19 21:40:22 +01:00
Alexander Krotov
75b8cfa92e Disable unnecessary image crate features
TIFF file format and parallel processing of JPEGs are not necessary to
recode avatars.
2020-01-18 00:30:52 +03:00
B. Petersen
f7b23fb0e2 check that recoding of jpg and png files actually works 2020-01-18 00:30:21 +03:00
B. Petersen
42f7abf2f5 for test_selfavatar*(), use the same function signature as for other tests 2020-01-18 00:30:21 +03:00
Alexander Krotov
08dd0e34d1 Make reqwest features "blocking" and "json" non-optional 2020-01-17 16:01:11 +03:00
Floris Bruynooghe
7540770dec Resultify get_chat_id_by_grpid
I want to avoid having to be able to represent a chat_id of 0 in order
to more nicely turn chat_id into a ChatId newtype.
2020-01-16 21:46:06 +01:00
Alexander Krotov
213c5df706 mimefactory: remove unused "increation" field 2020-01-16 23:37:53 +03:00
B. Petersen
578e4b2785 re-add some comment to the securejoin-flow 2020-01-16 17:06:24 +01:00
Floris Bruynooghe
8f8db0c431 Small language improvement 2020-01-15 07:27:07 +01:00
Alexander Krotov
df7253b13e Document MessageState and fix some typos 2020-01-15 00:48:10 +03:00
Alexander Krotov
fcafd63f41 Remove NOTE about third-party libraries
"libs" directory has been removed during transition to Rust
2020-01-14 10:29:08 +03:00
Floris Bruynooghe
c82fdb9fa1 Refactor return values from secure-join message handling
This return value was very complicated to understand.  Some failure
returns were returned as Err and some as Ok with no consistency, but
resulting in the same behaviour.

This refactor makes the handle_securejoin_handshake the sole place
responsible for maintaining the state of the secure join
process (context.bob) and also in charge of terminating the ongoing
process.  This is none of receive_imf's business.

The remaining returns are now cleanly classified in application-errors
and protocol errors:

Applications errors result in an Err and mean there is a bug or
something else serious went wrong, like database access suddenly
failed or so.  In this case receive_imf is still responsible for
clearing the state and resetting ongoing-process.  It may be possible
this should still be moved back to securejoin.rs so that recieve_imf
doesn't need to know anything about this either.

Protocol errors are not failures for receive_imf, it just means the
received message didn't follow the protocol.  Receive_imf in this case
is told to ignore the message: that is hide it but not delete it.

Other Ok returns also only say what needs to happen to the message:

- It's fully processed and needs no further processing, instead should
  be removed

- It should still be processed as a normal received message.

This changes some behaviour: if the chat creation/lookup for the
contact fails this is treated as an application error.  Previously
this was silently ignored and send_msg() would be called with a 0
chat_id without checking the response.  This resulted in the protocol
quitely being blocked.

This all shhould now make it easier to resultify more of the functions
called by this function, instead of having to deal with very
complicated application logic hidden in the return code.
2020-01-13 20:46:58 +01:00
Alexander Krotov
454c52f4ab Use correct SPDX license identifier in Cargo.toml 2020-01-13 10:48:16 +00:00
B. Petersen
73c69c3f93 fix optimisation 2020-01-13 01:43:27 +01:00
bjoern
614ec30e05 Update src/mimefactory.rs
Co-Authored-By: Alexander Krotov <ilabdsf@gmail.com>
2020-01-13 01:43:27 +01:00
B. Petersen
d7c07e0ed3 set subject to group-name, tweak prefixes
this commit sets the subject of groups to the name of the group.
if the message is a reply in the group, also the prefix `Re:` is added.
the Chat: prefix is removed for groups,
also the Fwd: prefix, as it this would be expected to be set to the
group name of the forwarded message, which is not really compatible with the
rule "group-name = subject" (forwarded messages are detected by the body,
not by the subject)
2020-01-13 01:43:27 +01:00
Alexander Krotov
08bd16c1b6 Update rusqlite 2020-01-12 21:54:38 +00:00
Alexander Krotov
4f06c72798 Remove top_evil_rs.py 2020-01-12 09:02:30 +00:00
Alexander Krotov
166fe2a4e5 Forbid unsafe code in deltachat crate 2020-01-12 09:02:30 +00:00
Alexander Krotov
b7635a71c8 Do not call unsafe rusqlite::ffi::sqlite3_threadsafe()
rusqlite already checks that SQLite is safe to use in multi-threaded mode when it opens the database and returns rusqlite::Error::SqliteSingleThreadedMode otherwise.
2020-01-12 09:02:30 +00:00
Alexander Krotov
6ad4bdea83 Make get_parent_mime_headers() private 2020-01-12 09:12:24 +01:00
Alexander Krotov
efb9a11d22 Do not select drafts as parent message
Also create a common parent_query() function to make sure we use the
same message as a parent when determining if it is encrypted and when
actually replying to it.
2020-01-12 09:12:24 +01:00
Alexander Krotov
047d09bcb1 Do not pass &Sql to parent_is_encrypted() 2020-01-12 09:12:24 +01:00
Alexander Krotov
e1ca6b5181 Rename last_msg_in_chat_encrypted into parent_is_encrypted 2020-01-12 09:12:24 +01:00
Alexander Krotov
36a2569537 Make last_msg_in_chat_encrypted a member function 2020-01-12 09:12:24 +01:00
Alexander Krotov
10ceddfa67 Set draft in test_reply_encrypted test
Adding unencrypted draft to chat makes the job of determining whether
last message is encrypted harder.
2020-01-12 09:12:24 +01:00
holger krekel
929677afe0 fix typo 2020-01-11 21:10:08 +00:00
Alexander Krotov
ee4adc1363 Return &str from extract_grpid 2020-01-11 14:59:53 +01:00
Alexander Krotov
c7b2bdfaac mimeparser: simplify get_subject() and get_rfc724_mid 2020-01-11 14:59:53 +01:00
Alexander Krotov
fde8fb960b oauth2: replace unwrap() and is_none() with "if let" 2020-01-11 14:59:53 +01:00
Alexander Krotov
9a43d26c60 dc_receive_imf: do not unwrap() Some(..) immediately after creation 2020-01-11 14:59:53 +01:00
Alexander Krotov
742f603b3b Use if let to avoid unwrap() of peerstate 2020-01-11 14:59:53 +01:00
Alexander Krotov
a82f2a5df3 Simplify needs_encoding() and add a test 2020-01-11 14:59:53 +01:00
Alexander Krotov
06bc5513ae Use Vec::first to avoid explicit is_empty() check 2020-01-11 14:59:53 +01:00
Alexander Krotov
47b937f880 Turn some enum Param comments into doc-comments 2020-01-11 15:27:09 +03:00
Alexander Krotov
775d27b6a9 Update group test 2020-01-10 15:20:23 +03:00
Alexander Krotov
6c838ab57c Never reset gossip timestamp for all chats at the same time 2020-01-10 15:20:23 +03:00
Alexander Krotov
8eebd2aa67 Do not reset gossip timestamp when group members change 2020-01-10 15:20:23 +03:00
Alexander Krotov
fc78a08657 Fix inverted should_do_gossip condition 2020-01-10 15:20:23 +03:00
Alexander Krotov
7b3cc95ab7 Derive Debug for Loaded and MimeFactory 2020-01-10 06:42:36 +00:00
Alexander Krotov
bd70765b7d Join recipients_names and recipients_addr into one vector
This change ensures on the type level that number of names and addresses
is the same.
2020-01-10 06:42:36 +00:00
Alexander Krotov
5b424aec22 Remove temporary variable "row" 2020-01-10 06:42:36 +00:00
Alexander Krotov
d6cc0694f0 Move Chat from MimeFactory to Loaded::Message 2020-01-10 06:42:36 +00:00
Alexander Krotov
9738515129 Construct immutable MimeFactory in from_msg 2020-01-10 06:42:36 +00:00
Alexander Krotov
b05ee10f41 Return Err from MimeFactory::from_msg on database errors 2020-01-10 06:42:36 +00:00
Alexander Krotov
67d85f0f86 Make all MimeFactory fields private 2020-01-10 06:42:36 +00:00
Alexander Krotov
2ddeef761f Fix gossiped_timestamp documentation 2020-01-09 15:40:58 +03:00
Alexander Krotov
43e0109d44 Update reqwest to 0.10.0 2020-01-09 12:17:17 +03:00
Alexander Krotov
02bb41697d Add MessengerMessage type for is_dc_message field 2020-01-08 19:00:45 +03:00
Alexander Krotov
12cd56e3e8 Rename type_0 into viewtype 2020-01-08 19:00:45 +03:00
Floris Bruynooghe
72cfb70e35 Refactor create_chat_from_msg_id
The goal here is to not have a `mut chat_id` as I want to change chat
id into an opaque newtype and we can't modify it like that anymore.
2020-01-08 14:58:44 +01:00
Alexander Krotov
40dc180b88 Update mailparse to 0.10.2
It does not allow parsing emails without @
2020-01-08 16:52:27 +03:00
Alexander Krotov
4529c326c6 Restore newlines and remove semicolon in test_parse_first_addr 2020-01-07 17:23:59 +01:00
Alexander Krotov
f6660af014 Update test_mdn_asymmetric
MDNs are processed now even when MDNs are disabled
2020-01-07 17:23:19 +01:00
Alexander Krotov
a52131b574 cargo fmt 2020-01-07 17:23:19 +01:00
Alexander Krotov
a48d0492c8 Create only one MarkseenMdnOnImap job even if message has multiple reports
All created jobs have the same parameters and move the same message,
so it does not make sense to create more than one.
2020-01-07 17:23:19 +01:00
Alexander Krotov
2bba1be817 Show received MDNs even if user has disabled them
Delta Chat does not request MDNs if they are disabled, so this change
does not make much difference, but simplifies the logic.
2020-01-07 17:23:19 +01:00
bjoern
8a394fb08f Merge pull request #1141 from deltachat/add-missing-event
send DC_EVENT_CHAT_MODIFIED when adding members to a group
2020-01-07 13:46:17 +01:00
Alexander Krotov
693ae9e8f2 Guess mimetype of .webm files as video/webm 2020-01-07 09:19:36 +00:00
B. Petersen
4b7b6d6cb3 send DC_EVENT_CHAT_MODIFIED when adding members to a group (removing is already fine) 2020-01-06 23:35:27 +01:00
Floris Bruynooghe
9a3bdfb14b Rename MimeParser to MimeMessage in mimeparser.rs
The struct really represents a parsed MIME message and is not used as
a parser itself.  After the from_bytes() call (which should arguably
use the FromStr trait instead) the struct is fully populated.
2020-01-06 09:46:47 +00:00
Alexander Krotov
7aeddc63ac Use DC_CHAT_ID_DEADDROP instead of constant "1" 2020-01-06 00:04:03 +00:00
Floris Bruynooghe
2990a1c255 Mark the ChatInfo struct non_exhaustive
This is a new feature in Rust 1.40, it means users outside the crate
will not be able to create these structs, allowing us to add fields
without breaking the public API.
2020-01-05 22:53:52 +00:00
B. Petersen
91eea03b18 hide messages with Secure-Join headers if the Secure-Join fails 2020-01-05 22:06:06 +01:00
Alexander Krotov
8702f290af mimeparser: remove unused parsed_protected_headers field 2020-01-05 22:03:10 +01:00
Alexander Krotov
d0b5b7ba03 Turn handle_reports comment into documentation comment 2020-01-04 22:24:16 +01:00
Alexander Krotov
f3d68c6f25 Add requests dependency to python/README.rst
It is used in tests/conftest.py
2020-01-04 20:18:31 +01:00
Alexander Krotov
91100d3fac Lowercase the addresses before gossip peerstate update
Normally it happens in addr_cmp function, but update_gossip_peerstates
forms a HashSet of addresses, so they should be lowercased beforehand.

Also adjust the mail_with_cc.txt to test for non-lowercase addresses.
2020-01-02 18:14:27 +00:00
Alexander Krotov
61833c32e5 Remove trailing space from deltachat.h 2020-01-01 23:05:08 +00:00
Alexander Krotov
1468f67b9b Warn about multiple From addresses only when necessary
The message says "only using first" so it should be printed right before the first address is taken with .get_index(0) operation.
2020-01-01 21:03:28 +00:00
Alexander Krotov
cb6376094c Get rid of DC_FP_* constants 2020-01-01 20:53:31 +00:00
björn petersen
ff175e661b Merge pull request #1130 from deltachat/fpr-without-addr
handle plain OPENPGP4FRP qr-codes correctly
2020-01-01 21:49:37 +01:00
B. Petersen
fe741f168a fix early exit on empty addresses in decode_openpgp() 2020-01-01 18:53:35 +01:00
B. Petersen
eaaf500d5e add failing test for plain OPENPGP4FRP-qr-code 2020-01-01 18:52:00 +01:00
björn petersen
05f85b33c5 Merge pull request #1124 from deltachat/mimeparser_message_kml
Location streaming fixes
2019-12-31 16:49:08 +01:00
Alexander Krotov
c4e0647bda Remove NULs from mimeparser tests 2019-12-30 17:32:39 +01:00
Alexander Krotov
6dcc1e09b9 mimeparser: add message.kml parsing test 2019-12-30 17:29:28 +01:00
Alexander Krotov
ea03e4d34a Send location.kml instead of message.kml for streaming
message.kml is sent when user manually selects a point on the map to
point to a location.
2019-12-30 17:20:46 +01:00
B. Petersen
b7a2d17e93 remove weird comment 2019-12-29 18:58:49 +00:00
B. Petersen
e1bd740249 init authname when received by From: header (it is also updated from there) 2019-12-29 18:58:49 +00:00
B. Petersen
de52c2da80 add failing test for receiving authnames 2019-12-29 18:58:49 +00:00
Alexander Krotov
a31c6d82c9 Remove get_origin_by_id 2019-12-28 22:33:57 +00:00
Alexander Krotov
25842894d2 Rename job::Try into job::Status 2019-12-28 14:35:14 +00:00
Alexander Krotov
83c98c2d55 Replace unstable try_trait with a job_try! macro 2019-12-28 14:35:14 +00:00
Alexander Krotov
fe2011742d Job error handling refactoring 2019-12-28 14:35:14 +00:00
B. Petersen
d87b676d60 do not abort search completely when updating a special name fails 2019-12-27 22:53:03 +00:00
Alexander Krotov
ffa6378108 Fix clippy warnings in deltachat-ffi 2019-12-27 10:28:50 +00:00
B. Petersen
f73ba895af remove some meanwhile unneeded allow-statements 2019-12-26 16:26:06 +00:00
B. Petersen
cb2a1147f0 fix searching for localized device chats 2019-12-26 16:23:42 +00:00
B. Petersen
6702ef4a71 add a failing test that does not fine localized device chats 2019-12-26 16:23:42 +00:00
Alexander Krotov
1d46791364 Add more Yandex domains 2019-12-26 01:48:28 +00:00
björn petersen
f61846dec9 Merge pull request #1107 from deltachat/list-language-bindings
list language bindings in README
2019-12-26 01:35:31 +01:00
B. Petersen
4776e196da add frontend projects to README 2019-12-26 00:42:07 +01:00
B. Petersen
839a48b678 list language bindings in README 2019-12-26 00:42:07 +01:00
Alexander Krotov
e203901224 Better logging message for job deletion
The logging message printed for successful job completion said that the
job "cannot be retried" even when it does not need to be retried.
2019-12-24 10:47:29 +00:00
Alexander Krotov
76d03f7fd2 Log SMTP connection failure errors 2019-12-23 07:14:11 +01:00
holger krekel
7f5e3aaaf7 add changelog for device-chat fix 2019-12-22 05:45:27 +01:00
B. Petersen
5bfbae4b00 add test for delete_and_reset_all_device_msgs() 2019-12-22 05:44:09 +01:00
B. Petersen
521a854635 delete and reset device-messages on import; this avoids wrong information in the device chat and allows adding correct information again 2019-12-22 05:44:09 +01:00
B. Petersen
ce15ef2db9 prep beta22 2019-12-22 05:42:33 +01:00
holger krekel
6d5cf89d33 enable link-time-optimization 2019-12-21 23:09:15 +00:00
holger krekel
b3b984f351 also "cargo update", pulls in aysnc-std 1.4.0 2019-12-21 23:09:15 +00:00
holger krekel
d5a0f1e711 use latest lettre master that contains the normalization fix 2019-12-21 23:09:15 +00:00
Alexander Krotov
67c36f3d98 Use usize to get pointer size
According to Rust documentation, usize is "The pointer-sized unsigned
integer type".

This change removes unnecessary libc dependency and makes top_evil_rs
script happier.
2019-12-21 23:07:35 +00:00
Alexander Krotov
8e0a29e9b5 Stop using Event callback return values
Since stock string callback has been deprecated, all event callbacks
return 0.

For compatibility, C declarations are not changed and FFI users are
expected to return 0 from their callbacks.
2019-12-21 22:51:44 +00:00
Alexander Krotov
47be879445 fix(job): log all job events in job_perform() 2019-12-21 15:24:30 +00:00
Alexander Krotov
860f8a7906 refactor(job): refer to std:: only once
This change unifies how we refer to std::time::Duration across the module.
2019-12-21 15:24:30 +00:00
Alexander Krotov
5d9baa053a refactor(job): implement Display for Job 2019-12-21 15:24:30 +00:00
Alexander Krotov
da174eae71 refactor(job): use Display to print Thread in job_perform() 2019-12-21 15:24:30 +00:00
björn petersen
7fac71aa81 Merge pull request #1094 from deltachat/tweak-repl
use println!() for all repl outputs
2019-12-21 16:00:27 +01:00
B. Petersen
7d51c1140d use println!() for all repl outputs
in the very beginning, we allowed using the cmdline partly from the ui
via a special function; this made info!() handy.
however, this is long gone and no longer needed.
2019-12-21 13:09:38 +01:00
Alexander Krotov
cd198223ea Remove unwrap() from time() function
This change is a part of the effort to enable clippy::option_unwrap_used and clippy::result_unwrap_used
2019-12-21 08:58:41 +01:00
Alexander Krotov
300fff40e3 refactor(smtp): remove unused SendTimeout Error variant
It was used when timeout was set on the whole smtp.send() operation.
Now only the operations inside smtp.send() can timeout, and such timeout
errors result in SendError, so SendTimeout is unused.
2019-12-21 08:57:29 +01:00
Floris Bruynooghe
08abac350d Tweak the python readme a bit more. 2019-12-21 00:10:45 +00:00
shubhank008
a57decf8be Up to date Readme.rst
-Moved wheels installation below source installation and added comment that they are outdated and not working atm
-Updated source compilation/installation to a more step by step process as current one was outdated and not working due to Rust now needing nightly build else failing to compile (subtle v2.2.2 do not work without nightly)
2019-12-20 21:14:16 +01:00
holger krekel
ee95e59243 "cargo update" 2019-12-20 18:11:29 +01:00
holger krekel
b0694bcf2c prep beta21 2019-12-20 15:17:59 +01:00
Alexander Krotov
c9f6e31ca9 Move dc_simplify.rs to simplify.rs 2019-12-20 12:55:57 +01:00
Alexander Krotov
fe4080d59f refactor(simplify): move dehtml dependency to mimeparser
This change also removes unnecessary String clone for HTML messages.
2019-12-20 12:55:57 +01:00
B. Petersen
7f6a1ad1a7 add some empty lines to enums & co to make things more readable and to avoid errors this way :) 2019-12-20 12:55:42 +01:00
Alexander Krotov
980bb35441 refactor(mimefactory): use iter::repeat_with instead of (0..) 2019-12-20 12:55:25 +01:00
holger krekel
01df2e2dc7 fix #1077 for unknown senders in a group chat (such as mailer daemons): don't recreate member list and show a special stockstring-ed message advising to hit "more info". 2019-12-20 11:59:10 +01:00
Floris Bruynooghe
ec40dd1b6f Change the JSON API function to be from a serialised struct
This is the first experiment towards using structs to define the API.
It adds it as a new method on the existing Chat struct.

The types in this struct could improve as well as many other things.
But this is a straight forward translation of the existing json! macro
into a struct.
2019-12-20 10:39:39 +01:00
Alexander Krotov
f2f8898004 fix(mimefactory): wrap base64-encoded attachments to 78 columns
RFC5322 requires emails to be wrapped to 78 columns excluding CRLF.
2019-12-20 09:11:35 +01:00
björn petersen
3afa37c6ea Merge pull request #1075 from deltachat/prepare_beta20
prep 1.0.0-beta.20
2019-12-19 17:17:26 +01:00
holger krekel
8c0ce38301 prep 1.0.0-beta.20 2019-12-19 16:55:24 +01:00
holger krekel
cc6aa3209c fix the fix 2019-12-19 16:53:55 +01:00
holger krekel
76a86763dd address @r10s comment 2019-12-19 16:53:55 +01:00
holger krekel
09fb039528 another reverse mut bites the dust 2019-12-19 16:53:55 +01:00
holger krekel
174d3300c4 not sure it's much better but using a static-sized array is probably better than a dynamically sized vec, thanks @dignifiedquire 2019-12-19 16:53:55 +01:00
holger krekel
8b57ce1792 remove unused include_in_contactlist 2019-12-19 16:53:55 +01:00
holger krekel
6c14e429eb Origin::is_verified() -> Origin::is_known() because this has nothing to do with verified groups or contacts 2019-12-19 16:53:55 +01:00
holger krekel
5f200c6bc3 don't pass incoming_origin as &mut as the caller doesn't need it 2019-12-19 16:53:55 +01:00
holger krekel
d52347ee1d also don't pass "to_id" and don't make it mut inside add_parts 2019-12-19 16:53:55 +01:00
holger krekel
d0d9aa4400 - move CC-parsing next to To-parsing where it blongs
- pass to_ids and from_id as immutable to add_parts
2019-12-19 16:53:55 +01:00
holger krekel
c3d909c818 add a test that contacts are properly created and fix ordering in dc_receive_imf to pass the test 2019-12-19 16:53:55 +01:00
dignifiedquire
d9e718b0e0 feat(imap): update async-imap for oauth2 fix 2019-12-19 13:51:59 +01:00
holger krekel
a7b55edb9b more call sites 2019-12-19 12:06:01 +01:00
holger krekel
000479d55e never block on interrupt_inbox_idle 2019-12-19 12:06:01 +01:00
Alexander Krotov
7ef22f2940 fix(smtp): reduce SMTP I/O timeout to 30 seconds
15 minute timeout was used because it applied to the whole send()
operation. Now timeout applies to each I/O operation, such as read()
or write(), so it can be made much shorter. In particular, this timeout
applies to read() call right after connection to plain or STARTTLS server,
in which case it is not reasonable to wait 15 minutes to receive one
line of data.

The timeout is triggered only if no progress is made within 30 seconds. It
likely indicates a network failure anyway.
2019-12-19 06:55:19 +01:00
Alexander Krotov
a242fcfd2c refactor(dehtml): remove Result unwrap in dehtml_starttag_cb() 2019-12-18 23:24:43 +03:00
Alexander Krotov
73c21ae0a9 refactor: enable clippy::unreadable_literal 2019-12-18 23:23:38 +03:00
dignifiedquire
2398454838 feat(smtp): update to use async-smtp based timeouts 2019-12-18 03:08:59 +03:00
holger krekel
c1ba5a30c5 prepare beta.19 2019-12-16 23:46:28 +01:00
holger krekel
2c0f847d3e remove too verbose logging for received messages 2019-12-16 23:12:17 +01:00
holger krekel
7d5e95f013 thread-name seems to be unnamed all the time so it's use is currently theoretic 2019-12-16 23:12:17 +01:00
Alexander Krotov
9000342de8 Get rid of unnecessary "async move" and ".await" 2019-12-16 18:02:50 +01:00
holger krekel
61b47aa0de try doing an smtp-send timeout 2019-12-16 18:02:50 +01:00
björn petersen
22f5c5fb74 Merge pull request #1057 from deltachat/beta18
prepare to tag 1.0.0-beta.18
2019-12-16 15:00:48 +01:00
holger krekel
801162d7be prepare to tag 1.0.0-beta.18 2019-12-16 13:20:14 +01:00
Alexander Krotov
f81f3fb060 Do not panic on failure to read IMAP server greeting 2019-12-16 13:19:34 +01:00
holger krekel
15792d8426 use dcc-stable branch that returns an error on missing greetings() "* OK Dovecot ready" (currently it panics) 2019-12-16 13:19:34 +01:00
Floris Bruynooghe
d7f345eef8 Add the thread id and name to info and warn log output 2019-12-16 13:11:44 +01:00
Alexander Krotov
e3031462c1 Replace expect() in select_with_uidvalidity with an Error 2019-12-16 14:44:52 +03:00
holger krekel
47d14271ab fix #1020 -- allow to set os_name in python bindings 2019-12-16 11:51:04 +01:00
holger krekel
2bf9fd6cbc revert printing file/lineno in Error-messages as these are typically user-visible
For info and warn it's fine
2019-12-16 03:17:08 +03:00
Floris Bruynooghe
19e716b522 Add filename and line no to log entries
This is done for all logging calls, also those which call error! which
is normally directly shown to the user.
2019-12-16 00:33:57 +01:00
Alexander Krotov
1ee15942cc Simplify simplify.rs
* Remove Simplify structure.

* Match for lines starting with 5 markers, not consisting of exactly 5 markers.

This is a regression from C to Rust conversion, see
2bb1c280d5/src/dc_simplify.c (L154)

* Add tests.
2019-12-16 02:31:38 +03:00
björn petersen
898e641256 Merge pull request #1048 from deltachat/beta17
add changelog for beta17 and bump versions
2019-12-15 23:32:11 +01:00
holger krekel
cda4ccff2a add changelog for beta17 and bump versions 2019-12-15 23:26:05 +01:00
B. Petersen
ba274482f7 make d.png much smaller in size to avoid avatar-recoding and to allow checking file contents 2019-12-15 23:12:37 +01:00
B. Petersen
435f734d60 adapt tests for new avatar recoding 2019-12-15 23:12:37 +01:00
B. Petersen
a5f949c4e2 recode group- and user-avatar to 192x192 pixel 2019-12-15 23:12:37 +01:00
B. Petersen
9fc556864e add image crate 2019-12-15 23:12:37 +01:00
Alexander Krotov
a0645dc713 Resultify dc_create_folder and don't ignore its errors 2019-12-16 00:49:51 +03:00
holger krekel
5893cd309d fixate async-imap to the working commit so that no one accidentally does "cargo update" and breaks things. 2019-12-16 00:49:37 +03:00
holger krekel
09c7ab1ee6 avoid addrparse to panic() and refactor according code a little with test. 2019-12-15 21:54:20 +01:00
holger krekel
4bacae3711 fix #1037 and simplify mdn-report related code and state 2019-12-15 22:18:34 +03:00
Alexander Krotov
8d3e536582 Do not panic on SystemTimeDifference 2019-12-15 19:56:13 +01:00
Alexander Krotov
697cc0a79b Make needs_encryption a bool 2019-12-15 19:55:49 +01:00
Alexander Krotov
a34ed5c02a Make recalc_fingerprints a bool 2019-12-15 19:55:49 +01:00
Alexander Krotov
256bb01606 Make prepend_subject a bool 2019-12-15 19:55:49 +01:00
Alexander Krotov
1de535363d Move parse_message_id from wrapmime.rs to imap/mod.rs 2019-12-15 19:13:49 +03:00
Alexander Krotov
24d9011939 Move get_autocrypt_mime from wrapmime.rs to e2ee.rs 2019-12-15 19:13:49 +03:00
holger krekel
6284bdc98f pull in latest async-smtp master to fix #1030 2019-12-15 15:27:25 +01:00
Alexander Krotov
e7351b1bb8 Restore constant and remove parenthesis 2019-12-15 16:43:12 +03:00
Alexander Krotov
b3ee89c6e5 Documentation improvements 2019-12-15 16:15:09 +03:00
Alexander Krotov
3f49492ccf cargo fmt 2019-12-14 22:28:15 +01:00
Alexander Krotov
ad700b45d0 Make Imap.connect() async 2019-12-14 22:25:33 +01:00
Alexander Krotov
74923b4575 Enable clippy::type_complexity error 2019-12-14 22:00:38 +01:00
Alexander Krotov
81199e7ee0 Update Cargo.lock 2019-12-14 18:16:54 +00:00
Alexander Krotov
ccca1b0bea Do not format! SQL queries 2019-12-14 18:16:54 +00:00
Alexander Krotov
ba1ced5e08 Switch to master branch of async-imap 2019-12-14 17:51:36 +01:00
Alexander Krotov
558466d506 Revert "Enable clippy::unreadable_literal error"
This reverts commit b424ef9ebb.
2019-12-14 15:55:33 +00:00
Alexander Krotov
b424ef9ebb Enable clippy::unreadable_literal error 2019-12-14 15:20:58 +00:00
Alexander Krotov
69f5fd86a4 Allow clippy::needless_range_loop only in simplify_plain_text 2019-12-14 15:20:58 +00:00
Alexander Krotov
f9ba9ae912 Enable clippy::large_enum_variant error 2019-12-14 15:20:58 +00:00
Alexander Krotov
b33fec6236 Enable clippy::block_in_if_condition_stmt error 2019-12-14 15:20:58 +00:00
Alexander Krotov
70e12485aa Switch to native_tls version of deltachat/lettre fork
This commit completely removes rusttls dependency
2019-12-14 16:12:13 +01:00
Alexander Krotov
b2b7674b59 Update Cargo.lock 2019-12-14 16:12:13 +01:00
holger krekel
8fa175f36d changelog and version up to 1.0.0-beta.16 2019-12-14 00:22:09 +01:00
B. Petersen
95fbc904d1 go to correct step so that param_autoconfig is actually used 2019-12-14 00:12:14 +01:00
B. Petersen
ce37a8dda2 use provider-database only if the user did not enter a server/port/whatever manually; this allows the user to override erroneous choices 2019-12-14 00:12:14 +01:00
holger krekel
e0499c9552 add preliminary support for offline-autoconfigure for nauta 2019-12-14 00:12:14 +01:00
Simon Laux
4825f2510f Merge pull request #1016 from deltachat/fix_displayname
fix displayname to use authname when availible
2019-12-13 13:21:03 +01:00
Alexander Krotov
7a12134795 Test that no gossip is sent in 1-to-1 chat 2019-12-13 12:51:44 +01:00
Alexander Krotov
66adfa074b Do not cache gossiped_timestamp 2019-12-13 12:51:44 +01:00
Alexander Krotov
9d201eb9c6 Add get_gossiped_timestamp 2019-12-13 12:51:44 +01:00
Alexander Krotov
789fc0a7e0 Resultify set_gossiped_timestamp 2019-12-13 12:51:44 +01:00
Alexander Krotov
9e309132f8 Only add gossip headers if chat has more than one peer 2019-12-13 12:51:44 +01:00
Alexander Krotov
88923173c2 Set is_gossiped to true when Autocrypt-Gossip header is added 2019-12-13 12:51:44 +01:00
Simon Laux
7b55863418 switch priority order 2019-12-13 11:58:21 +01:00
Simon Laux
38d5fad4e5 fix displayname to use authname when availible 2019-12-13 11:58:08 +01:00
holger krekel
4d1357568b use async-smtp master where @link2xt's branch has been merged 2019-12-13 11:03:46 +01:00
Alexander Krotov
ccc190f991 Switch to native_tls 2019-12-13 11:03:46 +01:00
B. Petersen
0a9f880fb4 remove another superfluous return value 2019-12-13 01:17:48 +01:00
holger krekel
b8faf54f0b remove superflous expression values 2019-12-13 01:17:48 +01:00
B. Petersen
888507f7ba test group- and user-avatars in the same mail 2019-12-13 01:17:48 +01:00
B. Petersen
29866092de adapt the protected-header test; protected-headers really require encryption now and parts of the tests are know to fail now. however, protected-headers are also tested in python implicitly in several places 2019-12-13 01:17:48 +01:00
B. Petersen
36fc2c83f2 return selfavatar from dc_get_info() 2019-12-13 01:17:48 +01:00
holger krekel
5e777b3c51 fix test also for avatar deletion, use that drain filter while we in this code area :) 2019-12-13 01:17:48 +01:00
holger krekel
5690c48863 fix merging of protected headers 2019-12-13 01:17:48 +01:00
holger krekel
8ab3363097 add a test that runs until an avatar is send encrypted 2019-12-13 01:17:48 +01:00
B. Petersen
f6861ca5f5 target comments from @flub and @dignifiedquire 2019-12-13 01:17:48 +01:00
B. Petersen
3bb58be2b5 add missing delete header 2019-12-13 01:17:48 +01:00
B. Petersen
409c96e571 add compatiblity headers 2019-12-13 01:17:48 +01:00
B. Petersen
d681fa6cba use Chat-User-Avatar and Chat-Group-Avatar
we change the name on the wire as the old Chat-Group-Image header
could not be used on random mails, it was the marker for a "Changed" message,
if we would keep this names, things will fail for exising installations
as messages are dropped and a "Group image changed" message is shown instead.
2019-12-13 01:17:48 +01:00
B. Petersen
7c3d8356c4 always handle incoming group-avatars 2019-12-13 01:17:48 +01:00
B. Petersen
a8842da50a && has a higher precedence than || - fix a logical error when resetting Unpromoted 2019-12-13 01:17:48 +01:00
B. Petersen
ff29b84146 share group image on group creation, fixes #602 2019-12-13 01:17:48 +01:00
B. Petersen
ab12a4eb39 move ImageAction parsing to a reusable function 2019-12-13 01:17:48 +01:00
B. Petersen
c3fd0889e2 use tri-state ImageAction instead of Option<Option> 2019-12-13 01:17:48 +01:00
B. Petersen
c62532a665 adapt to new HeaderDef api 2019-12-13 01:17:48 +01:00
B. Petersen
ca63d6ba1c handle incoming profile-images 2019-12-13 01:17:48 +01:00
B. Petersen
a1f496b019 let mimeparser handle profile-images 2019-12-13 01:17:48 +01:00
B. Petersen
da421438cd do not send selfavatar on existing installation without resetting 2019-12-13 01:17:48 +01:00
B. Petersen
7f723ef2bf attach selfavatar 2019-12-13 01:17:48 +01:00
B. Petersen
3cf39dace0 print profile-image in repl tool 2019-12-13 01:17:48 +01:00
B. Petersen
021ad4f12c set correct icon for DC_CONTACT_ID_DEVICE 2019-12-13 01:17:48 +01:00
B. Petersen
36edf447e7 load param incl. user-profile-image from db 2019-12-13 01:17:48 +01:00
B. Petersen
ea2273aef4 reset selfavatar_sent column when the selfavatar is updated 2019-12-13 01:17:48 +01:00
B. Petersen
251aa22c4c add column for selfavatar_sent timestamp 2019-12-13 01:17:48 +01:00
Alexander Krotov
541710147a Copy correct dc_contact_is_verified documentation from C core
instead of dc_contact_is_verified_ex documentation.
2019-12-11 17:54:28 +01:00
dignifiedquire
a197ca4cf7 feat: update dependencies, including rand@0.7 and rpgp@0.4 2019-12-11 15:33:17 +01:00
Simon Laux
775c36bb65 wip move some ci tasks to gh-actions (#998)
* add-action-rs-clippy-action

just to try this action out

* add --all argument

* rm --all argument again

* add cargo check and fmt

* disable circle ci counterparts(fetch, clippy, fmt)
2019-12-10 23:26:42 +01:00
holger krekel
1953b95c57 commit missing Cargo.lock 2019-12-10 21:42:52 +01:00
holger krekel
542a33952f prepare 1.0.0-beta.15 2019-12-10 21:35:20 +01:00
holger krekel
1819712667 make sure we send out <> around message-id's in references and in-reply-to 2019-12-10 21:25:36 +01:00
holger krekel
69a596fdff fix #994 -- avoid double-attachment messages in local chats 2019-12-10 21:25:36 +01:00
björn petersen
4f88b93495 Merge pull request #1003 from deltachat/flub-spec-their
Use 3rd person singular
2019-12-10 19:10:59 +01:00
Floris Bruynooghe
a388052e2a Use 3rd person singular 2019-12-10 19:07:22 +01:00
holger krekel
8b81ea3b6f actually update to 1.0.0-beta.14 2019-12-10 15:39:09 +01:00
holger krekel
b913de5928 prepare a 1.0.0-beta.14 with fixed deps 2019-12-10 14:42:55 +01:00
holger krekel
4e551cde66 update tags and rust-email dep 2019-12-10 14:17:16 +01:00
holger krekel
b681cbd47f prepare 1.0.0-beta.13 2019-12-10 14:02:21 +01:00
holger krekel
551253b4e0 use async-smtp with hostname fixed to version 0.1.5 2019-12-10 14:00:43 +01:00
Alexander Krotov
4ad9166b5a Move one chat test from stress.rs to chat module 2019-12-10 13:54:05 +01:00
holger krekel
8487255c33 prepare changelog for 1.0.0-beta.13 2019-12-10 13:52:57 +01:00
Alexander Krotov
2792d4ea1e Stop using create::error in login_param.rs 2019-12-10 12:39:58 +01:00
björn petersen
95180a850f Merge pull request #993 from deltachat/group_add_remove
Fix various issues around group add/remove
2019-12-10 12:14:52 +01:00
holger krekel
56ee7a0abd try to remove updates to lock file 2019-12-10 02:33:13 +01:00
holger krekel
d0a04be825 remove hack for decoding, and add a test that encoding/decoding works now 2019-12-10 01:35:09 +01:00
holger krekel
2cbf287998 don't cargo-update on CI jobs but use the Cargo.lock file we manually maintain/update 2019-12-10 00:40:18 +01:00
holger krekel
054cf98754 try to work around mailparser not decoding rfc2047 displaynames
this pulls in changes in our fork of rust-email  to also correctly
generate rfc2047 encoding
2019-12-10 00:26:46 +01:00
holger krekel
a95fbfe271 add a test 2019-12-09 22:28:29 +01:00
holger krekel
17ce02a87c add some comment and remove some code after quick a/v with @r10s 2019-12-09 21:45:01 +01:00
holger krekel
4dc5e0378f fix final problem, tests pass now 2019-12-09 20:25:46 +01:00
holger krekel
c33797ff84 remove reverse "check_self" return 2019-12-09 19:41:58 +01:00
holger krekel
f242b40d0a nicer print of imap capabilities 2019-12-09 18:36:26 +01:00
holger krekel
5f916f5a9c - create and use a ContactIds type as an ordered set instead of "Vec<u32>".
- recreate the group list more carefully, fixes #985

- resultify a few functions in the dc_receive pipeline

- don't quote displaynames in email-addresses, use utf8, preliminrarily addresses #976
2019-12-09 16:29:24 +01:00
holger krekel
6edb525540 snap 2019-12-09 14:16:46 +01:00
holger krekel
0c04d5b2ab add a failing test that creates 4 accounts and a group chat with remove/add
see also #985
2019-12-09 14:16:46 +01:00
Alexander Krotov
301852fd87 Add test for kebab_case serialization of From header 2019-12-09 15:43:06 +03:00
holger krekel
0e8df7d633 use automatic serialization, thanks @link2xt for the tip 2019-12-09 15:43:06 +03:00
holger krekel
e4155e0e16 remove lookup_field in favor of get(HeaderDef::...) with all headers defined in headerdef.rs 2019-12-09 15:43:06 +03:00
Floris Bruynooghe
2c4dbe6e68 Re-work some in-creation file handling
This effectively reverts
https://github.com/deltachat/deltachat-core-rust/pull/964 for chat.rs,
which in that PR was thought to fix something.  So maybe something is
still broken?  But after improving tests the previous code seems to be
correct.

- Update Python bindings to not always use dc_prepare_msg path when
  sending messages with attachements.  When using dc_prepare_msg the
  blobs need to be created in the blobdir since they will not get
  copied and many tests where not doing this.

- Add a test that ensures that calling dc_prepare_msg with a
  file **not** in the blobdir fails.

- Add a test that ensures that calling dc_send_msg directly with a
  file **not** in the blobdir copies the file to the blobdir.  This
  test cheats a little by knowing what the filename in the blobdir
  will be which is implementation-dependent and thus a bit brittle.
  But for now it proves correct behaviour so let's go with this.

- Improve the test_forward_increation test to ensure that the
  in-creation file only has it's final state before calling
  dc_send_msg.  This checks the correct file data is sent out and not
  the preparing data, this fails with the chat.rs changes in
  #964 (reverted here to make this work again).  Also fix the test to
  actually create the in-creation file in the blobdir.

- Fix test_send_file_twice_unicode_filename_mangling to not use
  in-creation.  It was not creating it's files in the blobdir and that
  is an error when using in-creation and it didn't seem it was trying
  to test something about the in-creation logic (which is tested in
  test_increation.py already).

- Fix Message._msgtate code which presumably was not used before?

- Rename `BlobObject::create_from_path` to
  `BlobObject::new_from_path`.  All the `BlobObject::create*` calls
  now always create new files which is much more consistent.  APIs
  should do what is obious.
2019-12-09 10:30:57 +01:00
Alexander Krotov
a99b96e36e Use bool for from_id_blocked flag 2019-12-09 09:54:46 +01:00
Alexander Krotov
0889467c7b Use bool for hidden flag 2019-12-09 09:54:46 +01:00
Alexander Krotov
d141e228de Use bool for incoming flag 2019-12-09 09:54:46 +01:00
Alexander Krotov
9b15c42801 Use bool for allow_creation flag 2019-12-09 09:54:46 +01:00
Alexander Krotov
a781b631e1 Return bool from is_reply_to_{known_message,messenger_message} 2019-12-09 09:54:46 +01:00
Alexander Krotov
08af5c8e09 Replace some integers with bools in dc_receive_imf.rs 2019-12-09 09:54:46 +01:00
holger krekel
93e8cca02f remove redundant state from smtp 2019-12-08 23:04:08 +01:00
Alexander Krotov
a8e9a1fbe5 Make Smtp.send async 2019-12-08 21:00:03 +01:00
Alexander Krotov
54eb30f3db Switch from lettre to async-smtp 2019-12-08 21:00:03 +01:00
Alexander Krotov
c08a1adc9b Deprecate AcceptInvalidHostnames option
Rustls does not offer a documented way to accept valid certificates with
invalid hostnames. Implementation of certificate verification in Rustls
does not have a public API and reimplementing it is error-prone.
2019-12-08 20:54:04 +01:00
Alexander Krotov
cd951ad396 Improve IMAP idle timeout error handling
Return proper cause and avoid matching on nested Result
2019-12-08 20:50:27 +01:00
holger krekel
33793d878b address @link2xt comment on done/async 2019-12-08 01:13:00 +01:00
holger krekel
357955015d [wip] initial detection of idle done() not working 2019-12-08 01:13:00 +01:00
holger krekel
d6d94adab0 remove encrypted attr from mimeparser. 2019-12-08 00:47:46 +01:00
holger krekel
099cc9f727 remove subject attribute 2019-12-08 00:47:46 +01:00
holger krekel
61f8d6f171 remove is_send_by_messenger attr from mimeparser 2019-12-08 00:47:46 +01:00
Alexander Krotov
a7af4685f1 Log IMAP certificate checks configuration 2019-12-08 00:31:22 +01:00
Alexander Krotov
2cebed4f77 Move dc_array.rs to deltachat-ffi package 2019-12-08 00:30:43 +01:00
holger krekel
3f2a371599 run lint without installing the package 2019-12-07 22:56:54 +01:00
holger krekel
a3ca3d9179 allow to use a different buildhost :) 2019-12-07 22:56:54 +01:00
holger krekel
86ace1a4af - test and fix receiving text/html attachment in multipart/mixed situations
They are now preserved as attachment, instead of diving into parsing-html
  and simplifying.

- adapt mime-debugging
2019-12-07 22:56:54 +01:00
holger krekel
1abdf62045 passes test but needs cleanup 2019-12-07 22:56:54 +01:00
holger krekel
9e2a96675d try fix incoming text/html in multipart/mixed 2019-12-07 22:56:54 +01:00
125 changed files with 11040 additions and 6178 deletions

View File

@@ -36,12 +36,6 @@ jobs:
executor: default
steps:
- checkout
- run:
name: Update submodules
command: git submodule update --init --recursive
- run:
name: Calculate dependencies
command: cargo generate-lockfile
- restore_cache:
keys:
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
@@ -49,7 +43,6 @@ jobs:
- run: rustup default $(cat rust-toolchain)
- run: rustup component add --toolchain $(cat rust-toolchain) rustfmt
- run: rustup component add --toolchain $(cat rust-toolchain) clippy-preview
- run: cargo update
- run: cargo fetch
- run: rustc +stable --version
- run: rustc +$(cat rust-toolchain) --version
@@ -91,7 +84,6 @@ jobs:
curl https://sh.rustup.rs -sSf | sh -s -- -y
- run: rustup install $(cat rust-toolchain)
- run: rustup default $(cat rust-toolchain)
- run: cargo update
- run: cargo fetch
- run:
name: Test
@@ -128,32 +120,19 @@ jobs:
paths:
- c-docs
# build_test_docs_wheel:
# docker:
# - image: deltachat/coredeps
# environment:
# TESTS: 1
# DOCS: 1
# working_directory: /mnt/crate
# steps:
# - *restore-workspace
# - *restore-cache
# - run:
# name: build docs, run tests and build wheels
# command: ci_scripts/run-python.sh
# - run:
# name: copying docs and wheels to workspace
# command: |
# mkdir -p workspace/python
# # cp -av docs workspace/c-docs
# cp -av python/.docker-tox/wheelhouse workspace/
# cp -av python/doc/_build/ workspace/py-docs
# - persist_to_workspace:
# root: workspace
# paths:
# # - c-docs
# - py-docs
# - wheelhouse
remote_python_packaging:
machine: true
steps:
- checkout
# the following commands on success produces
# workspace/{wheelhouse,py-docs} as artefact directories
- run: bash ci_scripts/remote_python_packaging.sh
- persist_to_workspace:
root: workspace
paths:
# - c-docs
- py-docs
- wheelhouse
remote_tests_rust:
machine: true
@@ -165,10 +144,7 @@ jobs:
machine: true
steps:
- checkout
#- attach_workspace:
# at: workspace
- run: ci_scripts/remote_tests_python.sh
# workspace/py-docs workspace/wheelhouse workspace/c-docs
upload_docs_wheels:
machine: true
@@ -176,6 +152,7 @@ jobs:
- checkout
- attach_workspace:
at: workspace
- run: pyenv versions
- run: pyenv global 3.5.2
- run: ls -laR workspace
- run: ci_scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse workspace/c-docs
@@ -187,7 +164,7 @@ jobs:
- *restore-cache
- run:
name: Run cargo clippy
command: cargo clippy --all
command: cargo clippy
workflows:
@@ -195,24 +172,44 @@ workflows:
test:
jobs:
- cargo_fetch
# - cargo_fetch
- remote_tests_rust
- remote_tests_rust:
filters:
tags:
only: /.*/
- remote_tests_python
- remote_tests_python:
filters:
tags:
only: /.*/
# - upload_docs_wheels:
- remote_python_packaging:
requires:
- remote_tests_python
- remote_tests_rust
filters:
tags:
only: /.*/
- upload_docs_wheels:
requires:
- remote_python_packaging
- build_doxygen
filters:
tags:
only: /.*/
# - rustfmt:
# requires:
# - build_test_docs_wheel
# - build_doxygen
- rustfmt:
requires:
- cargo_fetch
- clippy:
requires:
- cargo_fetch
# - cargo_fetch
# - clippy:
# requires:
# - cargo_fetch
- build_doxygen
- build_doxygen:
filters:
tags:
only: /.*/
# Linux Desktop 64bit
# - test_x86_64-unknown-linux-gnu:

48
.github/workflows/code-quality.yml vendored Normal file
View File

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

View File

@@ -1,8 +1,222 @@
# Changelog
## next (pending)
## 1.25.0
- save traffic by downloading only messages that are really displayed #1236
- change generated key type to Ed25519, these keys are much shorter
than RSA keys, which results in saving traffic and speed improvements #1287
- improve key handling #1237 #1240 #1242 #1247
- mute handling, apis are dc_set_chat_mute_duration()
dc_chat_is_muted() and dc_chat_get_remaining_mute_duration() #1143
- pinning chats, new apis are dc_set_chat_visibility() and
dc_chat_get_visibility() #1248
- add dc_provider_new_from_email() api that queries the new, integrated
provider-database #1207
- account creation by scanning a qr code
in the DCACCOUNT scheme (https://mailadm.readthedocs.io),
new api is dc_set_config_from_qr() #1249
- if possible, dc_join_securejoin(), returns the new chat-id immediately
and does the handshake in background #1225
- update imap and smtp dependencies #1115
- check for MOVE capability before using MOVE command #1263
- allow inline attachments from RFC 2183 #1280
- fix updating names from incoming mails #1298
- fix error messages shown on import #1234
- directly attempt to re-connect if the smtp connection is maybe stale #1296
- improve adding group members #1291
- improve rust-api #1261
- cleanup #1302 #1283 #1282 #1276 #1270-#1274 #1267 #1258-#1260
#1257 #1239 #1231 #1224
- update spec #1286 #1291
## 1.0.0-beta.24
- fix oauth2/gmail bug introduced in beta23 (not used in releases) #1219
- fix panic when receiving eg. cyrillic filenames #1216
- delete all consumed secure-join handshake messagess #1209 #1212
- rust-level cleanups #1218 #1217 #1210 #1205
- python-level cleanups #1204 #1202 #1201
## 1.0.0-beta.23
- #1197 fix imap-deletion of messages
- #1171 Combine multiple MDNs into a single mail, reducing traffic
- #1155 fix to not send out gossip always, reducing traffic
- #1160 fix reply-to-encrypted determination
- #1182 Add "Auto-Submitted: auto-replied" header to MDNs
- #1194 produce python wheels again, fix c/py.delta.chat
master-deployment
- rust-level housekeeping and improvements #1161 #1186 #1185 #1190 #1194 #1199 #1191 #1190 #1184 and more
- #1063 clarify licensing
- #1147 use mailparse 0.10.2
## 1.0.0-beta.22
- #1095 normalize email lineends to CRLF
- #1095 enable link-time-optimization, saves eg. on android 11 mb
- #1099 fix import regarding devicechats
- #1092 improve logging
- #1096 #1097 #1094 #1090 #1091 internal cleanups
## 1.0.0-beta.21
- #1078 #1082 ensure RFC compliance by producing 78 column lines for
encoded attachments.
- #1080 don't recreate and thus break group membership if an unknown
sender (or mailer-daemon) sends a message referencing the group chat
- #1081 #1079 some internal cleanups
- update imap-proto dependency, to fix yandex/oauth
## 1.0.0-beta.20
- #1074 fix OAUTH2/gmail
- #1072 fix group members not appearing in contact list
- #1071 never block interrupt_idle (thus hopefully also not on maybe_network())
- #1069 reduce smtp-timeout to 30 seconds
- #1066 #1065 avoid unwrap in dehtml, make literals more readable
## 1.0.0-beta.19
- #1058 timeout smtp-send if it doesn't complete in 15 minutes
- #1059 trim down logging
## 1.0.0-beta.18
- #1056 avoid panicking when we couldn't read imap-server's greeting
message
- #1055 avoid panicking when we don't have a selected folder
- #1052 #1049 #1051 improve logging to add thread-id/name and
file/lineno to each info/warn message.
- #1050 allow python bindings to initialize Account with "os_name".
## 1.0.0-beta.17
- #1044 implement avatar recoding to 192x192 in core to keep file sizes small.
- #1024 fix #1021 SQL/injection malformed Chat-Group-Name breakage
- #1036 fix smtp crash by pulling in a fixed async-smtp
- #1039 fix read-receipts appearing as normal messages when you change
MDN settings
- #1040 do not panic on SystemTimeDifference
- #1043 avoid potential crashes in malformed From/Chat-Disposition... headers
- #1045 #1041 #1038 #1035 #1034 #1029 #1025 various cleanups and doc
improvments
## 1.0.0-beta.16
- alleviate login problems with providers which only
support RSA1024 keys by switching back from Rustls
to native-tls, by using the new async-email/async-native-tls
crate from @dignifiedquire. thanks @link2xt.
- introduce per-contact profile images to send out
own profile image heuristically, and fix sending
out of profile images in "in-prepare" groups.
this also extends the Chat-spec that is maintained
in core to specify Chat-Group-Image and Chat-Group-Avatar
headers. thanks @r10s and @hpk42.
- fix merging of protected headers from the encrypted
to the unencrypted parts, now not happening recursively
anymore. thanks @hpk and @r10s
- fix/optimize autocrypt gossip headers to only get
sent when there are more than 2 people in a chat.
thanks @link2xt
- fix displayname to use the authenticated name
when available (displayname as coming from contacts
themselves). thanks @simon-laux
- introduce preliminary support for offline autoconfig
for nauta provider. thanks @hpk42 @r10s
## 1.0.0-beta.15
- fix #994 attachment appeared doubled in chats (and where actually
downloaded after smtp-send). @hpk42
## 1.0.0-beta.14
- fix packaging issue with our rust-email fork, now we are tracking
master again there. hpk42
## 1.0.0-beta.13
- fix #976 -- unicode-issues in display-name of email addresses. @hpk42
- fix #985 group add/remove member bugs resulting in broken groups. @hpk42
- fix hanging IMAP connections -- we now detect with a 15second timeout
if we cannot terminate the IDLE IMAP protocol. @hpk42 @link2xt
- fix incoming multipart/mixed containing html, to show up as
attachments again. Fixes usage for simplebot which sends html
files for users to interact with the bot. @adbenitez @hpk42
- refinements to internal autocrypt-handling code, do not send
prefer-encrypt=nopreference as it is the default if no attribute
is present. @linkxt
- simplify, modularize and rustify several parts
of dc-core (general WIP). @link2xt @flub @hpk42 @r10s
- use async-email/async-smtp to handle SMTP connections, might
fix connection/reconnection issues. @link2xt
- more tests and refinements for dealing with blobstorage @flub @hpk42
- use a dedicated build-server for CI testing of core PRs
- restructured (but not change) imap idle handling into own file. cc @link2xt
## 1.0.0-beta.12

2020
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,31 @@
[package]
name = "deltachat"
version = "1.0.0-beta.12"
version = "1.25.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL"
license = "MPL-2.0"
[profile.release]
lto = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
libc = "0.2.51"
pgp = { git = "https://github.com/rpgp/rpgp", branch = "master", default-features = false }
pgp = { version = "0.4.0", default-features = false }
hex = "0.4.0"
sha2 = "0.8.0"
rand = "0.6.5"
smallvec = "0.6.9"
reqwest = { version = "0.9.15", default-features = false, features = ["rustls-tls"] }
num-derive = "0.2.5"
rand = "0.7.0"
smallvec = "1.0.0"
reqwest = { version = "0.10.0", features = ["blocking", "json"] }
num-derive = "0.3.0"
num-traits = "0.2.6"
lettre = { git = "https://github.com/deltachat/lettre", branch = "feat/mail" }
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "feat/mail" }
async-imap = { git = "https://github.com/async-email/async-imap", branch="master" }
async-tls = "0.6"
async-std = { version = "1.0", features = ["unstable"] }
async-smtp = "0.2"
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
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"
@@ -30,12 +34,11 @@ serde_json = "1.0"
chrono = "0.4.6"
failure = "0.1.5"
failure_derive = "0.1.5"
# TODO: make optional
rustyline = "4.1.0"
indexmap = "1.3.0"
lazy_static = "1.4.0"
regex = "1.1.6"
rusqlite = { version = "0.20", features = ["bundled"] }
r2d2_sqlite = "0.12.0"
rusqlite = { version = "0.21", features = ["bundled"] }
r2d2_sqlite = "0.13.0"
r2d2 = "0.8.5"
strum = "0.16.0"
strum_macros = "0.16.0"
@@ -44,17 +47,19 @@ backtrace = "0.3.33"
byteorder = "1.3.1"
itertools = "0.8.0"
image-meta = "0.1.0"
quick-xml = "0.15.0"
quick-xml = "0.17.1"
escaper = "0.1.0"
bitflags = "1.1.0"
debug_stub_derive = "0.3.0"
sanitize-filename = "0.2.1"
stop-token = { version = "0.1.1", features = ["unstable"] }
rustls = "0.16.0"
webpki-roots = "0.18.0"
webpki = "0.21.0"
mailparse = "0.10.1"
mailparse = "0.10.2"
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"
@@ -75,10 +80,11 @@ path = "examples/simple.rs"
[[example]]
name = "repl"
path = "examples/repl/main.rs"
required-features = ["rustyline"]
[features]
default = ["nightly", "ringbuf"]
vendored = []
vendored = ["async-native-tls/vendored", "reqwest/native-tls-vendored", "async-smtp/native-tls-vendored"]
nightly = ["pgp/nightly"]
ringbuf = ["pgp/ringbuf"]

View File

@@ -2,9 +2,6 @@ The files in this directory and under its subdirectories
are (c) 2019 by Bjoern Petersen and contributors and released under the
Mozilla Public License Version 2.0, see below for a copy.
NOTE that the files in the "libs" directory are copyrighted by third parties
and come with their own respective licenses.
Mozilla Public License Version 2.0
==================================

View File

@@ -114,3 +114,22 @@ $ cargo test -- --ignored
[circle]: https://circleci.com/gh/deltachat/deltachat-core-rust/
[appveyor-shield]: https://ci.appveyor.com/api/projects/status/lqpegel3ld4ipxj8/branch/master?style=flat-square
[appveyor]: https://ci.appveyor.com/project/dignifiedquire/deltachat-core-rust/branch/master
## Language bindings and frontend projects
Language bindings are available for:
- [C](https://c.delta.chat)
- [Node.js](https://www.npmjs.com/package/deltachat-node)
- [Python](https://py.delta.chat)
- [Go](https://github.com/hugot/go-deltachat/)
- **Java** and **Swift** (contained in the Android/iOS repos)
The following "frontend" projects make use of the Rust-library
or its language bindings:
- [Android](https://github.com/deltachat/deltachat-android)
- [iOS](https://github.com/deltachat/deltachat-ios)
- [Desktop](https://github.com/deltachat/deltachat-desktop)
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
- several **Bots**

View File

@@ -8,7 +8,6 @@ install:
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin
- rustc -vV
- cargo -vV
- cargo update
build: false

View File

@@ -14,24 +14,22 @@ DOXYDOCDIR=${3:?directory where doxygen docs to be found}
export BRANCH=${CIRCLE_BRANCH:?specify branch for uploading purposes}
# DISABLED: python docs to py.delta.chat
#ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@py.delta.chat mkdir -p build/${BRANCH}
#rsync -avz \
# -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
# "$PYDOCDIR/html/" \
# delta@py.delta.chat:build/${BRANCH}
# python docs to py.delta.chat
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@py.delta.chat mkdir -p build/${BRANCH}
rsync -avz \
--delete \
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
"$PYDOCDIR/html/" \
delta@py.delta.chat:build/${BRANCH}
# C docs to c.delta.chat
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@c.delta.chat mkdir -p build-c/${BRANCH}
rsync -avz \
--delete \
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
"$DOXYDOCDIR/html/" \
delta@c.delta.chat:build-c/${BRANCH}
exit 0
# OUTDATED -- for re-use from python release-scripts
echo -----------------------
echo upload wheels
echo -----------------------
@@ -39,6 +37,7 @@ echo -----------------------
# Bundle external shared libraries into the wheels
pushd $WHEELHOUSEDIR
pip3 install -U pip
pip3 install devpi-client
devpi use https://m.devpi.net
devpi login dc --password $DEVPI_LOGIN
@@ -50,6 +49,9 @@ devpi use dc/$N_BRANCH || {
devpi use dc/$N_BRANCH
}
devpi index $N_BRANCH bases=/root/pypi
devpi upload deltachat*.whl
devpi upload deltachat*
popd
# remove devpi non-master dc indices if thy are too old
python ci_scripts/cleanup_devpi_indices.py

View File

@@ -0,0 +1,69 @@
"""
Remove old "dc" indices except for master which always stays.
"""
from requests import Session
import datetime
import sys
import subprocess
MAXDAYS=7
session = Session()
session.headers["Accept"] = "application/json"
def get_indexes(baseurl, username):
response = session.get(baseurl + username)
assert response.status_code == 200
result = response.json()["result"]
return result["indexes"]
def get_projectnames(baseurl, username, indexname):
response = session.get(baseurl + username + "/" + indexname)
assert response.status_code == 200
result = response.json()["result"]
return result["projects"]
def get_release_dates(baseurl, username, indexname, projectname):
response = session.get(baseurl + username + "/" + indexname + "/" + projectname)
assert response.status_code == 200
result = response.json()["result"]
dates = set()
for value in result.values():
if "+links" not in value:
continue
for link in value["+links"]:
for log in link["log"]:
dates.add(tuple(log["when"]))
return dates
def run():
baseurl = "https://m.devpi.net/"
username = "dc"
for indexname in get_indexes(baseurl, username):
projectnames = get_projectnames(baseurl, username, indexname)
if indexname == "master" or not indexname:
continue
assert projectnames == ["deltachat"]
for projectname in projectnames:
dates = get_release_dates(baseurl, username, indexname, projectname)
if not dates:
print(
"%s has no releases" % (baseurl + username + "/" + indexname),
file=sys.stderr)
date = datetime.datetime.now()
else:
date = datetime.datetime(*max(dates))
if (datetime.datetime.now() - date) > datetime.timedelta(days=MAXDAYS):
assert username and indexname
url = baseurl + username + "/" + indexname
subprocess.check_call(["devpi", "index", "-y", "--delete", url])
if __name__ == '__main__':
run()

View File

@@ -5,14 +5,11 @@ RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf
ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig
# Install a recent Perl, needed to install OpenSSL
# Install a recent Perl, needed to install the openssl crate
ADD deps/build_perl.sh /builder/build_perl.sh
RUN rm /usr/bin/perl
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
# Install OpenSSL
ADD deps/build_openssl.sh /builder/build_openssl.sh
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_openssl.sh && cd .. && rm -r tmp1
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
# Install python tools (auditwheels,tox, ...)

View File

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

View File

@@ -1,49 +0,0 @@
#!/bin/bash
#
# Build the Delta Chat C/Rust library
#
set -e -x
# perform clean build of core and install
export TOXWORKDIR=.docker-tox
# build core library
cargo build --release -p deltachat_ffi
# configure access to a base python and
# to several python interpreters needed by tox below
export PATH=$PATH:/opt/python/cp35-cp35m/bin
export PYTHONDONTWRITEBYTECODE=1
pushd /bin
ln -s /opt/python/cp27-cp27m/bin/python2.7
ln -s /opt/python/cp36-cp36m/bin/python3.6
ln -s /opt/python/cp37-cp37m/bin/python3.7
popd
#
# run python tests
#
if [ -n "$TESTS" ]; then
echo ----------------
echo run python tests
echo ----------------
pushd python
# first run all tests ...
rm -rf tests/__pycache__
rm -rf src/deltachat/__pycache__
export PYTHONDONTWRITEBYTECODE=1
tox --workdir "$TOXWORKDIR" -e py27,py35,py36,py37
popd
fi
if [ -n "$DOCS" ]; then
echo -----------------------
echo generating python docs
echo -----------------------
(cd python && tox --workdir "$TOXWORKDIR" -e doc)
fi

View File

@@ -0,0 +1,51 @@
#!/bin/bash
export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
set -xe
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
git ls-files >.rsynclist
# we seem to need .git for setuptools_scm versioning
find .git >>.rsynclist
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
set +x
# we have to create a remote file for the remote-docker run
# so we can do a simple ssh command with a TTY
# so that when our job dies, all container-runs are aborted.
# sidenote: the circle-ci machinery will kill ongoing jobs
# if there are new commits and we want to ensure that
# everything is terminated/cleaned up and we have no orphaned
# useless still-running docker-containers consuming resources.
ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
set +x -e
cd $BUILDDIR
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
set -x
# run everything else inside docker
docker run -e DCC_PY_LIVECONFIG \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh
_HERE
echo "--- Running $CIRCLE_JOB remotely"
ssh -t $SSHTARGET bash "$BUILDDIR/exec_docker_run"
mkdir -p workspace
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse/*manylinux1*" workspace/wheelhouse/
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/dist/*" workspace/wheelhouse/
rsync -avz "$SSHTARGET:$BUILDDIR/python/doc/_build/" workspace/py-docs

View File

@@ -2,7 +2,7 @@
export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export SSHTARGET=ci@b1.delta.chat
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")

View File

@@ -2,7 +2,7 @@
export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export SSHTARGET=ci@b1.delta.chat
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")

View File

@@ -3,5 +3,4 @@
set -ex
cd deltachat-ffi
doxygen
PROJECT_NUMBER=$(git log -1 --format "%h (%cd)") doxygen

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

48
ci_scripts/run_all.sh Executable file
View File

@@ -0,0 +1,48 @@
#!/bin/bash
#
# Build the Delta Chat Core Rust library, Python wheels and docs
set -e -x
# Perform clean build of core and install.
export TOXWORKDIR=.docker-tox
# compile core lib
export PATH=/root/.cargo/bin:$PATH
cargo build --release -p deltachat_ffi
# cargo test --all --all-features
# Statically link against libdeltachat.a.
export DCC_RS_DEV=$(pwd)
# Configure access to a base python and to several python interpreters
# needed by tox below.
export PATH=$PATH:/opt/python/cp35-cp35m/bin
export PYTHONDONTWRITEBYTECODE=1
pushd /bin
ln -s /opt/python/cp27-cp27m/bin/python2.7
ln -s /opt/python/cp36-cp36m/bin/python3.6
ln -s /opt/python/cp37-cp37m/bin/python3.7
ln -s /opt/python/cp38-cp38/bin/python3.8
popd
pushd python
# prepare a clean tox run
rm -rf tests/__pycache__
rm -rf src/deltachat/__pycache__
mkdir -p $TOXWORKDIR
# disable live-account testing to speed up test runs and wheel building
# XXX we may switch on some live-tests on for better ensurances
# Note that the independent remote_tests_python step does all kinds of
# live-testing already.
unset DCC_PY_LIVECONFIG
tox --workdir "$TOXWORKDIR" -e py35,py36,py37,py38,auditwheels
popd
echo -----------------------
echo generating python docs
echo -----------------------
(cd python && tox --workdir "$TOXWORKDIR" -e doc)

View File

@@ -1,11 +1,11 @@
[package]
name = "deltachat_ffi"
version = "1.0.0-beta.12"
version = "1.25.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
readme = "README.md"
license = "MIT OR Apache-2.0"
license = "MPL-2.0"
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
categories = ["cryptography", "std", "email"]
@@ -16,11 +16,11 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
deltachat = { path = "../", default-features = false }
deltachat-provider-database = "0.2.1"
libc = "0.2"
human-panic = "1.0.1"
num-traits = "0.2.6"
failure = "0.1.6"
serde_json = "1.0"
[features]
default = ["vendored", "nightly", "ringbuf"]

View File

@@ -364,6 +364,12 @@ char* dc_get_blobdir (const dc_context_t* context);
* also show all mails of confirmed contacts,
* DC_SHOW_EMAILS_ALL (2)=
* also show mails of unconfirmed contacts in the deaddrop.
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
* generate recommended key type (default),
* DC_KEY_GEN_RSA2048 (1)=
* generate RSA 2048 keypair
* DC_KEY_GEN_ED25519 (2)=
* generate Ed25519 keypair
* - `save_mime_headers` = 1=save mime headers
* and make dc_get_mime_headers() work for subsequent calls,
* 0=do not save mime headers (default)
@@ -403,20 +409,37 @@ 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.
* Set stock string translation.
*
* The function will emit warnings if it returns an error state.
* The function will emit warnings if it returns an error state.
*
* @memberof dc_context_t
* @param context The context object
* @param stock_id the integer id of the stock message (DC_STR_*)
* @param stock_msg the message to be used
* @param stock_msg the message to be used
* @return int (==0 on error, 1 on success)
*/
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 +875,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
@@ -880,7 +924,7 @@ void dc_maybe_network (dc_context_t* context);
* or "Not now".
* The UI can also offer a "Close" button that calls dc_marknoticed_contact() then.
* - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
* archived _any_ chat using dc_archive_chat(). The UI should show a link as
* archived _any_ chat using dc_set_chat_visibility(). The UI should show a link as
* "Show archived chats", if the user clicks this item, the UI should show a
* list of all archived chats that can be created by this function hen using
* the DC_GCL_ARCHIVED_ONLY flag.
@@ -1334,25 +1378,18 @@ uint32_t dc_get_next_media (dc_context_t* context, uint32_t ms
/**
* Archive or unarchive a chat.
* Set chat visibility to pinned, archived or normal.
*
* Archived chats are not included in the default chatlist returned
* by dc_get_chatlist(). Instead, if there are _any_ archived chats,
* the pseudo-chat with the chat_id DC_CHAT_ID_ARCHIVED_LINK will be added the the
* end of the chatlist.
*
* - To get a list of archived chats, use dc_get_chatlist() with the flag DC_GCL_ARCHIVED_ONLY.
* - To find out the archived state of a given chat, use dc_chat_get_archived()
* - Messages in archived chats are marked as being noticed, so they do not count as "fresh"
* - Calling this function usually results in the event #DC_EVENT_MSGS_CHANGED
* Calling this function usually results in the event #DC_EVENT_MSGS_CHANGED
* See @ref DC_CHAT_VISIBILITY for detailed information about the visibilities.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat to archive or unarchive.
* @param archive 1=archive chat, 0=unarchive chat, all other values are reserved for future use
* @param chat_id The ID of the chat to change the visibility for.
* @param visibility one of @ref DC_CHAT_VISIBILITY
* @return None.
*/
void dc_archive_chat (dc_context_t* context, uint32_t chat_id, int archive);
void dc_set_chat_visibility (dc_context_t* context, uint32_t chat_id, int visibility);
/**
@@ -1556,6 +1593,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
/**
@@ -1603,7 +1656,7 @@ char* dc_get_mime_headers (dc_context_t* context, uint32_t ms
void dc_delete_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
/**
* Empty IMAP server folder: delete all messages.
* Empty IMAP server folder: delete all messages.
*
* @memberof dc_context_t
* @param context The context object as created by dc_context_new()
@@ -1756,7 +1809,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 +2127,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 +2145,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 +2161,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 +2185,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 +2215,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);
@@ -2618,18 +2693,18 @@ dc_context_t* dc_chatlist_get_context (dc_chatlist_t* chatlist);
/**
* Get info summary for a chat, in json format.
* Get info summary for a chat, in json format.
*
* The returned json string has the following key/values:
* The returned json string has the following key/values:
*
* id: chat id
* name: chat/group name
* color: color of this chat
* last-message-from: who sent the last message
* last-message-text: message (truncated)
* id: chat id
* name: chat/group name
* color: color of this chat
* last-message-from: who sent the last message
* last-message-text: message (truncated)
* last-message-state: DC_STATE* constant
* last-message-date:
* avatar-path: path-to-blobfile
* last-message-date:
* avatar-path: path-to-blobfile
* is_verified: yes/no
* @return a utf8-encoded json string containing all requested info. Must be freed using dc_str_unref(). NULL is never returned.
@@ -2770,21 +2845,14 @@ uint32_t dc_chat_get_color (const dc_chat_t* chat);
/**
* Get archived state.
*
* - 0 = normal chat, not archived, not sticky.
* - 1 = chat archived
* - 2 = chat sticky (reserved for future use, if you do not support this value, just treat the chat as a normal one)
*
* To archive or unarchive chats, use dc_archive_chat().
* If chats are archived, this should be shown in the UI by a little icon or text,
* eg. the search will also return archived chats.
* Get visibility of chat.
* See @ref DC_CHAT_VISIBILITY for detailed information about the visibilities.
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return Archived state.
* @return One of @ref DC_CHAT_VISIBILITY
*/
int dc_chat_get_archived (const dc_chat_t* chat);
int dc_chat_get_visibility (const dc_chat_t* chat);
/**
@@ -2877,6 +2945,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
*
@@ -2989,15 +3077,15 @@ int dc_msg_get_viewtype (const dc_msg_t* msg);
* Get the state of a message.
*
* Incoming message states:
* - DC_STATE_IN_FRESH (10) - Incoming _fresh_ message. Fresh messages are not noticed nor seen and are typically shown in notifications. Use dc_get_fresh_msgs() to get all fresh messages.
* - DC_STATE_IN_NOTICED (13) - Incoming _noticed_ message. Eg. chat opened but message not yet read - noticed messages are not counted as unread but did not marked as read nor resulted in MDNs. Use dc_marknoticed_chat() or dc_marknoticed_contact() to mark messages as being noticed.
* - DC_STATE_IN_SEEN (16) - Incoming message, really _seen_ by the user. Marked as read on IMAP and MDN may be send. Use dc_markseen_msgs() to mark messages as being seen.
* - DC_STATE_IN_FRESH (10) - Incoming _fresh_ message. Fresh messages are neither noticed nor seen and are typically shown in notifications. Use dc_get_fresh_msgs() to get all fresh messages.
* - DC_STATE_IN_NOTICED (13) - Incoming _noticed_ message. E.g. chat opened but message not yet read - noticed messages are not counted as unread but did not marked as read nor resulted in MDNs. Use dc_marknoticed_chat() or dc_marknoticed_contact() to mark messages as being noticed.
* - DC_STATE_IN_SEEN (16) - Incoming message, really _seen_ by the user. Marked as read on IMAP and MDN may be sent. Use dc_markseen_msgs() to mark messages as being seen.
*
* Outgoing message states:
* - DC_STATE_OUT_PREPARING (18) - For files which need time to be prepared before they can be sent,
* the message enters this state before DC_STATE_OUT_PENDING.
* - DC_STATE_OUT_DRAFT (19) - Message saved as draft using dc_set_draft()
* - DC_STATE_OUT_PENDING (20) - The user has send the "send" button but the
* - DC_STATE_OUT_PENDING (20) - The user has pressed the "send" button but the
* message is not yet sent and is pending in some way. Maybe we're offline (no checkmark).
* - DC_STATE_OUT_FAILED (24) - _Unrecoverable_ error (_recoverable_ errors result in pending messages), you'll receive the event #DC_EVENT_MSG_FAILED.
* - DC_STATE_OUT_DELIVERED (26) - Outgoing message successfully delivered to server (one checkmark). Note, that already delivered messages may get into the state DC_STATE_OUT_FAILED if we get such a hint from the server.
@@ -3645,11 +3733,15 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
/**
* Same as dc_contact_is_verified() but allows speeding up things
* by adding the peerstate belonging to the contact.
* If you do not have the peerstate available, it is loaded automatically.
* Check if a contact was verified. E.g. by a secure-join QR code scan
* and if the key has not changed since this verification.
*
* @private @memberof dc_context_t
* The UI may draw a checkbox or something like that beside verified contacts.
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 0: contact is not verified.
* 2: SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown.
*/
int dc_contact_is_verified (dc_contact_t* contact);
@@ -3661,30 +3753,19 @@ int dc_contact_is_verified (dc_contact_t* contact);
*/
/**
* Create a provider struct for the given domain.
*
* @memberof dc_provider_t
* @param domain The domain to get provider info for.
* @return a dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
*/
dc_provider_t* dc_provider_new_from_domain (const char* domain);
/**
* Create a provider struct for the given email address.
*
* The provider is extracted from the email address and it's information is returned.
*
* @memberof dc_provider_t
* @param context The context object as created by dc_context_new().
* @param email The user's email address to extract the provider info form.
* @return a dc_provider_t struct which can be used with the dc_provider_get_*
* accessor functions. If no provider info is found, NULL will be
* returned.
*/
dc_provider_t* dc_provider_new_from_email (const char* email);
dc_provider_t* dc_provider_new_from_email (const dc_context_t* context, const char* email);
/**
@@ -3694,53 +3775,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.
@@ -3755,7 +3818,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);
/**
@@ -4050,11 +4113,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
#define DC_CERTCK_STRICT 1
/**
* Accept invalid hostnames, but not invalid certificates.
*/
#define DC_CERTCK_ACCEPT_INVALID_HOSTNAMES 2
/**
* Accept invalid certificates, including self-signed ones
* or having incorrect hostname.
@@ -4168,30 +4226,30 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_EVENT_IMAP_MESSAGE_MOVED 105
/**
* Emitted when an IMAP folder was emptied.
* Emitted when an IMAP folder was emptied.
*
* @param data1 0
* @param data2 (const char*) folder name.
* @param data2 (const char*) folder name.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_IMAP_FOLDER_EMPTIED 106
/**
* Emitted when a new blob file was successfully written
* Emitted when a new blob file was successfully written
*
* @param data1 0
* @param data2 (const char*) path name
* @param data2 (const char*) path name
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_NEW_BLOB_FILE 150
/**
* Emitted when a blob file was successfully deleted
* Emitted when a blob file was successfully deleted
*
* @param data1 0
* @param data2 (const char*) path name
* @param data2 (const char*) path name
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
@@ -4348,7 +4406,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
@@ -4466,6 +4524,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_EVENT_RETURNS_STRING(e) ((e)==DC_EVENT_GET_STRING) // not used anymore
char* dc_get_version_str (void); // deprecated
void dc_array_add_id (dc_array_t*, uint32_t); // deprecated
#define dc_archive_chat(a,b,c) dc_set_chat_visibility((a), (b), (c)? 1 : 0) // not used anymore
#define dc_chat_get_archived(a) (dc_chat_get_visibility((a))==1? 1 : 0) // not used anymore
/*
@@ -4475,6 +4535,13 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
#define DC_SHOW_EMAILS_ACCEPTED_CONTACTS 1
#define DC_SHOW_EMAILS_ALL 2
/*
* Values for dc_get|set_config("key_gen_type")
*/
#define DC_KEY_GEN_DEFAULT 0
#define DC_KEY_GEN_RSA2048 1
#define DC_KEY_GEN_ED25519 2
/**
* @defgroup DC_PROVIDER_STATUS DC_PROVIDER_STATUS
@@ -4486,23 +4553,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
@@ -4511,6 +4598,48 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
*/
/**
* @defgroup DC_CHAT_VISIBILITY DC_CHAT_VISIBILITY
*
* These constants describe the visibility of a chat.
* The chat visibiliry can be get using dc_chat_get_visibility()
* and set using dc_set_chat_visibility().
*
* @addtogroup DC_CHAT_VISIBILITY
* @{
*/
/**
* Chats with normal visibility are not archived and are shown below all pinned chats.
* Archived chats, that receive new messages automatically become normal chats.
*/
#define DC_CHAT_VISIBILITY_NORMAL 0
/**
* Archived chats are not included in the default chatlist returned by dc_get_chatlist().
* Instead, if there are _any_ archived chats, the pseudo-chat
* with the chat_id DC_CHAT_ID_ARCHIVED_LINK will be added the the end of the chatlist.
*
* The UI typically shows a little icon or chats beside archived chats in the chatlist,
* this is needed as eg. the search will also return archived chats.
*
* If archived chats receive new messages, they become normal chats again.
*
* To get a list of archived chats, use dc_get_chatlist() with the flag DC_GCL_ARCHIVED_ONLY.
*/
#define DC_CHAT_VISIBILITY_ARCHIVED 1
/**
* Pinned chats are included in the default chatlist. moreover,
* they are always the first items, whether they have fresh messages or not.
*/
#define DC_CHAT_VISIBILITY_PINNED 2
/**
* @}
*/
/*
* TODO: Strings need some doumentation about used placeholders.
*

View File

@@ -2,7 +2,6 @@ use crate::location::Location;
/* * the structure behind dc_array_t */
#[derive(Debug, Clone)]
#[allow(non_camel_case_types)]
pub enum dc_array_t {
Locations(Vec<Location>),
Uint(Vec<u32>),

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,5 +1,6 @@
use failure::Fail;
use std::ffi::{CStr, CString};
use std::ptr;
/// Duplicates a string
///
@@ -144,7 +145,7 @@ pub trait CStringExt {
impl CStringExt for CString {}
/// Convenience methods to make transitioning from raw C strings easier.
/// Convenience methods to turn strings into C strings.
///
/// To interact with (legacy) C APIs we often need to convert from
/// Rust strings to raw C strings. This can be clumsy to do correctly
@@ -174,6 +175,35 @@ impl<T: AsRef<str>> StrExt for T {
}
}
/// Convenience methods to turn optional strings into C strings.
///
/// This is the same as the [StrExt] trait but a different trait name
/// to work around the type system not allowing to implement [StrExt]
/// for `Option<impl StrExt>` When we already have an [StrExt] impl
/// for `AsRef<&str>`.
///
/// When the [Option] is [Option::Some] this behaves just like
/// [StrExt::strdup], when it is [Option::None] a null pointer is
/// returned.
pub trait OptStrExt {
/// Allocate a new raw C `*char` version of this string, or NULL.
///
/// See [StrExt::strdup] for details.
unsafe fn strdup(&self) -> *mut libc::c_char;
}
impl<T: AsRef<str>> OptStrExt for Option<T> {
unsafe fn strdup(&self) -> *mut libc::c_char {
match self {
Some(s) => {
let tmp = CString::yolo(s.as_ref());
dc_strdup(tmp.as_ptr())
}
None => ptr::null_mut(),
}
}
}
pub fn to_string_lossy(s: *const libc::c_char) -> String {
if s.is_null() {
return "".into();
@@ -347,4 +377,19 @@ mod tests {
assert_eq!(cmp, 0);
}
}
#[test]
fn test_strdup_opt_string() {
unsafe {
let s = Some("hello");
let c = s.strdup();
let cmp = strcmp(c, b"hello\x00" as *const u8 as *const libc::c_char);
free(c as *mut libc::c_void);
assert_eq!(cmp, 0);
let s: Option<&str> = None;
let c = s.strdup();
assert_eq!(c, ptr::null_mut());
}
}
}

View File

@@ -1,12 +1,13 @@
[package]
name = "deltachat_derive"
version = "0.1.0"
authors = ["Dmitry Bogatov <KAction@debian.org>"]
version = "2.0.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
[lib]
proc-macro = true
[dependencies]
syn = "0.14.4"
quote = "0.6.3"
syn = "1.0.13"
quote = "1.0.2"

View File

@@ -1,9 +1,8 @@
use std::path::Path;
use std::str::FromStr;
use deltachat::chat::{self, Chat};
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::constants::*;
use deltachat::contact::*;
use deltachat::context::*;
@@ -19,15 +18,16 @@ use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
use deltachat::Event;
use deltachat::{config, provider};
/// Reset database tables. This function is called from Core cmdline.
/// Reset database tables.
/// Argument is a bitmask, executing single or multiple actions in one call.
/// e.g. bitmask 7 triggers actions definded with bits 1, 2 and 4.
pub fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
info!(context, "Resetting tables ({})...", bits);
fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
println!("Resetting tables ({})...", bits);
if 0 != bits & 1 {
sql::execute(context, &context.sql, "DELETE FROM jobs;", params![]).unwrap();
info!(context, "(1) Jobs reset.");
println!("(1) Jobs reset.");
}
if 0 != bits & 2 {
sql::execute(
@@ -37,11 +37,11 @@ pub fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
params![],
)
.unwrap();
info!(context, "(2) Peerstates reset.");
println!("(2) Peerstates reset.");
}
if 0 != bits & 4 {
sql::execute(context, &context.sql, "DELETE FROM keypairs;", params![]).unwrap();
info!(context, "(4) Private keypairs reset.");
println!("(4) Private keypairs reset.");
}
if 0 != bits & 8 {
sql::execute(
@@ -80,11 +80,11 @@ pub fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
)
.unwrap();
sql::execute(context, &context.sql, "DELETE FROM leftgrps;", params![]).unwrap();
info!(context, "(8) Rest but server config reset.");
println!("(8) Rest but server config reset.");
}
context.call_cb(Event::MsgsChanged {
chat_id: 0,
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
@@ -94,7 +94,9 @@ pub fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
fn dc_poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), Error> {
let data = dc_read_file(context, filename)?;
dc_receive_imf(context, &data, "import", 0, 0);
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false) {
println!("dc_receive_imf errored: {:?}", err);
}
Ok(())
}
@@ -153,7 +155,7 @@ fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int {
let name = name_f.to_string_lossy();
if name.ends_with(".eml") {
let path_plus_name = format!("{}/{}", &real_spec, name);
info!(context, "Import: {}", path_plus_name);
println!("Import: {}", path_plus_name);
if dc_poke_eml_file(context, path_plus_name).is_ok() {
read_cnt += 1
}
@@ -161,13 +163,10 @@ fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int {
}
}
}
info!(
context,
"Import: {} items read from \"{}\".", read_cnt, &real_spec
);
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
if read_cnt > 0 {
context.call_cb(Event::MsgsChanged {
chat_id: 0,
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
}
@@ -188,8 +187,7 @@ fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
};
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
let msgtext = msg.get_text();
info!(
context,
println!(
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{} [{}]",
prefix.as_ref(),
msg.get_id(),
@@ -223,16 +221,14 @@ fn log_msglist(context: &Context, msglist: &Vec<MsgId>) -> Result<(), Error> {
let mut lines_out = 0;
for &msg_id in msglist {
if msg_id.is_daymarker() {
info!(
context,
println!(
"--------------------------------------------------------------------------------"
);
lines_out += 1
} else if !msg_id.is_special() {
if lines_out == 0 {
info!(
context,
println!(
"--------------------------------------------------------------------------------",
);
lines_out += 1
@@ -242,8 +238,7 @@ fn log_msglist(context: &Context, msglist: &Vec<MsgId>) -> Result<(), Error> {
}
}
if lines_out > 0 {
info!(
context,
println!(
"--------------------------------------------------------------------------------"
);
}
@@ -293,7 +288,7 @@ fn log_contactlist(context: &Context, contacts: &Vec<u32>) {
);
}
info!(context, "Contact#{}: {}{}", contact_id, line, line2);
println!("Contact#{}: {}{}", contact_id, line, line2);
}
}
}
@@ -304,7 +299,7 @@ fn chat_prefix(chat: &Chat) -> &'static str {
pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let chat_id = *context.cmdline_sel_chat_id.read().unwrap();
let mut sel_chat = if chat_id > 0 {
let mut sel_chat = if !chat_id.is_unset() {
Chat::load_from_db(context, chat_id).ok()
} else {
None
@@ -376,6 +371,8 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
listmedia\n\
archive <chat-id>\n\
unarchive <chat-id>\n\
pin <chat-id>\n\
unpin <chat-id>\n\
delchat <chat-id>\n\
===========================Message commands==\n\
listmsgs <query>\n\
@@ -397,6 +394,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\
@@ -489,7 +487,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
println!("{:#?}", context.get_info());
}
"interrupt" => {
interrupt_inbox_idle(context, true);
interrupt_inbox_idle(context);
}
"maybenetwork" => {
maybe_network(context);
@@ -508,23 +506,26 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let cnt = chatlist.len();
if cnt > 0 {
info!(
context,
println!(
"================================================================================"
);
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?;
info!(
context,
"{}#{}: {} [{} fresh]",
println!(
"{}#{}: {} [{} fresh] {}",
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
chat::get_fresh_msg_cnt(context, chat.get_id()),
chat.get_id().get_fresh_msg_cnt(context),
match chat.visibility {
ChatVisibility::Normal => "",
ChatVisibility::Archived => "📦",
ChatVisibility::Pinned => "📌",
},
);
let lot = chatlist.get_summary(context, i, Some(&chat));
let statestr = if chat.is_archived() {
let statestr = if chat.visibility == ChatVisibility::Archived {
" [Archived]"
} else {
match lot.get_state() {
@@ -538,8 +539,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let timestr = dc_timestamp_to_str(lot.get_timestamp());
let text1 = lot.get_text1();
let text2 = lot.get_text2();
info!(
context,
println!(
"{}{}{}{} [{}]{}",
text1.unwrap_or(""),
if text1.is_some() { ": " } else { "" },
@@ -552,14 +552,13 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
""
},
);
info!(
context,
println!(
"================================================================================"
);
}
}
if location::is_sending_locations_to_chat(context, 0) {
info!(context, "Location streaming enabled.");
if location::is_sending_locations_to_chat(context, ChatId::new(0)) {
println!("Location streaming enabled.");
}
println!("{} chats", cnt);
}
@@ -568,8 +567,8 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
bail!("Argument [chat-id] is missing.");
}
if !arg1.is_empty() {
let chat_id = arg1.parse()?;
println!("Selecting chat #{}", chat_id);
let chat_id = ChatId::new(arg1.parse()?);
println!("Selecting chat {}", chat_id);
sel_chat = Some(Chat::load_from_db(context, chat_id)?);
*context.cmdline_sel_chat_id.write().unwrap() = chat_id;
}
@@ -587,8 +586,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
} else {
format!("{} member(s)", members.len())
};
info!(
context,
println!(
"{}#{}: {} [{}]{}{}",
chat_prefix(sel_chat),
sel_chat.get_id(),
@@ -608,14 +606,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" => {
@@ -691,7 +686,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
ensure!(sel_chat.is_some(), "No chat selected.");
let contacts = chat::get_chat_contacts(context, sel_chat.as_ref().unwrap().get_id());
info!(context, "Memberlist:");
println!("Memberlist:");
log_contactlist(context, &contacts);
println!(
@@ -717,8 +712,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let default_marker = "-".to_string();
for location in &locations {
let marker = location.marker.as_ref().unwrap_or(&default_marker);
info!(
context,
println!(
"Loc#{}: {}: lat={} lng={} acc={} Chat#{} Contact#{} {} {}",
location.location_id,
dc_timestamp_to_str(location.timestamp),
@@ -732,7 +726,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
);
}
if locations.is_empty() {
info!(context, "No locations.");
println!("No locations.");
}
}
"sendlocations" => {
@@ -798,7 +792,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let chat = if let Some(ref sel_chat) = sel_chat {
sel_chat.get_id()
} else {
0 as libc::c_uint
ChatId::new(0)
};
let msglist = context.search_msgs(chat, arg1);
@@ -812,14 +806,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.");
}
}
@@ -855,15 +849,23 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
}
print!("\n");
}
"archive" | "unarchive" => {
"archive" | "unarchive" | "pin" | "unpin" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = arg1.parse()?;
chat::archive(context, chat_id, arg0 == "archive")?;
let chat_id = ChatId::new(arg1.parse()?);
chat_id.set_visibility(
context,
match arg0 {
"archive" => ChatVisibility::Archived,
"unarchive" | "unpin" => ChatVisibility::Normal,
"pin" => ChatVisibility::Pinned,
_ => panic!("Unexpected command (This should never happen)"),
},
)?;
}
"delchat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = arg1.parse()?;
chat::delete(context, chat_id)?;
let chat_id = ChatId::new(arg1.parse()?);
chat_id.delete(context)?;
}
"msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
@@ -884,7 +886,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
);
let mut msg_ids = [MsgId::new(0); 1];
let chat_id = arg2.parse()?;
let chat_id = ChatId::new(arg2.parse()?);
msg_ids[0] = MsgId::new(arg1.parse()?);
chat::forward_msgs(context, &msg_ids, chat_id)?;
}
@@ -936,7 +938,14 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let contact = Contact::get_by_id(context, contact_id)?;
let name_n_addr = contact.get_name_n_addr();
let mut res = format!("Contact info for: {}:\n\n", name_n_addr);
let mut res = format!(
"Contact info for: {}:\nIcon: {}\n",
name_n_addr,
match contact.get_profile_image(context) {
Some(image) => image.to_str().unwrap().to_string(),
None => "NoIcon".to_string(),
}
);
res += &Contact::get_encrinfo(context, contact_id)?;
@@ -973,6 +982,31 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
res.get_text2()
);
}
"setqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
match set_config_from_qr(context, arg1) {
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
Err(err) => println!("Cannot set config from QR code: {:?}", err),
}
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
match provider::get_provider_info(arg1) {
Some(info) => {
println!("Information for provider belonging to {}:", arg1);
println!("status: {}", info.status as u32);
println!("before_login_hint: {}", info.before_login_hint);
println!("after_login_hint: {}", info.after_login_hint);
println!("overview_page: {}", info.overview_page);
for server in info.server.iter() {
println!("server: {}:{}", server.hostname, server.port,);
}
}
None => {
println!("No information for provider belonging to {} found.", arg1);
}
}
}
// TODO: implement this again, unclear how to match this through though, without writing a parser.
// "event" => {
// ensure!(!arg1.is_empty(), "Argument <id> missing.");

View File

@@ -20,8 +20,8 @@ use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use deltachat::chat::ChatId;
use deltachat::config;
use deltachat::configure::*;
use deltachat::context::*;
use deltachat::job::*;
use deltachat::oauth2::*;
@@ -41,7 +41,7 @@ use self::cmdline::*;
// Event Handler
fn receive_event(_context: &Context, event: Event) -> libc::uintptr_t {
fn receive_event(_context: &Context, event: Event) {
match event {
Event::Info(msg) => {
/* do not show the event as this would fill the screen */
@@ -111,8 +111,6 @@ fn receive_event(_context: &Context, event: Event) -> libc::uintptr_t {
print!("\x1b[33m{{Received {:?}}}\n\x1b[0m", event);
}
}
0
}
// Threads for waiting for messages and for jobs
@@ -202,7 +200,7 @@ fn stop_threads(context: &Context) {
println!("Stopping threads");
IS_RUNNING.store(false, Ordering::Relaxed);
interrupt_inbox_idle(context, true);
interrupt_inbox_idle(context);
interrupt_mvbox_idle(context);
interrupt_sentbox_idle(context);
interrupt_smtp_idle(context);
@@ -264,7 +262,7 @@ const DB_COMMANDS: [&str; 11] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 24] = [
const CHAT_COMMANDS: [&str; 26] = [
"listchats",
"listarchived",
"chat",
@@ -288,6 +286,8 @@ const CHAT_COMMANDS: [&str; 24] = [
"listmedia",
"archive",
"unarchive",
"pin",
"unpin",
"delchat",
];
const MESSAGE_COMMANDS: [&str; 8] = [
@@ -462,7 +462,7 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
}
"configure" => {
start_threads(ctx.clone());
configure(&ctx.read().unwrap());
ctx.read().unwrap().configure();
}
"oauth2" => {
if let Some(addr) = ctx.read().unwrap().get_config(config::Config::Addr) {
@@ -486,9 +486,10 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
}
"getqr" | "getbadqr" => {
start_threads(ctx.clone());
if let Some(mut qr) =
dc_get_securejoin_qr(&ctx.read().unwrap(), arg1.parse().unwrap_or_default())
{
if let Some(mut qr) = dc_get_securejoin_qr(
&ctx.read().unwrap(),
ChatId::new(arg1.parse().unwrap_or_default()),
) {
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
qr.replace_range(12..22, "0000000000")

View File

@@ -7,7 +7,6 @@ use tempfile::tempdir;
use deltachat::chat;
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::configure::*;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::job::{
@@ -16,21 +15,18 @@ use deltachat::job::{
};
use deltachat::Event;
fn cb(_ctx: &Context, event: Event) -> usize {
fn cb(_ctx: &Context, event: Event) {
print!("[{:?}]", event);
match event {
Event::ConfigureProgress(progress) => {
print!(" progress: {}\n", progress);
0
println!(" progress: {}", progress);
}
Event::Info(msg) | Event::Warning(msg) | Event::Error(msg) | Event::ErrorNetwork(msg) => {
print!(" {}\n", msg);
0
println!(" {}", msg);
}
_ => {
print!("\n");
0
println!();
}
}
}
@@ -80,7 +76,7 @@ fn main() {
ctx.set_config(config::Config::Addr, Some("d@testrun.org"))
.unwrap();
ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap();
configure(&ctx);
ctx.configure();
thread::sleep(duration);
@@ -104,7 +100,7 @@ fn main() {
println!("stopping threads");
*running.write().unwrap() = false;
deltachat::job::interrupt_inbox_idle(&ctx, true);
deltachat::job::interrupt_inbox_idle(&ctx);
deltachat::job::interrupt_smtp_idle(&ctx);
println!("joining");

View File

@@ -1,6 +1,10 @@
0.600.1
0.700.0
---------
- lots of new Python APIs
- use rust core-beta23
- introduce automatic versioning via setuptools_scm,
based on py-X.Y.Z tags

View File

@@ -3,41 +3,98 @@ deltachat python bindings
=========================
This package provides bindings to the deltachat-core_ Rust -library
which provides imap/smtp/crypto handling as well as chat/group/messages
handling to Android, Desktop and IO user interfaces.
which implements IMAP/SMTP/MIME/PGP e-mail standards and offers
a low-level Chat/Contact/Message API to user interfaces and bots.
Installing pre-built packages (linux-only)
==========================================
If you have a linux system you may install the ``deltachat`` binary "wheel" package
Installing bindings from source (Updated: 20-Jan-2020)
=========================================================
Install Rust and Cargo first. Deltachat needs a specific nightly
version, the easiest is probably to first install Rust stable from
rustup and then use this to install the correct nightly version.
Bootstrap Rust and Cargo by using rustup::
curl https://sh.rustup.rs -sSf | sh
Then GIT clone the deltachat-core-rust repo and get the actual
rust- and cargo-toolchain needed by deltachat::
git clone https://github.com/deltachat/deltachat-core-rust
cd deltachat-core-rust
rustup show
To install the Delta Chat Python bindings make sure you have Python3 installed.
E.g. on Debian-based systems `apt install python3 python3-pip
python3-venv` should give you a usable python installation.
Ensure you are in the deltachat-core-rust/python directory, create the
virtual environment and activate it in your shell::
cd python
python3 -m venv venv # or: virtualenv venv
source venv/bin/activate
You should now be able to build the python bindings using the supplied script::
./install_python_bindings.py
The installation might take a while, depending on your machine.
The bindings will be installed in release mode but with debug symbols.
The release mode is currently necessary because some tests generate RSA keys
which is prohibitively slow in non-release mode.
After successful binding installation you can install a few more
Python packages before running the tests::
python -m pip install pytest pytest-timeout pytest-rerunfailures requests
pytest -v tests
running "live" tests with temporary accounts
---------------------------------------------
If you want to run "liveconfig" functional tests you can set
``DCC_NEW_TMP_EMAIL`` to:
- a particular https-url that you can ask for from the delta
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_NEW_TMP_EMAIL`` set pytest invocations will use real
e-mail accounts and run through all functional "liveconfig" tests.
Installing pre-built packages (Linux-only)
========================================================
If you have a Linux system you may try to install the ``deltachat`` binary "wheel" packages
without any "build-from-source" steps.
1. `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
then create a fresh python environment and activate it in your shell::
We suggest to `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
then create a fresh Python virtual environment and activate it in your shell::
virtualenv venv # or: python -m venv
source venv/bin/activate
Afterwards, invoking ``python`` or ``pip install`` will only
modify files in your ``venv`` directory and leave your system installation
alone.
2. Install the wheel for linux::
pip install deltachat
Verify it worked by typing::
python -c "import deltachat"
Installing a wheel from a PR/branch
---------------------------------------
Afterwards, invoking ``python`` or ``pip install`` only
modifies files in your ``venv`` directory and leaves
your system installation alone.
For Linux, we automatically build wheels for all github PR branches
and push them to a python package index. To install the latest github ``master`` branch::
and push them to a python package index. To install the latest
github ``master`` branch::
pip install -i https://m.devpi.net/dc/master deltachat
pip install --pre -i https://m.devpi.net/dc/master deltachat
To verify it worked::
python -c "import deltachat"
.. note::
@@ -46,65 +103,6 @@ and push them to a python package index. To install the latest github ``master``
`in contact with us <https://delta.chat/en/contribute>`_.
Installing bindings from source
===============================
If you can't use "binary" method above then you need to compile
to core deltachat library::
git clone https://github.com/deltachat/deltachat-core-rust
cd deltachat-core-rust
cd python
If you don't have one active, create and activate a python "virtualenv":
python virtualenv venv # or python -m venv
source venv/bin/activate
Afterwards ``which python`` tells you that it comes out of the "venv"
directory that contains all python install artifacts. Let's first
install test tools::
pip install pytest pytest-timeout pytest-rerunfailures requests
then cargo-build and install the deltachat bindings::
python install_python_bindings.py
The bindings will be installed in release mode but with debug symbols.
The release mode is necessary because some tests generate RSA keys
which is prohibitively slow in debug mode.
After successful binding installation you can finally run the tests::
pytest -v tests
.. note::
Some tests are sometimes failing/hanging because of
https://github.com/deltachat/deltachat-core-rust/issues/331
and
https://github.com/deltachat/deltachat-core-rust/issues/326
running "live" tests (experimental)
-----------------------------------
If you want to run "liveconfig" functional tests you can set
``DCC_PY_LIVECONFIG`` to:
- a particular https-url that you can ask for from the delta
chat devs.
- 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
e-mail accounts and run through all functional "liveconfig" tests.
Code examples
=============
@@ -115,15 +113,11 @@ You may look at `examples <https://py.delta.chat/examples.html>`_.
.. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust
Building manylinux1 wheels
==========================
.. note::
This section may not fully work.
Building manylinux1 based wheels
================================
Building portable manylinux1 wheels which come with libdeltachat.so
and all it's dependencies is easy using the provided docker tooling.
can be done with docker-tooling.
using docker pull / premade images
------------------------------------
@@ -136,9 +130,9 @@ organization::
This docker image can be used to run tests and build Python wheels for all interpreters::
$ bash ci_scripts/ci_run.sh
This command runs tests and build-wheel scripts in a docker container.
$ docker run -e DCC_NEW_TMP_EMAIL \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh
Optionally build your own docker image

View File

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

View File

@@ -13,6 +13,7 @@ def main():
"root": "..",
"relative_to": __file__,
'tag_regex': r'^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
'git_describe_command': "git describe --dirty --tags --long --match py-*.*",
},
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
long_description=long_description,

View File

@@ -3,6 +3,7 @@
from __future__ import print_function
import atexit
import threading
import os
import re
import time
from array import array
@@ -25,7 +26,7 @@ class Account(object):
by the underlying deltachat core library. All public Account methods are
meant to be memory-safe and return memory-safe objects.
"""
def __init__(self, db_path, logid=None, eventlogging=True, debug=True):
def __init__(self, db_path, logid=None, eventlogging=True, os_name=None, debug=True):
""" initialize account object.
:param db_path: a path to the account database. The database
@@ -33,10 +34,11 @@ class Account(object):
:param logid: an optional logging prefix that should be used with
the default internal logging.
:param eventlogging: if False no eventlogging and no context callback will be configured
:param os_name: this will be put to the X-Mailer header in outgoing messages
:param debug: turn on debug logging for events.
"""
self._dc_context = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, as_dc_charpointer(os_name)),
_destroy_dc_context,
)
if eventlogging:
@@ -94,9 +96,12 @@ class Account(object):
"""
self._check_config_key(name)
name = name.encode("utf8")
value = value.encode("utf8")
if name == b"addr" and self.is_configured():
raise ValueError("can not change 'addr' after account is configured.")
if value is not None:
value = value.encode("utf8")
else:
value = ffi.NULL
lib.dc_set_config(self._dc_context, name, value)
def get_config(self, name):
@@ -113,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.
@@ -132,6 +149,18 @@ class Account(object):
"""
return lib.dc_is_configured(self._dc_context)
def set_avatar(self, img_path):
"""Set self avatar.
:raises ValueError: if profile image could not be set
:returns: None
"""
if img_path is None:
self.set_config("selfavatar", None)
else:
assert os.path.exists(img_path), img_path
self.set_config("selfavatar", img_path)
def check_is_configured(self):
""" Raise ValueError if this account is not configured. """
if not self.is_configured():
@@ -561,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)
@@ -583,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.
@@ -109,6 +147,30 @@ class Chat(object):
# ------ chat messaging API ------------------------------
def send_msg(self, msg):
"""send a message by using a ready Message object.
:param msg: a :class:`deltachat.message.Message` instance
previously returned by
e.g. :meth:`deltachat.message.Message.new_empty` or
:meth:`prepare_file`.
:raises ValueError: if message can not be sent.
:returns: a :class:`deltachat.message.Message` instance as
sent out. This is the same object as was passed in, which
has been modified with the new state of the core.
"""
if msg.is_out_preparing():
assert msg.id != 0
# get a fresh copy of dc_msg, the core needs it
msg = Message.from_db(self.account, msg.id)
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
if sent_id == 0:
raise ValueError("message could not be sent")
# modify message in place to avoid bad state for the caller
msg._dc_msg = Message.from_db(self.account, sent_id)._dc_msg
return msg
def send_text(self, text):
""" send a text message and return the resulting Message instance.
@@ -130,9 +192,12 @@ class Chat(object):
:raises ValueError: if message can not be send/chat does not exist.
:returns: the resulting :class:`deltachat.message.Message` instance
"""
msg = self.prepare_message_file(path=path, mime_type=mime_type)
self.send_prepared(msg)
return msg
msg = Message.new_empty(self.account, view_type="file")
msg.set_file(path, mime_type)
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
if sent_id == 0:
raise ValueError("message could not be sent")
return Message.from_db(self.account, sent_id)
def send_image(self, path):
""" send an image message and return the resulting Message instance.
@@ -142,9 +207,12 @@ class Chat(object):
:returns: the resulting :class:`deltachat.message.Message` instance
"""
mime_type = mimetypes.guess_type(path)[0]
msg = self.prepare_message_file(path=path, mime_type=mime_type, view_type="image")
self.send_prepared(msg)
return msg
msg = Message.new_empty(self.account, view_type="image")
msg.set_file(path, mime_type)
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
if sent_id == 0:
raise ValueError("message could not be sent")
return Message.from_db(self.account, sent_id)
def prepare_message(self, msg):
""" create a new prepared message.
@@ -355,7 +423,7 @@ class Chat(object):
"""return True if this chat is archived.
:returns: True if archived.
"""
return lib.dc_chat_get_archived(self._dc_chat)
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_ARCHIVED
def enable_sending_locations(self, seconds):
"""enable sending locations for this chat.

View File

@@ -18,6 +18,7 @@ DC_QR_ASK_VERIFYGROUP = 202
DC_QR_FPR_OK = 210
DC_QR_FPR_MISMATCH = 220
DC_QR_FPR_WITHOUT_ADDR = 230
DC_QR_ACCOUNT = 250
DC_QR_ADDR = 320
DC_QR_TEXT = 330
DC_QR_URL = 332
@@ -68,7 +69,6 @@ DC_LP_SMTP_SOCKET_SSL = 0x20000
DC_LP_SMTP_SOCKET_PLAIN = 0x40000
DC_CERTCK_AUTO = 0
DC_CERTCK_STRICT = 1
DC_CERTCK_ACCEPT_INVALID_HOSTNAMES = 2
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3
DC_EMPTY_MVBOX = 0x01
DC_EMPTY_INBOX = 0x02
@@ -103,9 +103,15 @@ DC_EVENT_FILE_COPIED = 2055
DC_EVENT_IS_OFFLINE = 2081
DC_EVENT_GET_STRING = 2091
DC_STR_SELFNOTINGRP = 21
DC_KEY_GEN_DEFAULT = 0
DC_KEY_GEN_RSA2048 = 1
DC_KEY_GEN_ED25519 = 2
DC_PROVIDER_STATUS_OK = 1
DC_PROVIDER_STATUS_PREPARATION = 2
DC_PROVIDER_STATUS_BROKEN = 3
DC_CHAT_VISIBILITY_NORMAL = 0
DC_CHAT_VISIBILITY_ARCHIVED = 1
DC_CHAT_VISIBILITY_PINNED = 2
DC_STR_NOMESSAGES = 1
DC_STR_SELF = 2
DC_STR_DRAFT = 3
@@ -158,7 +164,7 @@ DC_STR_COUNT = 68
def read_event_defines(f):
rex = re.compile(r'#define\s+((?:DC_EVENT|DC_QR|DC_MSG|DC_LP|DC_EMPTY|DC_CERTCK|DC_STATE|DC_STR|'
r'DC_CONTACT_ID|DC_GCL|DC_CHAT|DC_PROVIDER)_\S+)\s+([x\d]+).*')
r'DC_CONTACT_ID|DC_GCL|DC_CHAT|DC_PROVIDER|DC_KEY_GEN)_\S+)\s+([x\d]+).*')
for line in f:
m = rex.match(line)
if m:

View File

@@ -47,3 +47,13 @@ class Contact(object):
def is_verified(self):
""" Return True if the contact is verified. """
return lib.dc_contact_is_verified(self._dc_contact)
def get_profile_image(self):
"""Get contact profile image.
:returns: path to profile image, None if no profile image exists.
"""
dc_res = lib.dc_contact_get_profile_image(self._dc_contact)
if dc_res == ffi.NULL:
return None
return from_dc_charpointer(dc_res)

View File

@@ -174,7 +174,7 @@ class Message(object):
@property
def _msgstate(self):
if self.id == 0:
dc_msg = self.message._dc_msg
dc_msg = self._dc_msg
else:
# load message from db to get a fresh/current state
dc_msg = ffi.gc(

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,18 +97,22 @@ class SessionLiveConfigFromFile:
class SessionLiveConfigFromURL:
def __init__(self, url, create_token):
def __init__(self, url):
self.configlist = []
for i in range(2):
res = requests.post(url, json={"token_create_user": int(create_token)})
self.url = url
def get(self, index):
try:
return self.configlist[index]
except IndexError:
assert index == len(self.configlist), index
res = requests.post(self.url)
if res.status_code != 200:
pytest.skip("creating newtmpuser failed {!r}".format(res))
d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"])
self.configlist.append(config)
def get(self, index):
return self.configlist[index]
return config
def exists(self):
return bool(self.configlist)
@@ -105,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):
@@ -120,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:
@@ -139,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:
@@ -170,18 +206,24 @@ 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, config={}):
ac, configdict = self.get_online_config(
pre_generated_key=pre_generated_key)
configdict.update(config)
ac.configure(**configdict)
ac.start_threads(mvbox=mvbox, sentbox=sentbox)
return ac
def get_one_online_account(self):
ac1 = self.get_online_configuring_account()
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
@@ -195,10 +237,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"))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

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:
@@ -16,6 +17,20 @@ class TestOfflineAccountBasic:
with pytest.raises(ValueError):
Account(p.strpath)
def test_os_name(self, tmpdir):
p = tmpdir.join("hello.db")
# we can't easily test if os_name is used in X-Mailer
# outgoing messages without a full Online test
# but we at least check Account accepts the arg
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()
@@ -204,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")
@@ -382,6 +409,23 @@ class TestOfflineChat:
assert not res.is_ask_verifygroup()
assert res.contact_id == 10
def test_group_chat_many_members_add_remove(self, ac1, lp):
lp.sec("ac1: creating group chat with 10 other members")
chat = ac1.create_group_chat(name="title1")
contacts = []
for i in range(10):
contact = ac1.create_contact("some{}@example.org".format(i))
contacts.append(contact)
chat.add_contact(contact)
num_contacts = len(chat.get_contacts())
assert num_contacts == 11
lp.sec("ac1: removing two contacts and checking things are right")
chat.remove_contact(contacts[9])
chat.remove_contact(contacts[3])
assert len(chat.get_contacts()) == 9
class TestOnlineAccount:
def get_chat(self, ac1, ac2, both_created=False):
@@ -392,6 +436,45 @@ class TestOnlineAccount:
ac2.create_chat_by_contact(ac2.create_contact(email=ac1.get_config("addr")))
return chat
@pytest.mark.ignored
def test_configure_generate_key(self, acfactory, lp):
# A slow test which will generate new keys.
ac1 = acfactory.get_online_configuring_account(
pre_generated_key=False,
config={"key_gen_type": str(const.DC_KEY_GEN_RSA2048)}
)
ac2 = acfactory.get_online_configuring_account(
pre_generated_key=False,
config={"key_gen_type": str(const.DC_KEY_GEN_ED25519)}
)
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
chat = self.get_chat(ac1, ac2, both_created=True)
lp.sec("ac1: send unencrypted message to ac2")
chat.send_text("message1")
lp.sec("ac2: waiting for message from ac1")
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
msg_in = ac2.get_message_by_id(ev[2])
assert msg_in.text == "message1"
assert not msg_in.is_encrypted()
lp.sec("ac2: send encrypted message to ac1")
msg_in.chat.send_text("message2")
lp.sec("ac1: waiting for message from ac2")
ev = ac1._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
msg2_in = ac1.get_message_by_id(ev[2])
assert msg2_in.text == "message2"
assert msg2_in.is_encrypted()
lp.sec("ac1: send encrypted message to ac2")
msg2_in.chat.send_text("message3")
lp.sec("ac2: waiting for message from ac1")
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
msg3_in = ac1.get_message_by_id(ev[2])
assert msg3_in.text == "message3"
assert msg3_in.is_encrypted()
def test_configure_canceled(self, acfactory):
ac1 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 200)
@@ -410,37 +493,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()
@@ -456,10 +555,7 @@ class TestOnlineAccount:
msg1 = Message.new_empty(ac1, "file")
msg1.set_text("withfile")
msg1.set_file(p)
message = chat.prepare_message(msg1)
assert message.is_out_preparing()
assert message.text == "withfile"
chat.send_prepared(message)
chat.send_msg(msg1)
lp.sec("ac2: receive message")
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
@@ -477,6 +573,30 @@ class TestOnlineAccount:
assert msg2.filename.endswith("html.zip")
assert msg.filename != msg2.filename
def test_send_file_html_attachment(self, tmpdir, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
chat = self.get_chat(ac1, ac2)
basename = "test.html"
content = "<html><body>text</body>data"
p = os.path.join(tmpdir.strpath, basename)
with open(p, "w") as f:
# write wrong html to see if core tries to parse it
# (it shouldn't as it's a file attachment)
f.write(content)
lp.sec("ac1: prepare and send attachment + text to ac2")
chat.send_file(p, mime_type="text/html")
lp.sec("ac2: receive message")
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
assert ev[2] > const.DC_CHAT_ID_LAST_SPECIAL
msg = ac2.get_message_by_id(ev[2])
assert open(msg.filename).read() == content
assert msg.filename.endswith(basename)
def test_mvbox_sentbox_threads(self, acfactory, lp):
lp.sec("ac1: start with mvbox thread")
ac1 = acfactory.get_online_configuring_account(mvbox=True, sentbox=True)
@@ -594,6 +714,9 @@ class TestOnlineAccount:
def test_send_and_receive_message_markseen(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
# make DC's life harder wrt to encodings
ac1.set_config("displayname", "ä name")
lp.sec("ac1: create chat with ac2")
chat = self.get_chat(ac1, ac2)
@@ -611,8 +734,9 @@ class TestOnlineAccount:
msg_in = ac2.get_message_by_id(msg_out.id)
assert msg_in.text == "message1"
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()
@@ -628,14 +752,26 @@ class TestOnlineAccount:
chat2b.mark_noticed()
assert chat2b.count_fresh_messages() == 0
lp.sec("mark message as seen on ac2, wait for changes on ac1")
ac2.mark_seen_messages([msg_in])
ac2._evlogger.consume_events()
lp.sec("sending a second message from ac1 to ac2")
msg_out2 = chat.send_text("message2")
lp.sec("wait for ac2 to receive second message")
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
assert ev[2] == msg_out2.id
msg_in2 = ac2.get_message_by_id(msg_out2.id)
lp.sec("mark messages as seen on ac2, wait for changes on ac1")
ac2.mark_seen_messages([msg_in, msg_in2])
lp.step("1")
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_READ")
assert ev[1] > const.DC_CHAT_ID_LAST_SPECIAL
assert ev[2] > const.DC_MSG_ID_LAST_SPECIAL
for i in range(2):
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_READ")
assert ev[1] > const.DC_CHAT_ID_LAST_SPECIAL
assert ev[2] > const.DC_MSG_ID_LAST_SPECIAL
lp.step("2")
assert msg_out.is_out_mdn_received()
assert msg_out2.is_out_mdn_received()
lp.sec("check that a second call to mark_seen does not create change or smtp job")
ac2._evlogger.consume_events()
@@ -645,6 +781,41 @@ class TestOnlineAccount:
except queue.Empty:
pass # mark_seen_messages() has generated events before it returns
def test_mdn_asymetric(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: create chat with ac2")
chat = self.get_chat(ac1, ac2, both_created=True)
# make sure mdns are enabled (usually enabled by default already)
ac1.set_config("mdns_enabled", "1")
ac2.set_config("mdns_enabled", "1")
lp.sec("sending text message from ac1 to ac2")
msg_out = chat.send_text("message1")
assert len(chat.get_messages()) == 1
lp.sec("disable ac1 MDNs")
ac1.set_config("mdns_enabled", "0")
lp.sec("wait for ac2 to receive message")
msg = ac2.wait_next_incoming_message()
assert len(msg.chat.get_messages()) == 1
lp.sec("ac2: mark incoming message as seen")
ac2.mark_seen_messages([msg])
lp.sec("ac1: waiting for incoming activity")
# MDN should be moved even though MDNs are already disabled
ac1._evlogger.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
assert len(chat.get_messages()) == 1
# MDN is received even though MDNs are already disabled
assert msg_out.is_out_mdn_received()
def test_send_and_receive_will_encrypt_decrypt(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
@@ -673,6 +844,12 @@ class TestOnlineAccount:
assert msg_back.text == "message-back"
assert msg_back.is_encrypted()
# Test that we do not gossip peer keys in 1-to-1 chat,
# as it makes no sense to gossip to peers their own keys.
# Gossip is only sent in encrypted messages,
# and we sent encrypted msg_back right above.
assert chat2b.get_summary()["gossiped_timestamp"] == 0
lp.sec("create group chat with two members, one of which has no encrypt state")
chat = ac1.create_group_chat("encryption test")
chat.add_contact(ac1.create_contact(ac2.get_config("addr")))
@@ -738,7 +915,21 @@ class TestOnlineAccount:
print("ac1: e2ee_enabled={}".format(ac1.get_config("e2ee_enabled")))
print("ac2: e2ee_enabled={}".format(ac2.get_config("e2ee_enabled")))
ac1.set_config("e2ee_enabled", "0")
chat.send_text("message2 -- should be encrypted")
# Set unprepared and unencrypted draft to test that it is not
# taken into account when determining whether last message is
# encrypted.
msg_draft = Message.new_empty(ac1, "text")
msg_draft.set_text("message2 -- should be encrypted")
chat.set_draft(msg_draft)
# Get the draft, prepare and send it.
msg_draft = chat.get_draft()
msg_out = chat.prepare_message(msg_draft)
chat.send_prepared(msg_out)
chat.set_draft(None)
assert chat.get_draft() is None
lp.sec("wait for ac2 to receive message")
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
@@ -934,7 +1125,54 @@ class TestOnlineAccount:
assert msg.text == "world"
assert msg.is_encrypted()
def test_set_get_profile_image(self, acfactory, data, lp):
def test_set_get_contact_avatar(self, acfactory, data, lp):
lp.sec("configuring ac1 and ac2")
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("ac1: set own profile image")
p = data.get_path("d.png")
ac1.set_avatar(p)
lp.sec("ac1: create 1:1 chat with ac2")
chat = self.get_chat(ac1, ac2, both_created=True)
msg = chat.send_text("hi -- do you see my brand new avatar?")
assert not msg.is_encrypted()
lp.sec("ac2: wait for receiving message and avatar from ac1")
msg1 = ac2.wait_next_incoming_message()
assert not msg1.chat.is_deaddrop()
received_path = msg1.get_sender_contact().get_profile_image()
assert open(received_path, "rb").read() == open(p, "rb").read()
lp.sec("ac2: set own profile image")
p = data.get_path("d.png")
ac2.set_avatar(p)
lp.sec("ac2: send back message")
m = msg1.chat.send_text("yes, i received your avatar -- how do you like mine?")
assert m.is_encrypted()
lp.sec("ac1: wait for receiving message and avatar from ac2")
msg2 = ac1.wait_next_incoming_message()
received_path = msg2.get_sender_contact().get_profile_image()
assert received_path is not None, "did not get avatar through encrypted message"
assert open(received_path, "rb").read() == open(p, "rb").read()
ac2._evlogger.consume_events()
ac1._evlogger.consume_events()
# XXX not sure if the following is correct / possible. you may remove it
lp.sec("ac1: delete profile image from chat, and send message to ac2")
ac1.set_avatar(None)
m = msg2.chat.send_text("i don't like my avatar anymore and removed it")
assert m.is_encrypted()
lp.sec("ac2: wait for message along with avatar deletion of ac1")
msg3 = ac2.wait_next_incoming_message()
assert msg3.get_sender_contact().get_profile_image() is None
def test_set_get_group_image(self, acfactory, data, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
lp.sec("create unpromoted group chat")
@@ -1036,6 +1274,104 @@ class TestOnlineAccount:
assert not locations3
class TestGroupStressTests:
def test_group_many_members_add_leave_remove(self, acfactory, lp):
lp.sec("creating and configuring five accounts")
accounts = [acfactory.get_online_configuring_account() for i in range(5)]
for acc in accounts:
wait_configuration_progress(acc, 1000)
ac1 = accounts.pop()
lp.sec("ac1: setting up contacts with 4 other members")
contacts = []
for acc, name in zip(accounts, list("äöüsr")):
contact = ac1.create_contact(acc.get_config("addr"), name=name)
contacts.append(contact)
# make sure we accept the "hi" message
ac1.create_chat_by_contact(contact)
# make sure the other side accepts our messages
c1 = acc.create_contact(ac1.get_config("addr"), "ä member")
chat1 = acc.create_chat_by_contact(c1)
# send a message to get the contact key via autocrypt header
chat1.send_text("hi")
msg = ac1.wait_next_incoming_message()
assert msg.text == "hi"
# Save fifth account for later
ac5 = accounts.pop()
contact5 = contacts.pop()
lp.sec("ac1: creating group chat with 3 other members")
chat = ac1.create_group_chat("title1")
for contact in contacts:
chat.add_contact(contact)
assert not chat.is_promoted()
lp.sec("ac1: send mesage to new group chat")
msg = chat.send_text("hello")
assert chat.is_promoted()
assert msg.is_encrypted()
gossiped_timestamp = chat.get_summary()["gossiped_timestamp"]
assert gossiped_timestamp > 0
num_contacts = len(chat.get_contacts())
assert num_contacts == 3 + 1
lp.sec("ac2: checking that the chat arrived correctly")
ac2 = accounts[0]
msg = ac2.wait_next_incoming_message()
assert msg.text == "hello"
print("chat is", msg.chat)
assert len(msg.chat.get_contacts()) == 4
lp.sec("ac3: checking that 'ac4' is a known contact")
ac3 = accounts[1]
msg3 = ac3.wait_next_incoming_message()
assert msg3.text == "hello"
ac3_contacts = ac3.get_contacts()
assert len(ac3_contacts) == 3
ac4_contacts = ac3.get_contacts(query=accounts[2].get_config("addr"))
assert len(ac4_contacts) == 1
lp.sec("ac2: removing one contact")
to_remove = contacts[-1]
msg.chat.remove_contact(to_remove)
lp.sec("ac1: receiving system message about contact removal")
sysmsg = ac1.wait_next_incoming_message()
assert to_remove.addr in sysmsg.text
assert len(sysmsg.chat.get_contacts()) == 3
# Receiving message about removed contact does not reset gossip
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
lp.sec("ac1: sending another message to the chat")
chat.send_text("hello2")
msg = ac2.wait_next_incoming_message()
assert msg.text == "hello2"
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
lp.sec("ac1: adding fifth member to the chat")
chat.add_contact(contact5)
# Additng contact to chat resets gossiped_timestamp
assert chat.get_summary()["gossiped_timestamp"] >= gossiped_timestamp
lp.sec("ac2: receiving system message about contact addition")
sysmsg = ac2.wait_next_incoming_message()
assert contact5.addr in sysmsg.text
assert len(sysmsg.chat.get_contacts()) == 4
lp.sec("ac5: waiting for message about addition to the chat")
sysmsg = ac5.wait_next_incoming_message()
msg = sysmsg.chat.send_text("hello!")
# Message should be encrypted because keys of other members are gossiped
assert msg.is_encrypted()
class TestOnlineConfigureFails:
def test_invalid_password(self, acfactory):
ac1, configdict = acfactory.get_online_config()

View File

@@ -1,10 +1,49 @@
from __future__ import print_function
import os.path
import shutil
import pytest
from filecmp import cmp
from deltachat import const
from conftest import wait_configuration_progress, wait_msgs_changed
from deltachat import const
class TestOnlineInCreation:
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
lp.sec("Creating in-creation file outside of blobdir")
assert tmpdir.strpath != ac1.get_blobdir()
src = tmpdir.join('file.txt').ensure(file=1)
with pytest.raises(Exception):
chat.prepare_message_file(src.strpath)
def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
lp.sec("Creating file outside of blobdir")
assert tmpdir.strpath != ac1.get_blobdir()
src = tmpdir.join('file.txt')
src.write("hello there\n")
chat.send_file(src.strpath)
blob_src = os.path.join(ac1.get_blobdir(), 'file.txt')
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
def test_forward_increation(self, acfactory, data, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
@@ -17,7 +56,10 @@ class TestOnlineInCreation:
wait_msgs_changed(ac1, 0, 0) # why no chat id?
lp.sec("create a message with a file in creation")
path = data.get_path("d.png")
orig = data.get_path("d.png")
path = os.path.join(ac1.get_blobdir(), 'd.png')
with open(path, "x") as fp:
fp.write("preparing")
prepared_original = chat.prepare_message_file(path)
assert prepared_original.is_out_preparing()
wait_msgs_changed(ac1, chat.id, prepared_original.id)
@@ -38,6 +80,7 @@ class TestOnlineInCreation:
lp.sec("finish creating the file and send it")
assert prepared_original.is_out_preparing()
shutil.copyfile(orig, path)
chat.send_prepared(prepared_original)
assert prepared_original.is_out_pending() or prepared_original.is_out_delivered()
wait_msgs_changed(ac1, chat.id, prepared_original.id)
@@ -59,11 +102,11 @@ class TestOnlineInCreation:
ev1 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev1[1] > const.DC_CHAT_ID_LAST_SPECIAL
received_original = ac2.get_message_by_id(ev1[2])
assert cmp(received_original.filename, path, False)
assert cmp(received_original.filename, orig, shallow=False)
lp.sec("wait2 for original or forwarded messages to arrive")
ev2 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev2[1] > const.DC_CHAT_ID_LAST_SPECIAL
assert ev2[1] != ev1[1]
received_copy = ac2.get_message_by_id(ev2[2])
assert cmp(received_copy.filename, path, False)
assert cmp(received_copy.filename, orig, shallow=False)

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,8 +7,8 @@ envlist =
[testenv]
commands =
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx {posargs:tests}
# python tests/package_wheels.py {toxworkdir}/wheelhouse
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs:tests}
python tests/package_wheels.py {toxworkdir}/wheelhouse
passenv =
TRAVIS
DCC_RS_DEV
@@ -32,7 +32,7 @@ commands =
[testenv:lint]
skipsdist = True
usedevelop = True
skip_install = True
deps =
flake8
# pygments required by rst-lint
@@ -65,11 +65,11 @@ commands =
[pytest]
addopts = -v -rs
python_files = tests/test_*.py
addopts = -v -ra --strict-markers
python_files = tests/test_*.py
norecursedirs = .tox
xfail_strict=true
timeout = 60
timeout = 90
timeout_method = thread
[flake8]

80
scripts/proxy.py Normal file
View File

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

View File

@@ -35,7 +35,7 @@ if __name__ == "__main__":
if len(sys.argv) < 2:
for x in ("Cargo.toml", "deltachat-ffi/Cargo.toml"):
print("{}: {}".format(x, read_toml_version(x)))
raise SystemExit("need argument: new version, example 1.0.0-beta.27")
raise SystemExit("need argument: new version, example: 1.25.0")
newversion = sys.argv[1]
if newversion.count(".") < 2:
raise SystemExit("need at least two dots in version")
@@ -45,7 +45,7 @@ if __name__ == "__main__":
assert core_toml == ffi_toml, (core_toml, ffi_toml)
for line in open("CHANGELOG.md"):
## 1.0.0-beta5
## 1.25.0
if line.startswith("## "):
if line[2:].strip().startswith(newversion):
break
@@ -55,7 +55,9 @@ if __name__ == "__main__":
replace_toml_version("Cargo.toml", newversion)
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
subprocess.call(["cargo", "update", "-p", "deltachat"])
subprocess.call(["cargo", "check"])
subprocess.call(["git", "add", "-u"])
# subprocess.call(["cargo", "update", "-p", "deltachat"])
print("after commit make sure to: ")
print("")

124
spec.md
View File

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

View File

@@ -6,10 +6,10 @@ use std::collections::BTreeMap;
use std::str::FromStr;
use std::{fmt, str};
use crate::constants::*;
use crate::contact::*;
use crate::context::Context;
use crate::key::*;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, SignedPublicKey};
/// Possible values for encryption preference
#[derive(PartialEq, Eq, Debug, Clone, Copy, FromPrimitive, ToPrimitive)]
@@ -52,13 +52,17 @@ impl str::FromStr for EncryptPreference {
#[derive(Debug)]
pub struct Aheader {
pub addr: String,
pub public_key: Key,
pub public_key: SignedPublicKey,
pub prefer_encrypt: EncryptPreference,
}
impl Aheader {
/// Creates new autocrypt header
pub fn new(addr: String, public_key: Key, prefer_encrypt: EncryptPreference) -> Self {
pub fn new(
addr: String,
public_key: SignedPublicKey,
prefer_encrypt: EncryptPreference,
) -> Self {
Aheader {
addr,
public_key,
@@ -71,9 +75,7 @@ impl Aheader {
wanted_from: &str,
headers: &[mailparse::MailHeader<'_>],
) -> Option<Self> {
use mailparse::MailHeaderMap;
if let Ok(Some(value)) = headers.get_first_value("Autocrypt") {
if let 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

@@ -6,9 +6,13 @@ use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use self::image::GenericImageView;
use crate::constants::AVATAR_SIZE;
use crate::context::Context;
use crate::events::Event;
extern crate image;
/// Represents a file in the blob directory.
///
/// The object has a name, which will always be valid UTF-8. Having a
@@ -159,7 +163,7 @@ impl<'a> BlobObject<'a> {
/// This merely delegates to the [BlobObject::create_and_copy] and
/// the [BlobObject::from_path] methods. See those for possible
/// errors.
pub fn create_from_path(
pub fn new_from_path(
context: &Context,
src: impl AsRef<Path>,
) -> std::result::Result<BlobObject, BlobError> {
@@ -318,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())
}
}
@@ -349,6 +352,31 @@ impl<'a> BlobObject<'a> {
}
true
}
pub fn recode_to_avatar_size(&self, context: &Context) -> Result<(), BlobError> {
let blob_abs = self.to_abs_path();
let img = image::open(&blob_abs).map_err(|err| BlobError::RecodeFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
if img.width() <= AVATAR_SIZE && img.height() <= AVATAR_SIZE {
return Ok(());
}
let img = img.thumbnail(AVATAR_SIZE, AVATAR_SIZE);
img.save(&blob_abs).map_err(|err| BlobError::WriteFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
Ok(())
}
}
impl<'a> fmt::Display for BlobObject<'a> {
@@ -382,6 +410,13 @@ pub enum BlobError {
cause: std::io::Error,
backtrace: failure::Backtrace,
},
RecodeFailure {
blobdir: PathBuf,
blobname: String,
#[cause]
cause: image::ImageError,
backtrace: failure::Backtrace,
},
WrongBlobdir {
blobdir: PathBuf,
src: PathBuf,
@@ -429,6 +464,9 @@ impl fmt::Display for BlobError {
blobname,
blobdir.display(),
),
BlobError::RecodeFailure {
blobdir, blobname, ..
} => write!(f, "Failed to recode {} in {}", blobname, blobdir.display(),),
BlobError::WrongBlobdir { blobdir, src, .. } => write!(
f,
"File path {} is not in blobdir {}",
@@ -559,14 +597,14 @@ mod tests {
let src_ext = t.dir.path().join("external");
fs::write(&src_ext, b"boo").unwrap();
let blob = BlobObject::create_from_path(&t.ctx, &src_ext).unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/external");
let data = fs::read(blob.to_abs_path()).unwrap();
assert_eq!(data, b"boo");
let src_int = t.ctx.get_blobdir().join("internal");
fs::write(&src_int, b"boo").unwrap();
let blob = BlobObject::create_from_path(&t.ctx, &src_int).unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_int).unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
let data = fs::read(blob.to_abs_path()).unwrap();
assert_eq!(data, b"boo");
@@ -576,7 +614,7 @@ mod tests {
let t = dummy_context();
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
fs::write(&src_ext, b"boo").unwrap();
let blob = BlobObject::create_from_path(&t.ctx, &src_ext).unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap();
assert_eq!(
blob.as_name(),
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
@@ -592,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");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,7 @@ use crate::stock::StockMessage;
#[derive(Debug)]
pub struct Chatlist {
/// Stores pairs of `chat_id, message_id`
ids: Vec<(u32, MsgId)>,
ids: Vec<(ChatId, MsgId)>,
}
impl Chatlist {
@@ -60,7 +60,7 @@ impl Chatlist {
/// or "Not now".
/// The UI can also offer a "Close" button that calls dc_marknoticed_contact() then.
/// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
/// archived *any* chat using dc_archive_chat(). The UI should show a link as
/// archived *any* chat using dc_set_chat_visibility(). The UI should show a link as
/// "Show archived chats", if the user clicks this item, the UI should show a
/// list of all archived chats that can be created by this function hen using
/// the DC_GCL_ARCHIVED_ONLY flag.
@@ -91,7 +91,7 @@ impl Chatlist {
let mut add_archived_link_item = false;
let process_row = |row: &rusqlite::Row| {
let chat_id: u32 = row.get(0)?;
let chat_id: ChatId = row.get(0)?;
let msg_id: MsgId = row.get(1).unwrap_or_default();
Ok((chat_id, msg_id))
};
@@ -127,13 +127,13 @@ impl Chatlist {
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?))
AND (hidden=0 OR state=?1))
WHERE c.id>9
AND c.blocked=0
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, query_contact_id as i32],
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
process_row,
process_rows,
)?
@@ -162,6 +162,12 @@ impl Chatlist {
let query = query.trim().to_string();
ensure!(!query.is_empty(), "missing query");
// allow searching over special names that may change at any time
// when the ui calls set_stock_translation()
if let Err(err) = update_special_chat_names(context) {
warn!(context, "cannot update special chat names: {:?}", err)
}
let str_like_cmd = format!("%{}%", query);
context.sql.query_map(
"SELECT c.id, m.id
@@ -193,19 +199,22 @@ impl Chatlist {
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?))
AND (hidden=0 OR state=?1))
WHERE c.id>9
AND c.blocked=0
AND c.archived=0
AND NOT c.archived=?2
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft],
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, ChatVisibility::Archived, ChatVisibility::Pinned],
process_row,
process_rows,
)?;
if 0 == listflags & DC_GCL_NO_SPECIALS {
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context) {
ids.insert(0, (DC_CHAT_ID_DEADDROP, last_deaddrop_fresh_msg_id));
ids.insert(
0,
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
);
}
add_archived_link_item = true;
}
@@ -214,9 +223,9 @@ impl Chatlist {
if add_archived_link_item && dc_get_archived_cnt(context) > 0 {
if ids.is_empty() && 0 != listflags & DC_GCL_ADD_ALLDONE_HINT {
ids.push((DC_CHAT_ID_ALLDONE_HINT, MsgId::new(0)));
ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0)));
}
ids.push((DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0)));
ids.push((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0)));
}
Ok(Chatlist { ids })
@@ -235,9 +244,9 @@ impl Chatlist {
/// Get a single chat ID of a chatlist.
///
/// To get the message object from the message ID, use dc_get_chat().
pub fn get_chat_id(&self, index: usize) -> u32 {
pub fn get_chat_id(&self, index: usize) -> ChatId {
if index >= self.ids.len() {
return 0;
return ChatId::new(0);
}
self.ids[index].0
}
@@ -269,8 +278,8 @@ impl Chatlist {
// This is because we may want to display drafts here or stuff as
// "is typing".
// Also, sth. as "No messages" would not work if the summary comes from a message.
let mut ret = Lot::new();
if index >= self.ids.len() {
ret.text2 = Some("ErrBadChatlistIndex".to_string());
return ret;
@@ -301,7 +310,7 @@ impl Chatlist {
None
};
if chat.id == DC_CHAT_ID_ARCHIVED_LINK {
if chat.id.is_archived_link() {
ret.text2 = None;
} else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED
{
@@ -312,9 +321,13 @@ impl Chatlist {
ret
}
pub fn get_index_for_id(&self, id: ChatId) -> Option<usize> {
self.ids.iter().position(|(chat_id, _)| chat_id == &id)
}
}
/// Get the number of archived chats
/// Returns the number of archived chats
pub fn dc_get_archived_cnt(context: &Context) -> u32 {
context
.sql
@@ -349,7 +362,6 @@ fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
mod tests {
use super::*;
use crate::chat;
use crate::test_utils::*;
#[test]
@@ -369,7 +381,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);
@@ -380,8 +392,33 @@ mod tests {
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
assert_eq!(chats.len(), 0);
chat::archive(&t.ctx, chat_id1, true).ok();
chat_id1
.set_visibility(&t.ctx, ChatVisibility::Archived)
.ok();
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
assert_eq!(chats.len(), 1);
}
#[test]
fn test_search_special_chat_names() {
let t = dummy_context();
t.ctx.update_device_chats().unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None).unwrap();
assert_eq!(chats.len(), 0);
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None).unwrap();
assert_eq!(chats.len(), 0);
t.ctx
.set_stock_translation(StockMessage::SavedMessages, "test-1234-save".to_string())
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None).unwrap();
assert_eq!(chats.len(), 1);
t.ctx
.set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string())
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None).unwrap();
assert_eq!(chats.len(), 1);
}
}

View File

@@ -8,7 +8,9 @@ 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;
/// The available configuration keys.
#[derive(
@@ -60,6 +62,9 @@ pub enum Config {
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
ShowEmails,
#[strum(props(default = "0"))]
KeyGenType,
SaveMimeHeaders,
ConfiguredAddr,
ConfiguredMailServer,
@@ -97,7 +102,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),
};
@@ -127,13 +132,23 @@ impl Context {
/// If `None` is passed as a value the value is cleared and set to the default if there is one.
pub fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
match key {
Config::Selfavatar if value.is_some() => {
let blob = BlobObject::create_from_path(&self, value.unwrap())?;
self.sql.set_raw_config(self, key, Some(blob.as_name()))
Config::Selfavatar => {
self.sql
.execute("UPDATE contacts SET selfavatar_sent=0;", NO_PARAMS)?;
self.sql
.set_raw_config_bool(self, "attach_selfavatar", true)?;
match value {
Some(value) => {
let blob = BlobObject::new_from_path(&self, value)?;
blob.recode_to_avatar_size(self)?;
self.sql.set_raw_config(self, key, Some(blob.as_name()))
}
None => self.sql.set_raw_config(self, key, None),
}
}
Config::InboxWatch => {
let ret = self.sql.set_raw_config(self, key, value);
interrupt_inbox_idle(self, true);
interrupt_inbox_idle(self);
ret
}
Config::SentboxWatch => {
@@ -179,7 +194,11 @@ mod tests {
use std::str::FromStr;
use std::string::ToString;
use crate::constants::AVATAR_SIZE;
use crate::test_utils::*;
use image::GenericImageView;
use std::fs::File;
use std::io::Write;
#[test]
fn test_to_string() {
@@ -199,30 +218,55 @@ mod tests {
}
#[test]
fn test_selfavatar() -> failure::Fallible<()> {
fn test_selfavatar_outside_blobdir() {
let t = dummy_context();
let avatar_src = t.dir.path().join("avatar.jpg");
std::fs::write(&avatar_src, b"avatar")?;
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
File::create(&avatar_src)
.unwrap()
.write_all(avatar_bytes)
.unwrap();
let avatar_blob = t.ctx.get_blobdir().join("avatar.jpg");
assert!(!avatar_blob.exists());
t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))?;
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
.unwrap();
assert!(avatar_blob.exists());
assert_eq!(std::fs::read(&avatar_blob)?, b"avatar");
assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64);
let avatar_cfg = t.ctx.get_config(Config::Selfavatar);
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
Ok(())
let img = image::open(avatar_src).unwrap();
assert_eq!(img.width(), 1000);
assert_eq!(img.height(), 1000);
let img = image::open(avatar_blob).unwrap();
assert_eq!(img.width(), AVATAR_SIZE);
assert_eq!(img.height(), AVATAR_SIZE);
}
#[test]
fn test_selfavatar_in_blobdir() -> failure::Fallible<()> {
fn test_selfavatar_in_blobdir() {
let t = dummy_context();
let avatar_src = t.ctx.get_blobdir().join("avatar.jpg");
std::fs::write(&avatar_src, b"avatar")?;
let avatar_src = t.ctx.get_blobdir().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
File::create(&avatar_src)
.unwrap()
.write_all(avatar_bytes)
.unwrap();
let img = image::open(&avatar_src).unwrap();
assert_eq!(img.width(), 900);
assert_eq!(img.height(), 900);
t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))?;
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
.unwrap();
let avatar_cfg = t.ctx.get_config(Config::Selfavatar);
assert_eq!(avatar_cfg, avatar_src.to_str().map(|s| s.to_string()));
Ok(())
let img = image::open(avatar_src).unwrap();
assert_eq!(img.width(), AVATAR_SIZE);
assert_eq!(img.height(), AVATAR_SIZE);
}
}

View File

@@ -6,16 +6,19 @@ mod read_url;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use async_std::task;
use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::e2ee;
use crate::job::*;
use crate::login_param::LoginParam;
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;
@@ -29,34 +32,36 @@ macro_rules! progress {
};
}
// connect
pub fn configure(context: &Context) {
if context.has_ongoing() {
warn!(context, "There is already another ongoing process running.",);
return;
impl Context {
/// Starts a configuration job.
pub fn configure(&self) {
if self.has_ongoing() {
warn!(self, "There is already another ongoing process running.",);
return;
}
job_kill_action(self, job::Action::ConfigureImap);
job_add(self, job::Action::ConfigureImap, 0, Params::new(), 0);
}
job_kill_action(context, Action::ConfigureImap);
job_add(context, Action::ConfigureImap, 0, Params::new(), 0);
}
/// Check if the context is already configured.
pub fn dc_is_configured(context: &Context) -> bool {
context.sql.get_raw_config_bool(context, "configured")
/// Checks if the context is already configured.
pub fn is_configured(&self) -> bool {
self.sql.get_raw_config_bool(self, "configured")
}
}
/*******************************************************************************
* Configure JOB
******************************************************************************/
#[allow(non_snake_case, unused_must_use)]
pub fn JobConfigureImap(context: &Context) {
#[allow(non_snake_case, unused_must_use, clippy::cognitive_complexity)]
pub(crate) fn JobConfigureImap(context: &Context) -> job::Status {
if !context.sql.is_open() {
error!(context, "Cannot configure, database not opened.",);
progress!(context, 0);
return;
return job::Status::Finished(Err(format_err!("Database not opened")));
}
if !context.alloc_ongoing() {
progress!(context, 0);
return;
return job::Status::Finished(Err(format_err!("Cannot allocated ongoing process")));
}
let mut success = false;
let mut imap_connected_here = false;
@@ -92,9 +97,11 @@ pub fn JobConfigureImap(context: &Context) {
let mut param_domain = "undefined.undefined".to_owned();
let mut param_addr_urlencoded: String =
"Internal Error: this value should never be used".to_owned();
let mut keep_flags = std::i32::MAX;
let mut keep_flags = 0;
const STEP_12_USE_AUTOCONFIG: u8 = 12;
const STEP_13_AFTER_AUTOCONFIG: u8 = 13;
const STEP_3_INDEX: u8 = 13;
let mut step_counter: u8 = 0;
while !context.shall_stop_ongoing() {
step_counter += 1;
@@ -110,7 +117,7 @@ pub fn JobConfigureImap(context: &Context) {
}
// Step 1: Load the parameters and check email-address and password
2 => {
if 0 != param.server_flags & 0x2 {
if 0 != param.server_flags & DC_LP_AUTH_OAUTH2 {
// the used oauth2 addr may differ, check this.
// if dc_get_oauth2_addr() is not available in the oauth2 implementation,
// just use the given one.
@@ -145,6 +152,7 @@ pub fn JobConfigureImap(context: &Context) {
// Step 2: Autoconfig
4 => {
progress!(context, 200);
if param.mail_server.is_empty()
&& param.mail_port == 0
/*&&param.mail_user.is_empty() -- the user can enter a loginname which is used by autoconfig then */
@@ -152,12 +160,18 @@ pub fn JobConfigureImap(context: &Context) {
&& param.send_port == 0
&& param.send_user.is_empty()
/*&&param.send_pw.is_empty() -- the password cannot be auto-configured and is no criterion for autoconfig or not */
&& param.server_flags & !0x2 == 0
&& (param.server_flags & !DC_LP_AUTH_OAUTH2) == 0
{
keep_flags = param.server_flags & 0x2;
// no advanced parameters entered by the user: query provider-database or do Autoconfig
keep_flags = param.server_flags & DC_LP_AUTH_OAUTH2;
if let Some(new_param) = get_offline_autoconfig(context, &param) {
// got parameters from our provider-database, skip Autoconfig, preserve the OAuth2 setting
param_autoconfig = Some(new_param);
step_counter = STEP_12_USE_AUTOCONFIG - 1; // minus one as step_counter is increased on next loop
}
} else {
// Autoconfig is not needed so skip it.
step_counter = STEP_3_INDEX - 1;
// advanced parameters entered by the user: skip Autoconfig
step_counter = STEP_13_AFTER_AUTOCONFIG - 1; // minus one as step_counter is increased on next loop
}
true
}
@@ -189,10 +203,7 @@ pub fn JobConfigureImap(context: &Context) {
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
@@ -242,8 +253,10 @@ pub fn JobConfigureImap(context: &Context) {
}
true
}
/* C. Do we have any result? */
12 => {
/* C. Do we have any autoconfig result?
If you change the match-number here, also update STEP_12_COPY_AUTOCONFIG above
*/
STEP_12_USE_AUTOCONFIG => {
progress!(context, 500);
if let Some(ref cfg) = param_autoconfig {
info!(context, "Got autoconfig: {}", &cfg);
@@ -256,15 +269,15 @@ pub fn JobConfigureImap(context: &Context) {
param.send_port = cfg.send_port;
param.send_user = cfg.send_user.clone();
param.server_flags = cfg.server_flags;
/* although param_autoconfig's data are no longer needed from, it is important to keep the object as
we may enter "deep guessing" if we could not read a configuration */
/* although param_autoconfig's data are no longer needed from,
it is used to later to prevent trying variations of port/server/logins */
}
param.server_flags |= keep_flags;
true
}
// Step 3: Fill missing fields with defaults
13 => {
// if you move this, don't forget to update STEP_3_INDEX, too
// If you change the match-number here, also update STEP_13_AFTER_AUTOCONFIG above
STEP_13_AFTER_AUTOCONFIG => {
if param.mail_server.is_empty() {
param.mail_server = format!("imap.{}", param_domain,)
}
@@ -426,8 +439,77 @@ pub fn JobConfigureImap(context: &Context) {
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> {
info!(
context,
"checking internal provider-info for offline autoconfig"
);
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();
let mut p = LoginParam::new();
p.addr = param.addr.clone();
p.mail_server = imap.hostname.to_string();
p.mail_user = imap.apply_username_pattern(param.addr.clone());
p.mail_port = imap.port as i32;
p.imap_certificate_checks = CertificateChecks::AcceptInvalidCertificates;
p.server_flags |= match imap.socket {
provider::Socket::STARTTLS => DC_LP_IMAP_SOCKET_STARTTLS,
provider::Socket::SSL => DC_LP_IMAP_SOCKET_SSL,
};
p.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(
@@ -485,17 +567,22 @@ fn try_imap_connection(
fn try_imap_one_param(context: &Context, param: &LoginParam) -> Option<bool> {
let inf = format!(
"imap: {}@{}:{} flags=0x{:x}",
param.mail_user, param.mail_server, param.mail_port, param.server_flags
"imap: {}@{}:{} flags=0x{:x} certificate_checks={}",
param.mail_user,
param.mail_server,
param.mail_port,
param.server_flags,
param.imap_certificate_checks
);
info!(context, "Trying: {}", inf);
if context
.inbox_thread
.read()
.unwrap()
.imap
.connect(context, &param)
{
if task::block_on(
context
.inbox_thread
.read()
.unwrap()
.imap
.connect(context, &param),
) {
info!(context, "success: {}", inf);
return Some(true);
}
@@ -567,6 +654,7 @@ fn try_smtp_one_param(context: &Context, param: &LoginParam) -> Option<bool> {
#[cfg(test)]
mod tests {
use super::*;
use crate::config::*;
use crate::configure::JobConfigureImap;
use crate::test_utils::*;
@@ -580,4 +668,19 @@ mod tests {
t.ctx.set_config(Config::MailPw, Some("123456")).unwrap();
JobConfigureImap(&t.ctx);
}
#[test]
fn test_get_offline_autoconfig() {
let context = dummy_context().ctx;
let mut params = LoginParam::new();
params.addr = "someone123@example.org".to_string();
assert!(get_offline_autoconfig(&context, &params).is_none());
let mut params = LoginParam::new();
params.addr = "someone123@nauta.cu".to_string();
let found_params = get_offline_autoconfig(&context, &params).unwrap();
assert_eq!(found_params.mail_server, "imap.nauta.cu".to_string());
assert_eq!(found_params.send_server, "smtp.nauta.cu".to_string());
}
}

View File

@@ -11,10 +11,10 @@ pub type Result<T> = std::result::Result<T, Error>;
pub fn read_url(context: &Context, url: &str) -> Result<String> {
info!(context, "Requesting URL {}", url);
match reqwest::Client::new()
match reqwest::blocking::Client::new()
.get(url)
.send()
.and_then(|mut res| res.text())
.and_then(|res| res.text())
{
Ok(res) => Ok(res),
Err(err) => {

View File

@@ -1,8 +1,9 @@
//! # Constants
#![allow(non_camel_case_types, dead_code)]
#![allow(dead_code)]
use deltachat_derive::*;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
lazy_static! {
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
@@ -15,7 +16,20 @@ const DC_SENTBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
)]
#[repr(u8)]
pub enum Blocked {
Not = 0,
@@ -43,12 +57,26 @@ impl Default for ShowEmails {
}
}
pub const DC_IMAP_SEEN: u32 = 0x1;
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(u8)]
pub enum KeyGenType {
Default = 0,
Rsa2048 = 1,
Ed25519 = 2,
}
impl Default for KeyGenType {
fn default() -> Self {
KeyGenType::Default
}
}
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
pub(crate) const DC_FROM_HANDSHAKE: i32 = 0x01;
pub const DC_GCL_ARCHIVED_ONLY: usize = 0x01;
pub const DC_GCL_NO_SPECIALS: usize = 0x02;
pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
@@ -58,9 +86,8 @@ pub const DC_GCM_ADDDAYMARKER: u32 = 0x01;
pub const DC_GCL_VERIFIED_ONLY: usize = 0x01;
pub const DC_GCL_ADD_SELF: usize = 0x02;
// values for DC_PARAM_FORCE_PLAINTEXT
pub(crate) const DC_FP_NO_AUTOCRYPT_HEADER: i32 = 2;
pub(crate) const DC_FP_ADD_AUTOCRYPT_HEADER: i32 = 1;
// unchanged user avatars are resent to the recipients every some days
pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
pub(crate) const DC_CHAT_ID_DEADDROP: u32 = 1;
@@ -89,6 +116,8 @@ pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
FromSql,
ToSql,
IntoStaticStr,
Serialize,
Deserialize,
)]
#[repr(u32)]
pub enum Chattype {
@@ -111,7 +140,7 @@ pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
/// approx. max. length returned by dc_msg_get_text()
const DC_MAX_GET_TEXT_LEN: usize = 30000;
/// approx. max. length returned by dc_get_msg_info()
const DC_MAX_GET_INFO_LEN: usize = 100000;
const DC_MAX_GET_INFO_LEN: usize = 100_000;
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
pub const DC_CONTACT_ID_SELF: u32 = 1;
@@ -119,6 +148,10 @@ pub const DC_CONTACT_ID_INFO: u32 = 2;
pub const DC_CONTACT_ID_DEVICE: u32 = 5;
pub const DC_CONTACT_ID_LAST_SPECIAL: u32 = 9;
// decorative address that is used for DC_CONTACT_ID_DEVICE
// when an api that returns an email is called.
pub const DC_CONTACT_ID_DEVICE_ADDR: &str = "device@localhost";
// Flags for empty server job
pub const DC_EMPTY_MVBOX: u32 = 0x01;
@@ -177,10 +210,27 @@ pub const DC_VC_CONTACT_CONFIRM: i32 = 6;
pub const DC_BOB_ERROR: i32 = 0;
pub const DC_BOB_SUCCESS: i32 = 1;
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
// max. width/height of an avatar
pub const AVATAR_SIZE: u32 = 192;
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
)]
#[repr(i32)]
pub enum Viewtype {
Unknown = 0,
/// Text message.
/// The text of the message is set using dc_msg_set_text()
/// and retrieved with dc_msg_get_text().

View File

@@ -1,3 +1,5 @@
//! Contacts module
use std::path::PathBuf;
use deltachat_derive::*;
@@ -5,16 +7,19 @@ use itertools::Itertools;
use rusqlite;
use crate::aheader::EncryptPreference;
use crate::chat::ChatId;
use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::e2ee;
use crate::error::Result;
use crate::error::{Error, Result};
use crate::events::Event;
use crate::key::*;
use crate::login_param::LoginParam;
use crate::message::{MessageState, MsgId};
use crate::mimeparser::AvatarAction;
use crate::param::*;
use crate::peerstate::*;
use crate::sql;
use crate::stock::StockMessage;
@@ -44,20 +49,28 @@ pub struct Contact {
///
/// Normal contact IDs are larger than these special ones (larger than DC_CONTACT_ID_LAST_SPECIAL).
pub id: u32,
/// Contact name. It is recommended to use `Contact::get_name`,
/// `Contact::get_display_name` or `Contact::get_name_n_addr` to access this field.
/// May be empty, initially set to `authname`.
name: String,
/// Name authorized by the contact himself. Only this name may be spread to others,
/// e.g. in To:-lists. May be empty. It is recommended to use `Contact::get_authname`,
/// to access this field.
authname: String,
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr`` to access this field.
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr` to access this field.
addr: String,
/// Blocked state. Use dc_contact_is_blocked to access this field.
blocked: bool,
pub blocked: bool,
/// The origin/source of the contact.
pub origin: Origin,
/// Parameters as Param::ProfileImage
pub param: Params,
}
/// Possible origins of a contact.
@@ -67,38 +80,54 @@ pub struct Contact {
#[repr(i32)]
pub enum Origin {
Unknown = 0,
/// From: of incoming messages of unknown sender
IncomingUnknownFrom = 0x10,
/// Cc: of incoming messages of unknown sender
IncomingUnknownCc = 0x20,
/// To: of incoming messages of unknown sender
IncomingUnknownTo = 0x40,
/// address scanned but not verified
UnhandledQrScan = 0x80,
/// Reply-To: of incoming message of known sender
IncomingReplyTo = 0x100,
/// Cc: of incoming message of known sender
IncomingCc = 0x200,
/// additional To:'s of incoming message of known sender
IncomingTo = 0x400,
/// a chat was manually created for this user, but no message yet sent
CreateChat = 0x800,
/// message sent by us
OutgoingBcc = 0x1000,
/// message sent by us
OutgoingCc = 0x2000,
/// message sent by us
OutgoingTo = 0x4000,
/// internal use
Internal = 0x40000,
/// address is in our address book
AdressBook = 0x80000,
/// set on Alice's side for contacts like Bob that have scanned the QR code offered by her. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling dc_contact_is_verified() !
SecurejoinInvited = 0x1000000,
SecurejoinInvited = 0x0100_0000,
/// set on Bob's side for contacts scanned and verified from a QR code. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling dc_contact_is_verified() !
SecurejoinJoined = 0x2000000,
/// contact added mannually by dc_create_contact(), this should be the largets origin as otherwise the user cannot modify the names
ManuallyCreated = 0x4000000,
SecurejoinJoined = 0x0200_0000,
/// contact added mannually by dc_create_contact(), this should be the largest origin as otherwise the user cannot modify the names
ManuallyCreated = 0x0400_0000,
}
impl Default for Origin {
@@ -108,19 +137,16 @@ impl Default for Origin {
}
impl Origin {
/// Contacts that are verified and known not to be spam.
pub fn is_verified(self) -> bool {
self as i32 >= 0x100
}
/// Contacts that are shown in the contact list.
pub fn include_in_contactlist(self) -> bool {
self as i32 >= DC_ORIGIN_MIN_CONTACT_LIST
/// Contacts that are known, i. e. they came in via accepted contacts or
/// themselves an accepted contact. Known contacts are shown in the
/// contact list when one creates a chat and wants to add members etc.
pub fn is_known(self) -> bool {
self >= Origin::IncomingReplyTo
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Modifier {
pub(crate) enum Modifier {
None,
Modified,
Created,
@@ -139,32 +165,10 @@ pub enum VerifiedStatus {
impl Contact {
pub fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result<Self> {
if contact_id == DC_CONTACT_ID_SELF {
let contact = Contact {
id: contact_id,
name: context.stock_str(StockMessage::SelfMsg).into(),
authname: "".into(),
addr: context
.get_config(Config::ConfiguredAddr)
.unwrap_or_default(),
blocked: false,
origin: Origin::Unknown,
};
return Ok(contact);
} else if contact_id == DC_CONTACT_ID_DEVICE {
let contact = Contact {
id: contact_id,
name: context.stock_str(StockMessage::DeviceMessages).into(),
authname: "".into(),
addr: "device@localhost".into(),
blocked: false,
origin: Origin::Unknown,
};
return Ok(contact);
}
context.sql.query_row(
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname FROM contacts c WHERE c.id=?;",
let mut res = context.sql.query_row(
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param
FROM contacts c
WHERE c.id=?;",
params![contact_id as i32],
|row| {
let contact = Self {
@@ -174,10 +178,21 @@ impl Contact {
addr: row.get::<_, String>(1)?,
blocked: row.get::<_, Option<i32>>(3)?.unwrap_or_default() != 0,
origin: row.get(2)?,
param: row.get::<_, String>(5)?.parse().unwrap_or_default(),
};
Ok(contact)
}
)
},
)?;
if contact_id == DC_CONTACT_ID_SELF {
res.name = context.stock_str(StockMessage::SelfMsg).to_string();
res.addr = context
.get_config(Config::ConfiguredAddr)
.unwrap_or_default();
} else if contact_id == DC_CONTACT_ID_DEVICE {
res.name = context.stock_str(StockMessage::DeviceMessages).to_string();
res.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
}
Ok(res)
}
/// Returns `true` if this contact is blocked.
@@ -248,7 +263,7 @@ impl Contact {
.is_ok()
{
context.call_cb(Event::MsgsChanged {
chat_id: 0,
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
}
@@ -285,9 +300,31 @@ impl Contact {
}
/// Lookup a contact and create it if it does not exist yet.
/// The contact is identified by the email-address, a name and an "origin" can be given.
///
/// The "origin" is where the address comes from -
/// from-header, cc-header, addressbook, qr, manual-edit etc.
/// In general, "better" origins overwrite the names of "worse" origins -
/// Eg. if we got a name in cc-header and later in from-header, the name will change -
/// this does not happen the other way round.
///
/// The "best" origin are manually created contacts -
/// names given manually can only be overwritten by further manual edits
/// (until they are set empty again or reset to the name seen in the From-header).
///
/// These manually edited names are _never_ used for sending on the wire -
/// this should avoid sending sth. as "Mama" or "Daddy" to some 3rd party.
/// Instead, for the wire, we use so called "authnames"
/// that can only be set and updated by a From-header.
///
/// The different names used in the function are:
/// - "name": name passed as function argument, belonging to the given origin
/// - "row_name": current name used in the database, typically set to "name"
/// - "row_authname": name as authorized from a contact, set only through a From-header
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
///
/// Returns the contact_id and a `Modifier` value indicating if a modification occured.
pub fn add_or_lookup(
pub(crate) fn add_or_lookup(
context: &Context,
name: impl AsRef<str>,
addr: impl AsRef<str>,
@@ -341,7 +378,9 @@ impl Contact {
if !name.as_ref().is_empty() {
if !row_name.is_empty() {
if origin >= row_origin && name.as_ref() != row_name {
if (origin >= row_origin || row_name == row_authname)
&& name.as_ref() != row_name
{
update_name = true;
}
} else {
@@ -350,6 +389,9 @@ impl Contact {
if origin == Origin::IncomingUnknownFrom && name.as_ref() != row_authname {
update_authname = true;
}
} else if origin == Origin::ManuallyCreated && !row_authname.is_empty() {
// no name given on manual edit, this will update the name to the authname
update_name = true;
}
Ok((row_id, row_name, row_addr, row_origin, row_authname))
@@ -360,16 +402,22 @@ impl Contact {
update_addr = true;
}
if update_name || update_authname || update_addr || origin > row_origin {
let new_name = if update_name {
if !name.as_ref().is_empty() {
name.as_ref()
} else {
&row_authname
}
} else {
&row_name
};
sql::execute(
context,
&context.sql,
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
params![
if update_name {
name.as_ref()
} else {
&row_name
},
new_name,
if update_addr { addr } else { &row_addr },
if origin > row_origin {
origin
@@ -387,27 +435,41 @@ impl Contact {
.ok();
if update_name {
// Update the contact name also if it is used as a group name.
// This is one of the few duplicated data, however, getting the chat list is easier this way.
sql::execute(
context,
&context.sql,
"UPDATE chats SET name=? WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
params![name.as_ref(), Chattype::Single, row_id]
params![new_name, Chattype::Single, row_id]
).ok();
}
sth_modified = Modifier::Modified;
}
} else if sql::execute(
context,
&context.sql,
"INSERT INTO contacts (name, addr, origin) VALUES(?, ?, ?);",
params![name.as_ref(), addr, origin,],
)
.is_ok()
{
row_id = sql::get_rowid(context, &context.sql, "contacts", "addr", addr);
sth_modified = Modifier::Created;
} else {
error!(context, "Cannot add contact.");
if origin == Origin::IncomingUnknownFrom {
update_authname = true;
}
if sql::execute(
context,
&context.sql,
"INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);",
params![
name.as_ref(),
addr,
origin,
if update_authname { name.as_ref() } else { "" }
],
)
.is_ok()
{
row_id = sql::get_rowid(context, &context.sql, "contacts", "addr", addr);
sth_modified = Modifier::Created;
info!(context, "added contact id={} addr={}", row_id, addr);
} else {
error!(context, "Cannot add contact.");
}
}
Ok((row_id, sth_modified))
@@ -491,7 +553,7 @@ impl Contact {
params![
self_addr,
DC_CONTACT_ID_LAST_SPECIAL as i32,
0x100,
Origin::IncomingReplyTo,
&s3str_like_cmd,
&s3str_like_cmd,
if flag_verified_only { 0 } else { 1 },
@@ -709,6 +771,16 @@ impl Contact {
Ok(Contact::load_from_db(context, contact_id)?)
}
pub fn update_param(&mut self, context: &Context) -> Result<()> {
sql::execute(
context,
&context.sql,
"UPDATE contacts SET param=? WHERE id=?",
params![self.param.to_string(), self.id as i32],
)?;
Ok(())
}
/// Get the ID of the contact.
pub fn get_id(&self) -> u32 {
self.id
@@ -742,6 +814,9 @@ impl Contact {
if !self.name.is_empty() {
return &self.name;
}
if !self.authname.is_empty() {
return &self.authname;
}
&self.addr
}
@@ -777,8 +852,11 @@ impl Contact {
if let Some(p) = context.get_config(Config::Selfavatar) {
return Some(PathBuf::from(p));
}
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
return Some(dc_get_abs_path(context, image_rel));
}
}
// TODO: else get image_abs from contact param
None
}
@@ -861,22 +939,6 @@ impl Contact {
.unwrap_or_default() as usize
}
pub fn get_origin_by_id(context: &Context, contact_id: u32, ret_blocked: &mut i32) -> Origin {
let mut ret = Origin::Unknown;
*ret_blocked = 0;
if let Ok(contact) = Contact::load_from_db(context, contact_id) {
/* we could optimize this by loading only the needed fields */
if contact.blocked {
*ret_blocked = 1;
} else {
ret = contact.origin;
}
}
ret
}
pub fn real_exists_by_id(context: &Context, contact_id: u32) -> bool {
if !context.sql.is_open() || contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return false;
@@ -957,6 +1019,31 @@ fn set_block_contact(context: &Context, contact_id: u32, new_blocking: bool) {
}
}
pub(crate) fn set_profile_image(
context: &Context,
contact_id: u32,
profile_image: &AvatarAction,
) -> Result<()> {
// the given profile image is expected to be already in the blob directory
// as profile images can be set only by receiving messages, this should be always the case, however.
let mut contact = Contact::load_from_db(context, contact_id)?;
let changed = match profile_image {
AvatarAction::Change(profile_image) => {
contact.param.set(Param::ProfileImage, profile_image);
true
}
AvatarAction::Delete => {
contact.param.remove(Param::ProfileImage);
true
}
};
if changed {
contact.update_param(context)?;
context.call_cb(Event::ContactsChanged(Some(contact_id)));
}
Ok(())
}
/// Normalize a name.
///
/// - Remove quotes (come from some bad MUA implementations)
@@ -1021,6 +1108,18 @@ fn cat_fingerprint(
}
}
impl Context {
/// determine whether the specified addr maps to the/a self addr
pub fn is_self_addr(&self, addr: &str) -> Result<bool> {
let self_addr = match self.get_config(Config::ConfiguredAddr) {
Some(s) => s,
None => return Err(Error::NotConfigured),
};
Ok(addr_cmp(self_addr, addr))
}
}
pub fn addr_cmp(addr1: impl AsRef<str>, addr2: impl AsRef<str>) -> bool {
let norm1 = addr_normalize(addr1.as_ref()).to_lowercase();
let norm2 = addr_normalize(addr2.as_ref()).to_lowercase();
@@ -1028,15 +1127,6 @@ pub fn addr_cmp(addr1: impl AsRef<str>, addr2: impl AsRef<str>) -> bool {
norm1 == norm2
}
pub fn addr_equals_self(context: &Context, addr: impl AsRef<str>) -> bool {
if !addr.as_ref().is_empty() {
if let Some(self_addr) = context.get_config(Config::ConfiguredAddr) {
return addr_cmp(addr, self_addr);
}
}
false
}
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
book.lines()
.chunks(2)
@@ -1120,6 +1210,18 @@ mod tests {
assert_eq!(contacts.len(), 0);
}
#[test]
fn test_is_self_addr() -> Result<()> {
let t = test_context(None);
assert!(t.ctx.is_self_addr("me@me.org").is_err());
let addr = configure_alice_keypair(&t.ctx);
assert_eq!(t.ctx.is_self_addr("me@me.org")?, false);
assert_eq!(t.ctx.is_self_addr(&addr)?, true);
Ok(())
}
#[test]
fn test_add_or_lookup() {
// add some contacts, this also tests add_address_book()
@@ -1209,6 +1311,139 @@ mod tests {
assert!(!contact.is_blocked());
}
#[test]
fn test_remote_authnames() {
let t = dummy_context();
// incoming mail `From: bob1 <bob@example.org>` - this should init authname and name
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"bob1",
"bob@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::Created);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "bob1");
assert_eq!(contact.get_name(), "bob1");
assert_eq!(contact.get_display_name(), "bob1");
// incoming mail `From: bob2 <bob@example.org>` - this should update authname and name
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"bob2",
"bob@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "bob2");
assert_eq!(contact.get_name(), "bob2");
assert_eq!(contact.get_display_name(), "bob2");
// manually edit name to "bob3" - authname should be still be "bob2" a given in `From:` above
let contact_id = Contact::create(&t.ctx, "bob3", "bob@example.org").unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "bob2");
assert_eq!(contact.get_name(), "bob3");
assert_eq!(contact.get_display_name(), "bob3");
// incoming mail `From: bob4 <bob@example.org>` - this should update authname, manually given name is still "bob3"
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"bob4",
"bob@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "bob4");
assert_eq!(contact.get_name(), "bob3");
assert_eq!(contact.get_display_name(), "bob3");
}
#[test]
fn test_remote_authnames_create_empty() {
let t = dummy_context();
// manually create "claire@example.org" without a given name
let contact_id = Contact::create(&t.ctx, "", "claire@example.org").unwrap();
assert!(contact_id > DC_CONTACT_ID_LAST_SPECIAL);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "claire@example.org");
// incoming mail `From: claire1 <claire@example.org>` - this should update authname and name
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"claire1",
"claire@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "claire1");
assert_eq!(contact.get_name(), "claire1");
assert_eq!(contact.get_display_name(), "claire1");
// incoming mail `From: claire2 <claire@example.org>` - this should update authname and name
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t.ctx,
"claire2",
"claire@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "claire2");
assert_eq!(contact.get_name(), "claire2");
assert_eq!(contact.get_display_name(), "claire2");
}
#[test]
fn test_remote_authnames_edit_empty() {
let t = dummy_context();
// manually create "dave@example.org"
let contact_id = Contact::create(&t.ctx, "dave1", "dave@example.org").unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "dave1");
assert_eq!(contact.get_display_name(), "dave1");
// incoming mail `From: dave2 <dave@example.org>` - this should update authname
Contact::add_or_lookup(
&t.ctx,
"dave2",
"dave@example.org",
Origin::IncomingUnknownFrom,
)
.unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "dave2");
assert_eq!(contact.get_name(), "dave1");
assert_eq!(contact.get_display_name(), "dave1");
// manually clear the name
Contact::create(&t.ctx, "", "dave@example.org").unwrap();
let contact = Contact::load_from_db(&t.ctx, contact_id).unwrap();
assert_eq!(contact.get_authname(), "dave2");
assert_eq!(contact.get_name(), "dave2");
assert_eq!(contact.get_display_name(), "dave2");
}
#[test]
fn test_addr_cmp() {
assert!(addr_cmp("AA@AA.ORG", "aa@aa.ORG"));

View File

@@ -1,12 +1,10 @@
//! Contacts module
//! Context module
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Condvar, Mutex, RwLock};
use libc::uintptr_t;
use crate::chat::*;
use crate::config::Config;
use crate::constants::*;
@@ -16,10 +14,10 @@ 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, MsgId};
use crate::message::{self, Message, MessengerMessage, MsgId};
use crate::param::Params;
use crate::smtp::Smtp;
use crate::sql::Sql;
@@ -32,12 +30,7 @@ use crate::sql::Sql;
/// * `event` - One of the [Event] items.
/// * `data1` - Depends on the event parameter, see [Event].
/// * `data2` - Depends on the event parameter, see [Event].
///
/// # Returns
///
/// This callback must return 0 unless stated otherwise in the event
/// description at [Event].
pub type ContextCallback = dyn Fn(&Context, Event) -> uintptr_t + Send + Sync;
pub type ContextCallback = dyn Fn(&Context, Event) -> () + Send + Sync;
#[derive(DebugStub)]
pub struct Context {
@@ -57,8 +50,8 @@ pub struct Context {
#[debug_stub = "Callback"]
cb: Box<ContextCallback>,
pub os_name: Option<String>,
pub cmdline_sel_chat_id: Arc<RwLock<u32>>,
pub bob: Arc<RwLock<BobStatus>>,
pub cmdline_sel_chat_id: Arc<RwLock<ChatId>>,
pub(crate) bob: Arc<RwLock<BobStatus>>,
pub last_smeared_timestamp: RwLock<i64>,
pub running_state: Arc<RwLock<RunningState>>,
/// Mutex to avoid generating the key for the user more than once.
@@ -82,16 +75,7 @@ pub fn get_info() -> HashMap<&'static str, String> {
let mut res = HashMap::new();
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("sqlite_version", rusqlite::version().to_string());
res.insert(
"sqlite_thread_safe",
unsafe { rusqlite::ffi::sqlite3_threadsafe() }.to_string(),
);
res.insert(
"arch",
(std::mem::size_of::<*mut libc::c_void>())
.wrapping_mul(8)
.to_string(),
);
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
res.insert("level", "awesome".into());
res
}
@@ -99,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");
@@ -132,7 +118,7 @@ impl Context {
oauth2_critical: Arc::new(Mutex::new(())),
bob: Arc::new(RwLock::new(Default::default())),
last_smeared_timestamp: RwLock::new(0),
cmdline_sel_chat_id: Arc::new(RwLock::new(0)),
cmdline_sel_chat_id: Arc::new(RwLock::new(ChatId::new(0))),
inbox_thread: Arc::new(RwLock::new(JobThread::new(
"INBOX",
"configured_inbox_folder",
@@ -172,8 +158,8 @@ impl Context {
self.blobdir.as_path()
}
pub fn call_cb(&self, event: Event) -> uintptr_t {
(*self.cb)(self, event)
pub fn call_cb(&self, event: Event) {
(*self.cb)(self, event);
}
/*******************************************************************************
@@ -298,6 +284,11 @@ impl Context {
res.insert("database_version", dbversion.to_string());
res.insert("blobdir", self.get_blobdir().display().to_string());
res.insert("display_name", displayname.unwrap_or_else(|| unset.into()));
res.insert(
"selfavatar",
self.get_config(Config::Selfavatar)
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert("is_configured", is_configured.to_string());
res.insert("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2.to_string());
@@ -356,7 +347,7 @@ impl Context {
}
#[allow(non_snake_case)]
pub fn search_msgs(&self, chat_id: u32, query: impl AsRef<str>) -> Vec<MsgId> {
pub fn search_msgs(&self, chat_id: ChatId, query: impl AsRef<str>) -> Vec<MsgId> {
let real_query = query.as_ref().trim();
if real_query.is_empty() {
return Vec::new();
@@ -364,7 +355,7 @@ impl Context {
let strLikeInText = format!("%{}%", real_query);
let strLikeBeg = format!("{}%", real_query);
let query = if 0 != chat_id {
let query = if !chat_id.is_unset() {
concat!(
"SELECT m.id AS id, m.timestamp AS timestamp",
" FROM msgs m",
@@ -396,7 +387,7 @@ impl Context {
self.sql
.query_map(
query,
params![chat_id as i32, &strLikeInText, &strLikeBeg],
params![chat_id, &strLikeInText, &strLikeBeg],
|row| row.get::<_, MsgId>("id"),
|rows| {
let mut ret = Vec::new();
@@ -447,15 +438,17 @@ impl Context {
return;
}
// 1 = dc message, 2 = reply to dc message
if 0 != msg.is_dc_message {
job_add(
self,
Action::MoveMsg,
msg.id.to_u32() as i32,
Params::new(),
0,
);
match msg.is_dc_message {
MessengerMessage::No => {}
MessengerMessage::Yes | MessengerMessage::Reply => {
job_add(
self,
Action::MoveMsg,
msg.id.to_u32() as i32,
Params::new(),
0,
);
}
}
}
}
@@ -485,14 +478,14 @@ impl Default for RunningState {
}
#[derive(Debug, Default)]
pub struct BobStatus {
pub(crate) struct BobStatus {
pub expects: i32,
pub status: i32,
pub qr_scan: Option<Lot>,
}
#[derive(Debug, PartialEq)]
pub enum PerformJobsNeeded {
pub(crate) enum PerformJobsNeeded {
Not,
AtOnce,
AvoidDos,
@@ -509,7 +502,7 @@ pub struct SmtpState {
pub idle: bool,
pub suspended: bool,
pub doing_jobs: bool,
pub perform_jobs_needed: PerformJobsNeeded,
pub(crate) perform_jobs_needed: PerformJobsNeeded,
pub probe_network: bool,
}
@@ -528,7 +521,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
std::fs::write(&dbfile, b"123").unwrap();
let res = Context::new(Box::new(|_, _| 0), "FakeOs".into(), dbfile);
let res = Context::new(Box::new(|_, _| ()), "FakeOs".into(), dbfile);
assert!(res.is_err());
}
@@ -543,7 +536,7 @@ mod tests {
fn test_blobdir_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
Context::new(Box::new(|_, _| 0), "FakeOS".into(), dbfile).unwrap();
Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile).unwrap();
let blobdir = tmp.path().join("db.sqlite-blobs");
assert!(blobdir.is_dir());
}
@@ -554,7 +547,7 @@ mod tests {
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("db.sqlite-blobs");
std::fs::write(&blobdir, b"123").unwrap();
let res = Context::new(Box::new(|_, _| 0), "FakeOS".into(), dbfile);
let res = Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile);
assert!(res.is_err());
}
@@ -564,7 +557,7 @@ mod tests {
let subdir = tmp.path().join("subdir");
let dbfile = subdir.join("db.sqlite");
let dbfile2 = dbfile.clone();
Context::new(Box::new(|_, _| 0), "FakeOS".into(), dbfile).unwrap();
Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile).unwrap();
assert!(subdir.is_dir());
assert!(dbfile2.is_file());
}
@@ -574,7 +567,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir(Box::new(|_, _| 0), "FakeOS".into(), dbfile, blobdir);
let res = Context::with_blobdir(Box::new(|_, _| ()), "FakeOS".into(), dbfile, blobdir);
assert!(res.is_err());
}
@@ -583,7 +576,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs");
let res = Context::with_blobdir(Box::new(|_, _| 0), "FakeOS".into(), dbfile, blobdir);
let res = Context::with_blobdir(Box::new(|_, _| ()), "FakeOS".into(), dbfile, blobdir);
assert!(res.is_err());
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,273 +0,0 @@
use crate::dehtml::*;
#[derive(Copy, Clone)]
pub struct Simplify {
pub is_forwarded: bool,
}
/// Return index of footer line in vector of message lines, or vector length if
/// no footer is found.
///
/// Also return whether not-standard (rfc3676, §4.3) footer is found.
fn find_message_footer(lines: &[&str]) -> (usize, bool) {
for (ix, &line) in lines.iter().enumerate() {
// quoted-printable may encode `-- ` to `-- =20` which is converted
// back to `-- `
match line {
"-- " | "-- " => return (ix, false),
"--" | "---" | "----" => return (ix, true),
_ => (),
}
}
(lines.len(), false)
}
impl Simplify {
pub fn new() -> Self {
Simplify {
is_forwarded: false,
}
}
/// Simplify and normalise text: Remove quotes, signatures, unnecessary
/// lineends etc.
/// The data returned from simplify() must be free()'d when no longer used.
pub fn simplify(&mut self, input: &str, is_html: bool, is_msgrmsg: bool) -> String {
let mut out = if is_html {
dehtml(input)
} else {
input.to_string()
};
out.retain(|c| c != '\r');
out = self.simplify_plain_text(&out, is_msgrmsg);
out.retain(|c| c != '\r');
out
}
/**
* Simplify Plain Text
*/
#[allow(non_snake_case, clippy::mut_range_bound)]
fn simplify_plain_text(&mut self, buf_terminated: &str, is_msgrmsg: bool) -> String {
/* This function ...
... removes all text after the line `-- ` (footer mark)
... removes full quotes at the beginning and at the end of the text -
these are all lines starting with the character `>`
... remove a non-empty line before the removed quote (contains sth. like "On 2.9.2016, Bjoern wrote:" in different formats and lanugages) */
/* split the given buffer into lines */
let lines: Vec<_> = buf_terminated.split('\n').collect();
let mut l_first: usize = 0;
let mut is_cut_at_begin = false;
let (mut l_last, mut is_cut_at_end) = find_message_footer(&lines);
if l_last > l_first + 2 {
let line0 = lines[l_first];
let line1 = lines[l_first + 1];
let line2 = lines[l_first + 2];
if line0 == "---------- Forwarded message ----------"
&& line1.starts_with("From: ")
&& line2.is_empty()
{
self.is_forwarded = true;
l_first += 3
}
}
for l in l_first..l_last {
let line = lines[l];
if line == "-----"
|| line == "_____"
|| line == "====="
|| line == "*****"
|| line == "~~~~~"
{
l_last = l;
is_cut_at_end = true;
/* done */
break;
}
}
if !is_msgrmsg {
let mut l_lastQuotedLine = None;
for l in (l_first..l_last).rev() {
let line = lines[l];
if is_plain_quote(line) {
l_lastQuotedLine = Some(l)
} else if !is_empty_line(line) {
break;
}
}
if let Some(last_quoted_line) = l_lastQuotedLine {
l_last = last_quoted_line;
is_cut_at_end = true;
if l_last > 1 && is_empty_line(lines[l_last - 1]) {
l_last -= 1
}
if l_last > 1 {
let line = lines[l_last - 1];
if is_quoted_headline(line) {
l_last -= 1
}
}
}
}
if !is_msgrmsg {
let mut l_lastQuotedLine_0 = None;
let mut hasQuotedHeadline = 0;
for l in l_first..l_last {
let line = lines[l];
if is_plain_quote(line) {
l_lastQuotedLine_0 = Some(l)
} else if !is_empty_line(line) {
if is_quoted_headline(line)
&& 0 == hasQuotedHeadline
&& l_lastQuotedLine_0.is_none()
{
hasQuotedHeadline = 1i32
} else {
/* non-quoting line found */
break;
}
}
}
if let Some(last_quoted_line) = l_lastQuotedLine_0 {
l_first = last_quoted_line + 1;
is_cut_at_begin = true
}
}
/* re-create buffer from the remaining lines */
let mut ret = String::new();
if is_cut_at_begin {
ret += "[...]";
}
/* we write empty lines only in case and non-empty line follows */
let mut pending_linebreaks = 0;
let mut content_lines_added = 0;
for l in l_first..l_last {
let line = lines[l];
if is_empty_line(line) {
pending_linebreaks += 1
} else {
if 0 != content_lines_added {
if pending_linebreaks > 2i32 {
pending_linebreaks = 2i32
}
while 0 != pending_linebreaks {
ret += "\n";
pending_linebreaks -= 1
}
}
// the incoming message might contain invalid UTF8
ret += line;
content_lines_added += 1;
pending_linebreaks = 1i32
}
}
if is_cut_at_end && (!is_cut_at_begin || 0 != content_lines_added) {
ret += " [...]";
}
ret
}
}
/**
* Tools
*/
fn is_empty_line(buf: &str) -> bool {
// XXX: can it be simplified to buf.chars().all(|c| c.is_whitespace())?
//
// Strictly speaking, it is not equivalent (^A is not whitespace, but less than ' '),
// but having control sequences in email body?!
//
// See discussion at: https://github.com/deltachat/deltachat-core-rust/pull/402#discussion_r317062392
for c in buf.chars() {
if c > ' ' {
return false;
}
}
true
}
fn is_quoted_headline(buf: &str) -> bool {
/* This function may be called for the line _directly_ before a quote.
The function checks if the line contains sth. like "On 01.02.2016, xy@z wrote:" in various languages.
- Currently, we simply check if the last character is a ':'.
- Checking for the existence of an email address may fail (headlines may show the user's name instead of the address) */
buf.len() <= 80 && buf.ends_with(':')
}
fn is_plain_quote(buf: &str) -> bool {
buf.starts_with('>')
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
// proptest does not support [[:graphical:][:space:]] regex.
fn test_simplify_plain_text_fuzzy(input in "[!-~\t \n]+") {
let output = Simplify::new().simplify_plain_text(&input, true);
assert!(output.split('\n').all(|s| s != "-- "));
}
}
#[test]
fn test_simplify_trim() {
let mut simplify = Simplify::new();
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2\n\r";
let plain = simplify.simplify(html, true, false);
assert_eq!(plain, "line1\nline2");
}
#[test]
fn test_simplify_parse_href() {
let mut simplify = Simplify::new();
let html = "<a href=url>text</a";
let plain = simplify.simplify(html, true, false);
assert_eq!(plain, "[text](url)");
}
#[test]
fn test_simplify_bold_text() {
let mut simplify = Simplify::new();
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
let plain = simplify.simplify(html, true, false);
assert_eq!(plain, "text *bold*<>");
}
#[test]
fn test_simplify_html_encoded() {
let mut simplify = Simplify::new();
let html =
"&lt;&gt;&quot;&apos;&amp; &auml;&Auml;&ouml;&Ouml;&uuml;&Uuml;&szlig; foo&AElig;&ccedil;&Ccedil; &diams;&lrm;&rlm;&zwnj;&noent;&zwj;";
let plain = simplify.simplify(html, true, false);
assert_eq!(
plain,
"<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}"
);
}
#[test]
fn test_simplify_utilities() {
assert!(is_empty_line(" \t"));
assert!(is_empty_line(""));
assert!(is_empty_line(" \r"));
assert!(!is_empty_line(" x"));
assert!(is_plain_quote("> hello world"));
assert!(is_plain_quote(">>"));
assert!(!is_plain_quote("Life is pain"));
assert!(!is_plain_quote(""));
}
}

View File

@@ -49,8 +49,8 @@ pub(crate) fn dc_truncate(buf: &str, approx_chars: usize, do_unwrap: bool) -> Co
/// - harmonize together while being different enough
/// (therefore, we cannot just use random rgb colors :)
const COLORS: [u32; 16] = [
0xe56555, 0xf28c48, 0x8e85ee, 0x76c84d, 0x5bb6cc, 0x549cdd, 0xd25c99, 0xb37800, 0xf23030,
0x39b249, 0xbb243b, 0x964078, 0x66874f, 0x308ab9, 0x127ed0, 0xbe450c,
0xe5_65_55, 0xf2_8c_48, 0x8e_85_ee, 0x76_c8_4d, 0x5b_b6_cc, 0x54_9c_dd, 0xd2_5c_99, 0xb3_78_00,
0xf2_30_30, 0x39_b2_49, 0xbb_24_3b, 0x96_40_78, 0x66_87_4f, 0x30_8a_b9, 0x12_7e_d0, 0xbe_45_0c,
];
pub(crate) fn dc_str_to_color(s: impl AsRef<str>) -> u32 {
@@ -59,7 +59,7 @@ pub(crate) fn dc_str_to_color(s: impl AsRef<str>) -> u32 {
let bytes = str_lower.as_bytes();
for (i, byte) in bytes.iter().enumerate() {
checksum += (i + 1) * *byte as usize;
checksum %= 0xffffff;
checksum %= 0x00ff_ffff;
}
let color_index = checksum % COLORS.len();
@@ -182,22 +182,6 @@ fn encode_66bits_as_base64(v1: u32, v2: u32, fill: u32) -> String {
String::from_utf8(wrapped_writer).unwrap()
}
pub(crate) fn dc_create_incoming_rfc724_mid(
message_timestamp: i64,
contact_id_from: u32,
contact_ids_to: &[u32],
) -> Option<String> {
/* create a deterministic rfc724_mid from input such that
repeatedly calling it with the same input results in the same Message-id */
let largest_id_to = contact_ids_to.iter().max().copied().unwrap_or_default();
let result = format!(
"{}-{}-{}@stub",
message_timestamp, contact_id_from, largest_id_to
);
Some(result)
}
/// Function generates a Message-ID that can be used for a new outgoing message.
/// - this function is called for all outgoing messages.
/// - the message ID should be globally unique
@@ -238,45 +222,7 @@ 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
#[allow(non_snake_case)]
pub fn dc_get_filesuffix_lc(path_filename: impl AsRef<str>) -> Option<String> {
Path::new(path_filename.as_ref())
.extension()
@@ -397,11 +343,14 @@ pub(crate) fn dc_copy_file(
}
}
pub(crate) fn dc_create_folder(context: &Context, path: impl AsRef<std::path::Path>) -> bool {
pub(crate) fn dc_create_folder(
context: &Context,
path: impl AsRef<std::path::Path>,
) -> Result<(), std::io::Error> {
let path_abs = dc_get_abs_path(context, &path);
if !path_abs.exists() {
match fs::create_dir_all(path_abs) {
Ok(_) => true,
Ok(_) => Ok(()),
Err(err) => {
warn!(
context,
@@ -409,11 +358,11 @@ pub(crate) fn dc_create_folder(context: &Context, path: impl AsRef<std::path::Pa
path.as_ref().display(),
err
);
false
Err(err)
}
}
} else {
true
Ok(())
}
}
@@ -499,7 +448,7 @@ pub(crate) fn dc_get_next_backup_path(
pub(crate) fn time() -> i64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.unwrap_or_default()
.as_secs() as i64
}
@@ -519,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,
@@ -667,7 +616,6 @@ mod tests {
);
}
#[test]
#[test]
fn test_dc_extract_grpid_from_rfc724_mid() {
// Should return None if we pass invalid mid
@@ -722,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::*;
@@ -780,27 +728,6 @@ mod tests {
}
}
#[test]
fn test_dc_create_incoming_rfc724_mid() {
let res = dc_create_incoming_rfc724_mid(123, 45, &[6, 7]);
assert_eq!(res, Some("123-45-7@stub".into()));
let res = dc_create_incoming_rfc724_mid(123, 45, &[]);
assert_eq!(res, Some("123-45-0@stub".into()));
}
#[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();
@@ -849,7 +776,7 @@ mod tests {
assert!(dc_delete_file(context, "$BLOBDIR/foobar"));
assert!(dc_delete_file(context, "$BLOBDIR/dada"));
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder"));
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder").is_ok());
assert!(dc_file_exist(context, "$BLOBDIR/foobar-folder",));
assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder"));

View File

@@ -139,13 +139,12 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
dehtml.add_text = AddText::YesPreserveLineEnds;
}
"a" => {
if let Some(href) = event.html_attributes().find(|attr| {
attr.as_ref()
.map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "href")
.unwrap_or_default()
}) {
if let Some(href) = event
.html_attributes()
.filter_map(|attr| attr.ok())
.find(|attr| String::from_utf8_lossy(attr.key).trim().to_lowercase() == "href")
{
let href = href
.unwrap()
.unescape_and_decode_value(reader)
.unwrap_or_default()
.to_lowercase();
@@ -189,4 +188,41 @@ mod tests {
assert_eq!(dehtml(input), output);
}
}
#[test]
fn test_dehtml_parse_br() {
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2\n\r";
let plain = dehtml(html);
assert_eq!(plain, "line1\n\r\r\rline2");
}
#[test]
fn test_dehtml_parse_href() {
let html = "<a href=url>text</a";
let plain = dehtml(html);
assert_eq!(plain, "[text](url)");
}
#[test]
fn test_dehtml_bold_text() {
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
let plain = dehtml(html);
assert_eq!(plain, "text *bold*<>");
}
#[test]
fn test_dehtml_html_encoded() {
let html =
"&lt;&gt;&quot;&apos;&amp; &auml;&Auml;&ouml;&Ouml;&uuml;&Uuml;&szlig; foo&AElig;&ccedil;&Ccedil; &diams;&lrm;&rlm;&zwnj;&noent;&zwj;";
let plain = dehtml(html);
assert_eq!(
plain,
"<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}"
);
}
}

View File

@@ -1,26 +1,29 @@
//! End-to-end encryption support.
use std::collections::HashSet;
use std::convert::TryFrom;
use mailparse::MailHeaderMap;
use mailparse::ParsedMail;
use num_traits::FromPrimitive;
use crate::aheader::*;
use crate::config::Config;
use crate::constants::KeyGenType;
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;
use crate::securejoin::handle_degrade_event;
use crate::wrapmime;
#[derive(Debug)]
pub struct EncryptHelper {
pub prefer_encrypt: EncryptPreference,
pub addr: String,
pub public_key: Key,
pub public_key: SignedPublicKey,
}
impl EncryptHelper {
@@ -103,8 +106,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"))?;
@@ -118,12 +121,12 @@ impl EncryptHelper {
pub fn try_decrypt(
context: &Context,
mail: &mailparse::ParsedMail<'_>,
mail: &ParsedMail<'_>,
message_time: i64,
) -> 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)
@@ -192,49 +195,67 @@ 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();
let keygen_type =
KeyGenType::from_i32(context.get_config_int(Config::KeyGenType)).unwrap_or_default();
info!(context, "Generating keypair with type {}", keygen_type);
let keypair = pgp::create_keypair(EmailAddress::new(self_addr.as_ref())?, keygen_type)?;
key::store_self_keypair(context, &keypair, KeyPairUse::Default)?;
info!(
context,
"Generating keypair with {} bits, e={} ...", 2048, 65537,
"Keypair generated in {:.3}s.",
start.elapsed().as_secs()
);
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")),
}
Ok(keypair.public)
}
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail<'b>> {
ensure!(
mail.ctype.mimetype == "multipart/encrypted",
"Not a multipart/encrypted message: {}",
mail.ctype.mimetype
);
ensure!(
mail.subparts.len() == 2,
"Invalid Autocrypt Level 1 Mime Parts"
);
ensure!(
mail.subparts[0].ctype.mimetype == "application/pgp-encrypted",
"Invalid Autocrypt Level 1 version part: {:?}",
mail.subparts[0].ctype,
);
ensure!(
mail.subparts[1].ctype.mimetype == "application/octet-stream",
"Invalid Autocrypt Level 1 encrypted part: {:?}",
mail.subparts[1].ctype
);
Ok(&mail.subparts[1])
}
fn decrypt_if_autocrypt_message<'a>(
context: &Context,
mail: &mailparse::ParsedMail<'a>,
mail: &ParsedMail<'a>,
private_keyring: &Keyring,
public_keyring_for_validate: &Keyring,
ret_valid_signatures: &mut HashSet<String>,
@@ -246,14 +267,14 @@ fn decrypt_if_autocrypt_message<'a>(
//
// Errors are returned for failures related to decryption of AC-messages.
let encrypted_data_part = match wrapmime::get_autocrypt_mime(mail) {
Err(err) => {
// not a proper autocrypt message, abort and ignore
info!(context, "Not an autocrypt message: {:?}", err);
let encrypted_data_part = match get_autocrypt_mime(mail) {
Err(_) => {
// not an autocrypt mime message, abort and ignore
return Ok(None);
}
Ok(res) => res,
};
info!(context, "Detected Autocrypt-mime message");
decrypt_part(
context,
@@ -267,7 +288,7 @@ fn decrypt_if_autocrypt_message<'a>(
/// Returns Ok(None) if nothing encrypted was found.
fn decrypt_part(
_context: &Context,
mail: &mailparse::ParsedMail<'_>,
mail: &ParsedMail<'_>,
private_keyring: &Keyring,
public_keyring_for_validate: &Keyring,
ret_valid_signatures: &mut HashSet<String>,
@@ -313,7 +334,7 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
/// However, Delta Chat itself has no problem with encrypted multipart/report
/// parts and MUAs should be encouraged to encrpyt multipart/reports as well so
/// that we could use the normal Autocrypt processing.
fn contains_report(mail: &mailparse::ParsedMail<'_>) -> bool {
fn contains_report(mail: &ParsedMail<'_>) -> bool {
mail.ctype.mimetype == "multipart/report"
}

View File

@@ -6,34 +6,57 @@ use lettre_email::mime;
pub enum Error {
#[fail(display = "{:?}", _0)]
Failure(failure::Error),
#[fail(display = "SQL error: {:?}", _0)]
SqlError(#[cause] crate::sql::Error),
#[fail(display = "{:?}", _0)]
Io(std::io::Error),
#[fail(display = "{:?}", _0)]
Message(String),
#[fail(display = "{:?}", _0)]
MessageWithCause(String, #[cause] failure::Error, failure::Backtrace),
#[fail(display = "{:?}", _0)]
Image(image_meta::ImageError),
#[fail(display = "{:?}", _0)]
Utf8(std::str::Utf8Error),
#[fail(display = "PGP: {:?}", _0)]
Pgp(pgp::errors::Error),
#[fail(display = "Base64Decode: {:?}", _0)]
Base64Decode(base64::DecodeError),
#[fail(display = "{:?}", _0)]
FromUtf8(std::string::FromUtf8Error),
#[fail(display = "{}", _0)]
BlobError(#[cause] crate::blob::BlobError),
#[fail(display = "Invalid Message ID.")]
InvalidMsgId,
#[fail(display = "Watch folder not found {:?}", _0)]
WatchFolderNotFound(String),
#[fail(display = "Inalid Email: {:?}", _0)]
#[fail(display = "Invalid Email: {:?}", _0)]
MailParseError(#[cause] mailparse::MailParseError),
#[fail(display = "Building invalid Email: {:?}", _0)]
LettreError(#[cause] lettre_email::error::Error),
#[fail(display = "SMTP error: {:?}", _0)]
SmtpError(#[cause] async_smtp::error::Error),
#[fail(display = "FromStr error: {:?}", _0)]
FromStr(#[cause] mime::FromStrError),
#[fail(display = "Not Configured")]
NotConfigured,
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -98,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

@@ -4,6 +4,7 @@ use std::path::PathBuf;
use strum::EnumProperty;
use crate::chat::ChatId;
use crate::message::MsgId;
impl Event {
@@ -21,56 +22,38 @@ pub enum Event {
/// The library-user may write an informational string to the log.
/// Passed to the callback given to dc_context_new().
/// This event should not be reported to the end-user using a popup or something like that.
///
/// @return 0
#[strum(props(id = "100"))]
Info(String),
/// Emitted when SMTP connection is established and login was successful.
///
/// @return 0
#[strum(props(id = "101"))]
SmtpConnected(String),
/// Emitted when IMAP connection is established and login was successful.
///
/// @return 0
#[strum(props(id = "102"))]
ImapConnected(String),
/// Emitted when a message was successfully sent to the SMTP server.
///
/// @return 0
#[strum(props(id = "103"))]
SmtpMessageSent(String),
/// Emitted when an IMAP message has been marked as deleted
///
/// @return 0
#[strum(props(id = "104"))]
ImapMessageDeleted(String),
/// Emitted when an IMAP message has been moved
///
/// @return 0
#[strum(props(id = "105"))]
ImapMessageMoved(String),
/// Emitted when an IMAP folder was emptied
///
/// @return 0
#[strum(props(id = "106"))]
ImapFolderEmptied(String),
/// Emitted when an new file in the $BLOBDIR was created
///
/// @return 0
#[strum(props(id = "150"))]
NewBlobFile(String),
/// Emitted when an new file in the $BLOBDIR was created
///
/// @return 0
#[strum(props(id = "151"))]
DeletedBlobFile(String),
@@ -78,8 +61,6 @@ pub enum Event {
/// Passed to the callback given to dc_context_new().
///
/// This event should not be reported to the end-user using a popup or something like that.
///
/// @return 0
#[strum(props(id = "300"))]
Warning(String),
@@ -94,8 +75,6 @@ pub enum Event {
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the *last* error
/// in a messasge box then.
///
/// @return
#[strum(props(id = "400"))]
Error(String),
@@ -112,8 +91,6 @@ pub enum Event {
/// Moreover, if the UI detects that the device is offline,
/// it is probably more useful to report this to the user
/// instead of the string from data2.
///
/// @return 0
#[strum(props(id = "401"))]
ErrorNetwork(String),
@@ -122,8 +99,6 @@ pub enum Event {
/// dc_set_chat_name(), dc_set_chat_profile_image(),
/// dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
/// dc_send_text_msg() or another sending function.
///
/// @return 0
#[strum(props(id = "410"))]
ErrorSelfNotInGroup(String),
@@ -132,54 +107,41 @@ pub enum Event {
/// - Messages sent, received or removed
/// - Chats created, deleted or archived
/// - A draft has been set
///
/// @return 0
#[strum(props(id = "2000"))]
MsgsChanged { chat_id: u32, msg_id: MsgId },
MsgsChanged { chat_id: ChatId, msg_id: MsgId },
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
///
/// @return 0
#[strum(props(id = "2005"))]
IncomingMsg { chat_id: u32, msg_id: MsgId },
IncomingMsg { chat_id: ChatId, msg_id: MsgId },
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
///
/// @return 0
#[strum(props(id = "2010"))]
MsgDelivered { chat_id: u32, msg_id: MsgId },
MsgDelivered { chat_id: ChatId, msg_id: MsgId },
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_FAILED, see dc_msg_get_state().
///
/// @return 0
#[strum(props(id = "2012"))]
MsgFailed { chat_id: u32, msg_id: MsgId },
MsgFailed { chat_id: ChatId, msg_id: MsgId },
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
///
/// @return 0
#[strum(props(id = "2015"))]
MsgRead { chat_id: u32, msg_id: MsgId },
MsgRead { chat_id: ChatId, msg_id: MsgId },
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
/// and dc_remove_contact_from_chat().
///
/// @return 0
#[strum(props(id = "2020"))]
ChatModified(u32),
ChatModified(ChatId),
/// Contact(s) created, renamed, blocked or deleted.
///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
/// @return 0
#[strum(props(id = "2030"))]
ContactsChanged(Option<u32>),
@@ -188,14 +150,12 @@ pub enum Event {
/// @param data1 (u32) contact_id of the contact for which the location has changed.
/// If the locations of several contacts have been changed,
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
/// @return 0
#[strum(props(id = "2035"))]
LocationChanged(Option<u32>),
/// Inform about the configuration progress started by configure().
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @return 0
#[strum(props(id = "2041"))]
ConfigureProgress(usize),
@@ -203,7 +163,6 @@ pub enum Event {
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0
/// @return 0
#[strum(props(id = "2051"))]
ImexProgress(usize),
@@ -214,7 +173,6 @@ pub enum Event {
/// services.
///
/// @param data2 0
/// @return 0
#[strum(props(id = "2052"))]
ImexFileWritten(PathBuf),
@@ -230,7 +188,6 @@ pub enum Event {
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
/// @return 0
#[strum(props(id = "2060"))]
SecurejoinInviterProgress { contact_id: u32, progress: usize },
@@ -242,14 +199,12 @@ pub enum Event {
/// @param data2 (int) Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
/// @return 0
#[strum(props(id = "2061"))]
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
/// This event is sent out to the inviter when a joiner successfully joined a group.
/// @param data1 (int) chat_id
/// @param data2 (int) contact_id
/// @return 0
#[strum(props(id = "2062"))]
SecurejoinMemberAdded { chat_id: u32, contact_id: u32 },
SecurejoinMemberAdded { chat_id: ChatId, contact_id: u32 },
}

96
src/headerdef.rs Normal file
View File

@@ -0,0 +1,96 @@
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 {
MessageId,
Subject,
Date,
From_,
To,
Cc,
Disposition,
OriginalMessageId,
/// Delta Chat extension for message IDs in combined MDNs
AdditionalMessageIds,
ListId,
References,
InReplyTo,
Precedence,
ChatVersion,
ChatGroupId,
ChatGroupName,
ChatGroupNameChanged,
ChatVerified,
ChatGroupAvatar,
ChatUserAvatar,
ChatVoiceMessage,
ChatGroupMemberRemoved,
ChatGroupMemberAdded,
ChatContent,
ChatDuration,
ChatDispositionNotificationTo,
Autocrypt,
AutocryptSetupMessage,
SecureJoin,
SecureJoinGroup,
SecureJoinFingerprint,
SecureJoinInvitenumber,
SecureJoinAuth,
_TestHeader,
}
impl HeaderDef {
/// Returns the corresponding Event id.
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())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
/// Test that kebab_case serialization works as expected
fn kebab_test() {
assert_eq!(HeaderDef::From_.get_headername(), "from");
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
);
}
}

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

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

View File

@@ -1,15 +1,17 @@
use super::Imap;
use async_imap::extensions::idle::IdleResponse;
use async_imap::extensions::idle::{Handle as ImapIdleHandle, IdleResponse};
use async_native_tls::TlsStream;
use async_std::net::TcpStream;
use async_std::prelude::*;
use async_std::task;
use std::sync::atomic::Ordering;
use std::time::{Duration, SystemTime};
use crate::context::Context;
use crate::imap_client::*;
use super::select_folder;
use super::session::Session;
type Result<T> = std::result::Result<T, Error>;
@@ -18,15 +20,15 @@ pub enum Error {
#[fail(display = "IMAP IDLE protocol failed to init/complete")]
IdleProtocolFailed(#[cause] async_imap::error::Error),
#[fail(display = "IMAP IDLE protocol timed out")]
IdleTimeout(#[cause] async_std::future::TimeoutError),
#[fail(display = "IMAP server does not have IDLE capability")]
IdleAbilityMissing,
#[fail(display = "IMAP select folder error")]
SelectFolderError(#[cause] select_folder::Error),
#[fail(display = "IMAP error")]
ImapError(#[cause] async_imap::error::Error),
#[fail(display = "Setup handle error")]
SetupHandleError(#[cause] super::Error),
}
@@ -37,6 +39,27 @@ impl From<select_folder::Error> for Error {
}
}
#[derive(Debug)]
pub(crate) enum IdleHandle {
Secure(ImapIdleHandle<TlsStream<TcpStream>>),
Insecure(ImapIdleHandle<TcpStream>),
}
impl Session {
pub fn idle(self) -> IdleHandle {
match self {
Session::Secure(i) => {
let h = i.idle();
IdleHandle::Secure(h)
}
Session::Insecure(i) => {
let h = i.idle();
IdleHandle::Insecure(h)
}
}
}
}
impl Imap {
pub fn can_idle(&self) -> bool {
task::block_on(async move { self.config.read().await.can_idle })
@@ -77,21 +100,34 @@ 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);
}
}
}
match handle.done().await {
// if we can't properly terminate the idle
// protocol let's break the connection.
let res =
async_std::future::timeout(Duration::from_secs(15), handle.done())
.await
.map_err(|err| {
self.trigger_reconnect();
Error::IdleTimeout(err)
})?;
match res {
Ok(session) => {
*self.session.lock().await = Some(Session::Secure(session));
}
@@ -121,21 +157,34 @@ 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);
}
}
}
match handle.done().await {
// if we can't properly terminate the idle
// protocol let's break the connection.
let res =
async_std::future::timeout(Duration::from_secs(15), handle.done())
.await
.map_err(|err| {
self.trigger_reconnect();
Error::IdleTimeout(err)
})?;
match res {
Ok(session) => {
*self.session.lock().await = Some(Session::Insecure(session));
}
@@ -218,7 +267,7 @@ impl Imap {
"IMAP-fake-IDLE done after {:.4}s",
SystemTime::now()
.duration_since(fake_idle_start_time)
.unwrap()
.unwrap_or_default()
.as_millis() as f64
/ 1000.,
);

View File

@@ -5,30 +5,37 @@
use std::sync::atomic::{AtomicBool, Ordering};
use num_traits::FromPrimitive;
use async_imap::{
error::Result as ImapResult,
types::{Fetch, Flag, Mailbox, Name, NameAttribute},
types::{Capability, Fetch, Flag, Mailbox, Name, NameAttribute},
};
use async_std::sync::{Mutex, RwLock};
use async_std::task;
use crate::config::*;
use crate::constants::*;
use crate::context::Context;
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_receive_imf::{
dc_receive_imf, from_field_to_contact_id, is_msgrmsg_rfc724_mid_in_list,
};
use crate::events::Event;
use crate::imap_client::*;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::job::{job_add, Action};
use crate::login_param::{CertificateChecks, LoginParam};
use crate::message::{self, update_server_uid};
use crate::oauth2::dc_get_oauth2_access_token;
use crate::param::Params;
use crate::stock::StockMessage;
use crate::wrapmime;
mod client;
mod idle;
pub mod select_folder;
mod session;
const DC_IMAP_SEEN: usize = 0x0001;
use client::Client;
use session::Session;
type Result<T> = std::result::Result<T, Error>;
@@ -64,6 +71,12 @@ 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),
#[fail(display = "IMAP other error: {:?}", _0)]
Other(String),
}
@@ -92,6 +105,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,
@@ -100,7 +119,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:*";
@@ -152,7 +184,10 @@ struct ImapConfig {
pub selected_mailbox: Option<Mailbox>,
pub selected_folder_needs_expunge: bool,
pub can_idle: bool,
pub has_xlist: bool,
/// True if the server has MOVE capability as defined in
/// https://tools.ietf.org/html/rfc6851
pub can_move: bool,
pub imap_delimiter: char,
}
@@ -170,7 +205,7 @@ impl Default for ImapConfig {
selected_mailbox: None,
selected_folder_needs_expunge: false,
can_idle: false,
has_xlist: false,
can_move: false,
imap_delimiter: '.',
}
}
@@ -324,14 +359,12 @@ impl Imap {
cfg.imap_port = 0;
cfg.can_idle = false;
cfg.has_xlist = false;
cfg.can_move = false;
}
/// Connects to imap account using already-configured parameters.
pub fn connect_configured(&self, context: &Context) -> Result<()> {
if async_std::task::block_on(async move {
self.is_connected().await && !self.should_reconnect()
}) {
if async_std::task::block_on(self.is_connected()) && !self.should_reconnect() {
return Ok(());
}
if !context.sql.get_raw_config_bool(context, "configured") {
@@ -341,7 +374,7 @@ impl Imap {
let param = LoginParam::from_database(context, "configured_");
// the trailing underscore is correct
if self.connect(context, &param) {
if task::block_on(self.connect(context, &param)) {
self.ensure_configured_folders(context, true)
} else {
Err(Error::ConnectionFailed(format!("{}", param)))
@@ -350,77 +383,80 @@ impl Imap {
/// tries connecting to imap account using the specific login
/// parameters
pub fn connect(&self, context: &Context, lp: &LoginParam) -> bool {
task::block_on(async move {
if lp.mail_server.is_empty() || lp.mail_user.is_empty() || lp.mail_pw.is_empty() {
return false;
}
pub async fn connect(&self, context: &Context, lp: &LoginParam) -> bool {
if lp.mail_server.is_empty() || lp.mail_user.is_empty() || lp.mail_pw.is_empty() {
return false;
}
{
let addr = &lp.addr;
let imap_server = &lp.mail_server;
let imap_port = lp.mail_port as u16;
let imap_user = &lp.mail_user;
let imap_pw = &lp.mail_pw;
let server_flags = lp.server_flags as usize;
{
let addr = &lp.addr;
let imap_server = &lp.mail_server;
let imap_port = lp.mail_port as u16;
let imap_user = &lp.mail_user;
let imap_pw = &lp.mail_pw;
let server_flags = lp.server_flags as usize;
let mut config = self.config.write().await;
config.addr = addr.to_string();
config.imap_server = imap_server.to_string();
config.imap_port = imap_port;
config.imap_user = imap_user.to_string();
config.imap_pw = imap_pw.to_string();
config.certificate_checks = lp.imap_certificate_checks;
config.server_flags = server_flags;
}
let mut config = self.config.write().await;
config.addr = addr.to_string();
config.imap_server = imap_server.to_string();
config.imap_port = imap_port;
config.imap_user = imap_user.to_string();
config.imap_pw = imap_pw.to_string();
config.certificate_checks = lp.imap_certificate_checks;
config.server_flags = server_flags;
}
if let Err(err) = self.setup_handle_if_needed(context).await {
warn!(context, "failed to setup imap handle: {}", err);
self.free_connect_params().await;
return false;
}
if let Err(err) = self.setup_handle_if_needed(context).await {
warn!(context, "failed to setup imap handle: {}", err);
self.free_connect_params().await;
return false;
}
let teardown = match &mut *self.session.lock().await {
Some(ref mut session) => match session.capabilities().await {
Ok(caps) => {
if !context.sql.is_open() {
warn!(context, "IMAP-LOGIN as {} ok but ABORTING", lp.mail_user,);
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| s + &format!(" {:?}", c));
self.config.write().await.can_idle = can_idle;
self.config.write().await.has_xlist = has_xlist;
*self.connected.lock().await = true;
emit_event!(
context,
Event::ImapConnected(format!(
"IMAP-LOGIN as {}, capabilities: {}",
lp.mail_user, caps_list,
))
);
false
}
}
Err(err) => {
info!(context, "CAPABILITY command error: {}", err);
let teardown = match &mut *self.session.lock().await {
Some(ref mut session) => match session.capabilities().await {
Ok(caps) => {
if !context.sql.is_open() {
warn!(context, "IMAP-LOGIN as {} ok but ABORTING", lp.mail_user,);
true
} else {
let can_idle = caps.has_str("IDLE");
let can_move = caps.has_str("MOVE");
let caps_list = caps.iter().fold(String::new(), |s, c| {
if let Capability::Atom(x) = c {
s + &format!(" {}", x)
} else {
s + &format!(" {:?}", c)
}
});
self.config.write().await.can_idle = can_idle;
self.config.write().await.can_move = can_move;
*self.connected.lock().await = true;
emit_event!(
context,
Event::ImapConnected(format!(
"IMAP-LOGIN as {}, capabilities: {}",
lp.mail_user, caps_list,
))
);
false
}
},
None => true,
};
}
Err(err) => {
info!(context, "CAPABILITY command error: {}", err);
true
}
},
None => true,
};
if teardown {
self.disconnect(context);
if teardown {
self.disconnect(context);
false
} else {
true
}
})
false
} else {
true
}
}
pub fn disconnect(&self, context: &Context) {
@@ -478,7 +514,10 @@ impl Imap {
let (uid_validity, last_seen_uid) = self.get_config_last_seen_uid(context, &folder);
let config = self.config.read().await;
let mailbox = config.selected_mailbox.as_ref().expect("just selected");
let mailbox = config
.selected_mailbox
.as_ref()
.ok_or_else(|| Error::NoMailbox(folder.to_string()))?;
let new_uid_validity = match mailbox.uid_validity {
Some(v) => v,
@@ -549,6 +588,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())?;
@@ -556,7 +598,7 @@ impl Imap {
let mut list = if let Some(ref mut session) = &mut *self.session.lock().await {
// fetch messages with larger UID than the last one seen
// (`UID FETCH lastseenuid+1:*)`, see RFC 4549
// `(UID FETCH lastseenuid+1:*)`, see RFC 4549
let set = format!("{}:*", last_seen_uid + 1);
match session.uid_fetch(set, PREFETCH_FLAGS).await {
Ok(list) => list,
@@ -574,11 +616,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
@@ -587,20 +634,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,
@@ -608,6 +644,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;
@@ -651,17 +715,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);
@@ -680,44 +746,48 @@ impl Imap {
folder.as_ref(),
err
);
return 0;
return Err(Error::FetchFailed(err));
}
}
} else {
// we could not get a valid imap session, this should be retried
self.trigger_reconnect();
return 0;
return Err(Error::Other("Could not get IMAP session".to_string()));
};
if msgs.is_empty() {
if let Some(msg) = msgs.first() {
// XXX put flags into a set and pass them to dc_receive_imf
let is_deleted = msg.flags().any(|flag| flag == Flag::Deleted);
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
if !is_deleted && msg.body().is_some() {
let body = msg.body().unwrap_or_default();
if let Err(err) =
dc_receive_imf(context, &body, folder.as_ref(), server_uid, is_seen)
{
warn!(
context,
"dc_receive_imf failed for imap-message {}/{}: {:?}",
folder.as_ref(),
server_uid,
err
);
}
}
} else {
warn!(
context,
"Message #{} does not exist in folder \"{}\".",
server_uid,
folder.as_ref()
);
} else {
let msg = &msgs[0];
// XXX put flags into a set and pass them to dc_receive_imf
let is_deleted = msg.flags().any(|flag| match flag {
Flag::Deleted => true,
_ => false,
});
let is_seen = msg.flags().any(|flag| match flag {
Flag::Seen => true,
_ => false,
});
let flags = if is_seen { DC_IMAP_SEEN } else { 0 };
if !is_deleted && msg.body().is_some() {
let body = msg.body().unwrap_or_default();
dc_receive_imf(context, &body, folder.as_ref(), server_uid, flags as u32);
}
}
1
Ok(())
}
pub fn can_move(&self) -> bool {
task::block_on(async move { self.config.read().await.can_move })
}
pub fn mv(
@@ -749,52 +819,74 @@ impl Imap {
let set = format!("{}", uid);
let display_folder_id = format!("{}/{}", folder, uid);
if let Some(ref mut session) = &mut *self.session.lock().await {
match session.uid_mv(&set, &dest_folder).await {
Ok(_) => {
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} moved to {}",
display_folder_id, dest_folder
))
);
return ImapActionResult::Success;
}
Err(err) => {
warn!(
context,
"Cannot move message, fallback to COPY/DELETE {}/{} to {}: {}",
folder,
uid,
dest_folder,
err
);
}
}
} else {
unreachable!();
};
if let Some(ref mut session) = &mut *self.session.lock().await {
match session.uid_copy(&set, &dest_folder).await {
Ok(_) => {
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid);
ImapActionResult::Failed
} else {
self.config.write().await.selected_folder_needs_expunge = true;
ImapActionResult::Success
if self.can_move() {
if let Some(ref mut session) = &mut *self.session.lock().await {
match session.uid_mv(&set, &dest_folder).await {
Ok(_) => {
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} moved to {}",
display_folder_id, dest_folder
))
);
return ImapActionResult::Success;
}
Err(err) => {
warn!(
context,
"Cannot move message, fallback to COPY/DELETE {}/{} to {}: {}",
folder,
uid,
dest_folder,
err
);
}
}
Err(err) => {
warn!(context, "Could not copy message: {}", err);
ImapActionResult::Failed
}
} else {
unreachable!();
};
} else {
info!(
context,
"Server does not support MOVE, fallback to COPY/DELETE {}/{} to {}",
folder,
uid,
dest_folder
);
}
if let Some(ref mut session) = &mut *self.session.lock().await {
if let Err(err) = session.uid_copy(&set, &dest_folder).await {
warn!(context, "Could not copy message: {}", err);
return ImapActionResult::Failed;
}
} else {
unreachable!();
}
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid);
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} copied to {} (delete FAILED)",
display_folder_id, dest_folder
))
);
ImapActionResult::Failed
} else {
self.config.write().await.selected_folder_needs_expunge = true;
emit_event!(
context,
Event::ImapMessageMoved(format!(
"IMAP Message {} copied to {} (delete successfull)",
display_folder_id, dest_folder
))
);
ImapActionResult::Success
}
})
}
@@ -899,7 +991,6 @@ impl Imap {
})
}
// only returns 0 on connection problems; we should try later again in this case *
pub fn delete_msg(
&self,
context: &Context,
@@ -919,9 +1010,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 '{}'",
@@ -929,9 +1022,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!(
@@ -941,8 +1036,8 @@ impl Imap {
remote_message_id,
message_id,
);
*uid = 0;
}
*uid = 0;
}
Err(err) => {
warn!(
@@ -1083,7 +1178,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() {
@@ -1190,7 +1284,7 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
message::rfc724_mid_exists(context, &rfc724_mid)
{
if old_server_folder.is_empty() && old_server_uid == 0 {
info!(context, "[move] detected bbc-self {}", rfc724_mid,);
info!(context, "[move] detected bcc-self {}", rfc724_mid,);
context.do_heuristics_moves(server_folder.as_ref(), msg_id);
job_add(
context,
@@ -1212,17 +1306,98 @@ fn precheck_imf(context: &Context, rfc724_mid: &str, server_folder: &str, server
}
}
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 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))?;
if let Some(info) = addrs.extract_single_info() {
return Ok(info.addr);
}
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()));
}
wrapmime::parse_message_id(&message_id.unwrap()).map_err(Into::into)
bail!("could not parse message_id: {}", value);
}
fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>> {
let header_bytes = match prefetch_msg.header() {
Some(header_bytes) => header_bytes,
None => return Ok(Vec::new()),
};
let (headers, _) = mailparse::parse_headers(header_bytes)?;
Ok(headers)
}
fn prefetch_get_message_id(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);
}
}
if let Some(value) = headers.get_header_value(HeaderDef::References)? {
if is_msgrmsg_rfc724_mid_in_list(context, &value) {
return Ok(true);
}
}
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)]
mod tests {
use super::*;
#[test]
fn test_parse_message_id() {
assert_eq!(
parse_message_id("Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
assert_eq!(
parse_message_id("<Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org>").unwrap(),
"Mr.PRUe8HJBoaO.3whNvLCMFU0@testrun.org"
);
}
}

View File

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

View File

@@ -8,8 +8,8 @@ use rand::{thread_rng, Rng};
use crate::blob::BlobObject;
use crate::chat;
use crate::chat::delete_and_reset_all_device_msgs;
use crate::config::Config;
use crate::configure::*;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
@@ -17,7 +17,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::*;
@@ -33,16 +33,19 @@ pub enum ImexMode {
/// and `private-key-default.asc`, if there are more keys, they are written to files as
/// `public-key-<id>.asc` and `private-key-<id>.asc`
ExportSelfKeys = 1,
/// Import private keys found in the directory given as `param1`.
/// The last imported key is made the default keys unless its name contains the string `legacy`.
/// Public keys are not imported.
ImportSelfKeys = 2,
/// Export a backup to the directory given as `param1`.
/// The backup contains all contacts, chats, images and other data and device independent settings.
/// The backup does not contain device dependent settings as ringtones or LED notification settings.
/// The name of the backup is typically `delta-chat.<day>.bak`, if more than one backup is create on a day,
/// the format is `delta-chat.<day>-<number>.bak`
ExportBackup = 11,
/// `param1` is the file (not: directory) to import. The file is normally
/// created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
/// is only possible as long as the context is not configured or used in another way.
@@ -133,14 +136,16 @@ fn do_initiate_key_transfer(context: &Context) -> Result<String> {
let chat_id = chat::create_by_contact_id(context, DC_CONTACT_ID_SELF)?;
msg = Message::default();
msg.type_0 = Viewtype::File;
msg.viewtype = Viewtype::File;
msg.param.set(Param::File, setup_file_blob.as_name());
msg.param
.set(Param::MimeType, "application/autocrypt-setup");
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
msg.param
.set_int(Param::ForcePlaintext, DC_FP_NO_AUTOCRYPT_HEADER);
msg.param.set_int(
Param::ForcePlaintext,
ForcePlaintext::NoAutocryptHeader as i32,
);
ensure!(!context.shall_stop_ongoing(), "canceled");
let msg_id = chat::send_msg(context, chat_id, &mut msg)?;
@@ -283,7 +288,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()) {
@@ -308,34 +312,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(())
}
@@ -380,7 +376,7 @@ pub fn JobImexImap(context: &Context, job: &Job) -> Result<()> {
context.free_ongoing();
bail!("Cannot create private key or private key not available.");
} else {
dc_create_folder(context, &param);
dc_create_folder(context, &param)?;
}
}
let path = Path::new(param);
@@ -417,7 +413,7 @@ fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Resul
);
ensure!(
!dc_is_configured(context),
!context.is_configured(),
"Cannot import backups to accounts in use."
);
context.sql.close(&context);
@@ -438,6 +434,8 @@ fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Resul
"could not re-open db"
);
delete_and_reset_all_device_msgs(&context)?;
let total_files_cnt = context
.sql
.query_get_value::<_, isize>(context, "SELECT COUNT(*) FROM backup_blobs;", params![])
@@ -738,7 +736,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;
@@ -793,8 +790,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();

File diff suppressed because it is too large Load Diff

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

@@ -1,18 +1,6 @@
#![forbid(unsafe_code)]
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
// for now we hide warnings to not clutter/hide errors during "cargo clippy"
#![allow(
clippy::type_complexity,
clippy::cognitive_complexity,
clippy::too_many_arguments,
clippy::block_in_if_condition_stmt,
clippy::large_enum_variant
)]
#![allow(
clippy::unreadable_literal,
clippy::needless_range_loop,
clippy::match_bool
)]
#![feature(ptr_wrapping_offset_from)]
#![allow(clippy::match_bool)]
#[macro_use]
extern crate failure_derive;
@@ -29,30 +17,32 @@ extern crate strum_macros;
extern crate debug_stub_derive;
#[macro_use]
mod log;
pub mod log;
#[macro_use]
pub mod error;
pub mod headerdef;
pub(crate) mod events;
pub use events::*;
mod aheader;
pub mod blob;
mod blob;
pub mod chat;
pub mod chatlist;
pub mod config;
pub mod configure;
mod configure;
pub mod constants;
pub mod contact;
pub mod context;
mod e2ee;
mod imap;
mod imap_client;
pub mod imex;
#[macro_use]
pub mod job;
mod job_thread;
pub mod key;
pub mod keyring;
mod keyring;
pub mod location;
mod login_param;
pub mod lot;
@@ -63,19 +53,18 @@ pub mod oauth2;
mod param;
pub mod peerstate;
pub mod pgp;
pub mod provider;
pub mod qr;
pub mod securejoin;
mod simplify;
mod smtp;
pub mod sql;
pub mod stock;
mod token;
#[macro_use]
mod wrapmime;
mod dehtml;
pub mod dc_array;
pub mod dc_receive_imf;
mod dc_simplify;
pub mod dc_tools;
/// if set imap/incoming and smtp/outgoing MIME messages will be printed

View File

@@ -4,14 +4,14 @@ use bitflags::bitflags;
use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::chat;
use crate::chat::{self, ChatId};
use crate::config::Config;
use crate::constants::*;
use crate::context::*;
use crate::dc_tools::*;
use crate::error::Error;
use crate::events::Event;
use crate::job::*;
use crate::job::{self, job_action_exists, job_add, Job};
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::*;
@@ -28,7 +28,7 @@ pub struct Location {
pub timestamp: i64,
pub contact_id: u32,
pub msg_id: u32,
pub chat_id: u32,
pub chat_id: ChatId,
pub marker: Option<String>,
pub independent: u32,
}
@@ -64,11 +64,10 @@ impl Kml {
Default::default()
}
pub fn parse(context: &Context, content: &[u8]) -> Result<Self, Error> {
ensure!(content.len() <= 1024 * 1024, "kml-file is too large");
pub fn parse(context: &Context, to_parse: &[u8]) -> Result<Self, Error> {
ensure!(to_parse.len() <= 1024 * 1024, "kml-file is too large");
let to_parse = String::from_utf8_lossy(content);
let mut reader = quick_xml::Reader::from_str(&to_parse);
let mut reader = quick_xml::Reader::from_reader(to_parse);
reader.trim_text(true);
let mut kml = Kml::new();
@@ -193,9 +192,9 @@ impl Kml {
}
// location streaming
pub fn send_locations_to_chat(context: &Context, chat_id: u32, seconds: i64) {
pub fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: i64) {
let now = time();
if !(seconds < 0 || chat_id <= DC_CHAT_ID_LAST_SPECIAL) {
if !(seconds < 0 || chat_id.is_special()) {
let is_sending_locations_before = is_sending_locations_to_chat(context, chat_id);
if sql::execute(
context,
@@ -207,7 +206,7 @@ pub fn send_locations_to_chat(context: &Context, chat_id: u32, seconds: i64) {
params![
if 0 != seconds { now } else { 0 },
if 0 != seconds { now + seconds } else { 0 },
chat_id as i32,
chat_id,
],
)
.is_ok()
@@ -228,8 +227,8 @@ pub fn send_locations_to_chat(context: &Context, chat_id: u32, seconds: i64) {
schedule_MAYBE_SEND_LOCATIONS(context, false);
job_add(
context,
Action::MaybeSendLocationsEnded,
chat_id as i32,
job::Action::MaybeSendLocationsEnded,
chat_id.to_u32() as i32,
Params::new(),
seconds + 1,
);
@@ -240,17 +239,23 @@ pub fn send_locations_to_chat(context: &Context, chat_id: u32, seconds: i64) {
#[allow(non_snake_case)]
fn schedule_MAYBE_SEND_LOCATIONS(context: &Context, force_schedule: bool) {
if force_schedule || !job_action_exists(context, Action::MaybeSendLocations) {
job_add(context, Action::MaybeSendLocations, 0, Params::new(), 60);
if force_schedule || !job_action_exists(context, job::Action::MaybeSendLocations) {
job_add(
context,
job::Action::MaybeSendLocations,
0,
Params::new(),
60,
);
};
}
pub fn is_sending_locations_to_chat(context: &Context, chat_id: u32) -> bool {
pub fn is_sending_locations_to_chat(context: &Context, chat_id: ChatId) -> bool {
context
.sql
.exists(
"SELECT id FROM chats WHERE (? OR id=?) AND locations_send_until>?;",
params![if chat_id == 0 { 1 } else { 0 }, chat_id as i32, time()],
params![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()],
)
.unwrap_or_default()
}
@@ -296,7 +301,7 @@ pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> b
pub fn get_range(
context: &Context,
chat_id: u32,
chat_id: ChatId,
contact_id: u32,
timestamp_from: i64,
mut timestamp_to: i64,
@@ -314,8 +319,8 @@ pub fn get_range(
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
params![
if chat_id == 0 { 1 } else { 0 },
chat_id as i32,
if chat_id.is_unset() { 1 } else { 0 },
chat_id,
if contact_id == 0 { 1 } else { 0 },
contact_id as i32,
timestamp_from,
@@ -359,13 +364,14 @@ fn is_marker(txt: &str) -> bool {
txt.len() == 1 && !txt.starts_with(' ')
}
/// Deletes all locations from the database.
pub fn delete_all(context: &Context) -> Result<(), Error> {
sql::execute(context, &context.sql, "DELETE FROM locations;", params![])?;
context.call_cb(Event::LocationChanged(None));
Ok(())
}
pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error> {
pub fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Error> {
let mut last_added_location_id = 0;
let self_addr = context
@@ -374,7 +380,7 @@ pub fn get_kml(context: &Context, chat_id: u32) -> Result<(String, u32), Error>
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
params![chat_id as i32], |row| {
params![chat_id], |row| {
let send_begin: i64 = row.get(0)?;
let send_until: i64 = row.get(1)?;
let last_sent: i64 = row.get(2)?;
@@ -459,14 +465,14 @@ pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String
pub fn set_kml_sent_timestamp(
context: &Context,
chat_id: u32,
chat_id: ChatId,
timestamp: i64,
) -> Result<(), Error> {
sql::execute(
context,
&context.sql,
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
params![timestamp, chat_id as i32],
params![timestamp, chat_id],
)?;
Ok(())
@@ -489,12 +495,12 @@ pub fn set_msg_location_id(
pub fn save(
context: &Context,
chat_id: u32,
chat_id: ChatId,
contact_id: u32,
locations: &[Location],
independent: bool,
) -> Result<u32, Error> {
ensure!(chat_id > DC_CHAT_ID_LAST_SPECIAL, "Invalid chat id");
ensure!(!chat_id.is_special(), "Invalid chat id");
context
.sql
.prepare2(
@@ -514,7 +520,7 @@ pub fn save(
stmt_insert.execute(params![
location.timestamp,
contact_id as i32,
chat_id as i32,
chat_id,
location.latitude,
location.longitude,
location.accuracy,
@@ -542,7 +548,7 @@ pub fn save(
}
#[allow(non_snake_case)]
pub fn JobMaybeSendLocations(context: &Context, _job: &Job) {
pub(crate) fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Status {
let now = time();
let mut continue_streaming = false;
info!(
@@ -556,7 +562,7 @@ pub fn JobMaybeSendLocations(context: &Context, _job: &Job) {
WHERE locations_send_until>?;",
params![now],
|row| {
let chat_id: i32 = row.get(0)?;
let chat_id: ChatId = row.get(0)?;
let locations_send_begin: i64 = row.get(1)?;
let locations_last_sent: i64 = row.get(2)?;
continue_streaming = true;
@@ -623,44 +629,46 @@ pub fn JobMaybeSendLocations(context: &Context, _job: &Job) {
for (chat_id, mut msg) in msgs.into_iter() {
// TODO: better error handling
chat::send_msg(context, chat_id as u32, &mut msg).unwrap_or_default();
chat::send_msg(context, chat_id, &mut msg).unwrap_or_default();
}
}
if continue_streaming {
schedule_MAYBE_SEND_LOCATIONS(context, true);
}
job::Status::Finished(Ok(()))
}
#[allow(non_snake_case)]
pub fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) {
pub(crate) fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) -> job::Status {
// this function is called when location-streaming _might_ have ended for a chat.
// the function checks, if location-streaming is really ended;
// if so, a device-message is added if not yet done.
let chat_id = job.foreign_id;
let chat_id = ChatId::new(job.foreign_id);
if let Ok((send_begin, send_until)) = context.sql.query_row(
let (send_begin, send_until) = job_try!(context.sql.query_row(
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
params![chat_id as i32],
params![chat_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
) {
if !(send_begin != 0 && time() <= send_until) {
// still streaming -
// may happen as several calls to dc_send_locations_to_chat()
// do not un-schedule pending DC_MAYBE_SEND_LOC_ENDED jobs
if !(send_begin == 0 && send_until == 0) {
// not streaming, device-message already sent
if context.sql.execute(
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
params![chat_id as i32],
).is_ok() {
let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
chat::add_info_msg(context, chat_id, stock_str);
context.call_cb(Event::ChatModified(chat_id));
}
}
));
if !(send_begin != 0 && time() <= send_until) {
// still streaming -
// may happen as several calls to dc_send_locations_to_chat()
// do not un-schedule pending DC_MAYBE_SEND_LOC_ENDED jobs
if !(send_begin == 0 && send_until == 0) {
// not streaming, device-message already sent
job_try!(context.sql.execute(
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
params![chat_id],
));
let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
chat::add_info_msg(context, chat_id, stock_str);
context.call_cb(Event::ChatModified(chat_id));
}
}
job::Status::Finished(Ok(()))
}
#[cfg(test)]

View File

@@ -7,7 +7,13 @@ macro_rules! info {
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
emit_event!($ctx, $crate::Event::Info(formatted));
let thread = ::std::thread::current();
let full = format!("{thid:?} {file}:{line}: {msg}",
thid = thread.id(),
file = file!(),
line = line!(),
msg = &formatted);
emit_event!($ctx, $crate::Event::Info(full));
}};
}
@@ -18,7 +24,13 @@ macro_rules! warn {
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
emit_event!($ctx, $crate::Event::Warning(formatted));
let thread = ::std::thread::current();
let full = format!("{thid:?} {file}:{line}: {msg}",
thid = thread.id(),
file = file!(),
line = line!(),
msg = &formatted);
emit_event!($ctx, $crate::Event::Warning(full));
}};
}

View File

@@ -4,11 +4,6 @@ use std::borrow::Cow;
use std::fmt;
use crate::context::Context;
use crate::error::Error;
use async_std::sync::Arc;
use rustls;
use webpki;
use webpki_roots;
#[derive(Copy, Clone, Debug, Display, FromPrimitive)]
#[repr(i32)]
@@ -16,7 +11,11 @@ use webpki_roots;
pub enum CertificateChecks {
Automatic = 0,
Strict = 1,
AcceptInvalidHostnames = 2,
/// Same as AcceptInvalidCertificates
/// Previously known as AcceptInvalidHostnames, now deprecated.
AcceptInvalidCertificates2 = 2,
AcceptInvalidCertificates = 3,
}
@@ -130,7 +129,7 @@ impl LoginParam {
&self,
context: &Context,
prefix: impl AsRef<str>,
) -> Result<(), Error> {
) -> crate::sql::Result<()> {
let prefix = prefix.as_ref();
let sql = &context.sql;
@@ -259,49 +258,22 @@ fn get_readable_flags(flags: i32) -> String {
res
}
pub struct NoCertificateVerification {}
impl rustls::ServerCertVerifier for NoCertificateVerification {
fn verify_server_cert(
&self,
_roots: &rustls::RootCertStore,
_presented_certs: &[rustls::Certificate],
_dns_name: webpki::DNSNameRef<'_>,
_ocsp: &[u8],
) -> Result<rustls::ServerCertVerified, rustls::TLSError> {
Ok(rustls::ServerCertVerified::assertion())
}
}
pub fn dc_build_tls_config(certificate_checks: CertificateChecks) -> rustls::ClientConfig {
let mut config = rustls::ClientConfig::new();
config
.root_store
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
pub fn dc_build_tls(certificate_checks: CertificateChecks) -> async_native_tls::TlsConnector {
let tls_builder = async_native_tls::TlsConnector::new();
match certificate_checks {
CertificateChecks::Strict => {}
CertificateChecks::Automatic => {
// Same as AcceptInvalidCertificates for now.
// TODO: use provider database when it becomes available
config
.dangerous()
.set_certificate_verifier(Arc::new(NoCertificateVerification {}));
}
CertificateChecks::AcceptInvalidCertificates => {
// TODO: only accept invalid certs
config
.dangerous()
.set_certificate_verifier(Arc::new(NoCertificateVerification {}));
}
CertificateChecks::AcceptInvalidHostnames => {
// TODO: only accept invalid hostnames
config
.dangerous()
.set_certificate_verifier(Arc::new(NoCertificateVerification {}));
tls_builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true)
}
CertificateChecks::Strict => tls_builder,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => tls_builder
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true),
}
config
}
#[cfg(test)]
@@ -313,8 +285,8 @@ mod tests {
use std::string::ToString;
assert_eq!(
"accept_invalid_hostnames".to_string(),
CertificateChecks::AcceptInvalidHostnames.to_string()
"accept_invalid_certificates".to_string(),
CertificateChecks::AcceptInvalidCertificates.to_string()
);
}
}

View File

@@ -73,20 +73,31 @@ pub enum LotState {
// Qr States
/// id=contact
QrAskVerifyContact = 200,
/// text1=groupname
QrAskVerifyGroup = 202,
/// id=contact
QrFprOk = 210,
/// id=contact
QrFprMissmatch = 220,
/// test1=formatted fingerprint
QrFprWithoutAddr = 230,
/// text1=domain
QrAccount = 250,
/// id=contact
QrAddr = 320,
/// text1=text
QrText = 330,
/// text1=URL
QrUrl = 332,
/// text1=error string
QrError = 400,

View File

@@ -4,8 +4,9 @@ use std::path::{Path, PathBuf};
use deltachat_derive::{FromSql, ToSql};
use failure::Fail;
use serde::{Deserialize, Serialize};
use crate::chat::{self, Chat};
use crate::chat::{self, Chat, ChatId};
use crate::constants::*;
use crate::contact::*;
use crate::context::*;
@@ -29,7 +30,9 @@ const SUMMARY_CHARACTERS: usize = 160;
/// Some message IDs are reserved to identify special message types.
/// This type can represent both the special as well as normal
/// messages.
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
#[derive(
Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
)]
pub struct MsgId(u32);
impl MsgId {
@@ -47,10 +50,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.
@@ -148,6 +148,33 @@ impl rusqlite::types::FromSql for MsgId {
#[fail(display = "Invalid Message ID.")]
pub struct InvalidMsgId;
#[derive(
Debug,
Copy,
Clone,
PartialEq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
)]
#[repr(u8)]
pub(crate) enum MessengerMessage {
No = 0,
Yes = 1,
/// No, but reply to messenger message.
Reply = 2,
}
impl Default for MessengerMessage {
fn default() -> Self {
Self::No
}
}
/// An object representing a single message in memory.
/// The message object is not updated.
/// If you want an update, you have to recreate the object.
@@ -155,13 +182,13 @@ pub struct InvalidMsgId;
/// to check if a mail was sent, use dc_msg_is_sent()
/// approx. max. length returned by dc_msg_get_text()
/// approx. max. length returned by dc_get_msg_info()
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Message {
pub(crate) id: MsgId,
pub(crate) from_id: u32,
pub(crate) to_id: u32,
pub(crate) chat_id: u32,
pub(crate) type_0: Viewtype,
pub(crate) chat_id: ChatId,
pub(crate) viewtype: Viewtype,
pub(crate) state: MessageState,
pub(crate) hidden: bool,
pub(crate) timestamp_sort: i64,
@@ -172,8 +199,7 @@ pub struct Message {
pub(crate) in_reply_to: Option<String>,
pub(crate) server_folder: Option<String>,
pub(crate) server_uid: u32,
// TODO: enum
pub(crate) is_dc_message: u32,
pub(crate) is_dc_message: MessengerMessage,
pub(crate) starred: bool,
pub(crate) chat_blocked: Blocked,
pub(crate) location_id: u32,
@@ -183,7 +209,7 @@ pub struct Message {
impl Message {
pub fn new(viewtype: Viewtype) -> Self {
let mut msg = Message::default();
msg.type_0 = viewtype;
msg.viewtype = viewtype;
msg
}
@@ -236,7 +262,7 @@ impl Message {
msg.timestamp_sort = row.get("timestamp")?;
msg.timestamp_sent = row.get("timestamp_sent")?;
msg.timestamp_rcvd = row.get("timestamp_rcvd")?;
msg.type_0 = row.get("type")?;
msg.viewtype = row.get("type")?;
msg.state = row.get("state")?;
msg.is_dc_message = row.get("msgrmsg")?;
@@ -312,10 +338,10 @@ impl Message {
}
pub fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<(), Error> {
if chat::msgtype_has_file(self.type_0) {
if chat::msgtype_has_file(self.viewtype) {
let file_param = self.param.get_path(Param::File, context)?;
if let Some(path_and_filename) = file_param {
if (self.type_0 == Viewtype::Image || self.type_0 == Viewtype::Gif)
if (self.viewtype == Viewtype::Image || self.viewtype == Viewtype::Gif)
&& !self.param.exists(Param::Width)
{
self.param.set_int(Param::Width, 0);
@@ -385,16 +411,16 @@ impl Message {
self.from_id
}
pub fn get_chat_id(&self) -> u32 {
pub fn get_chat_id(&self) -> ChatId {
if self.chat_blocked != Blocked::Not {
1
ChatId::new(DC_CHAT_ID_DEADDROP)
} else {
self.chat_id
}
}
pub fn get_viewtype(&self) -> Viewtype {
self.type_0
self.viewtype
}
pub fn get_state(&self) -> MessageState {
@@ -474,7 +500,7 @@ impl Message {
pub fn get_summarytext(&self, context: &Context, approx_characters: usize) -> String {
get_summarytext_by_raw(
self.type_0,
self.viewtype,
self.text.as_ref(),
&self.param,
approx_characters,
@@ -518,11 +544,11 @@ impl Message {
/// copied to the blobdir. Thus those attachments need to be
/// created immediately in the blobdir with a valid filename.
pub fn is_increation(&self) -> bool {
chat::msgtype_has_file(self.type_0) && self.state == MessageState::OutPreparing
chat::msgtype_has_file(self.viewtype) && self.state == MessageState::OutPreparing
}
pub fn is_setupmessage(&self) -> bool {
if self.type_0 != Viewtype::File {
if self.viewtype != Viewtype::File {
return false;
}
@@ -595,18 +621,60 @@ impl Message {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
ToSql,
FromSql,
Serialize,
Deserialize,
)]
#[repr(i32)]
pub enum MessageState {
Undefined = 0,
/// Incoming *fresh* message. Fresh messages are neither noticed
/// nor seen and are typically shown in notifications.
InFresh = 10,
/// Incoming *noticed* message. E.g. chat opened but message not
/// yet read - noticed messages are not counted as unread but did
/// not marked as read nor resulted in MDNs.
InNoticed = 13,
/// Incoming message, really *seen* by the user. Marked as read on
/// IMAP and MDN may be sent.
InSeen = 16,
/// For files which need time to be prepared before they can be
/// sent, the message enters this state before
/// OutPending.
OutPreparing = 18,
/// Message saved as draft.
OutDraft = 19,
/// The user has pressed the "send" button but the message is not
/// yet sent and is pending in some way. Maybe we're offline (no
/// checkmark).
OutPending = 20,
/// *Unrecoverable* error (*recoverable* errors result in pending
/// messages).
OutFailed = 24,
/// Outgoing message successfully delivered to server (one
/// checkmark). Note, that already delivered messages may get into
/// the OutFailed state if we get such a hint from the server.
OutDelivered = 26,
/// Outgoing message read by the recipient (two checkmarks; this
/// requires goodwill on the receiver's side)
OutMdnRcvd = 28,
}
@@ -616,6 +684,27 @@ impl Default for MessageState {
}
}
impl std::fmt::Display for MessageState {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Undefined => "Undefined",
Self::InFresh => "Fresh",
Self::InNoticed => "Noticed",
Self::InSeen => "Seen",
Self::OutPreparing => "Preparing",
Self::OutDraft => "Draft",
Self::OutPending => "Pending",
Self::OutFailed => "Failed",
Self::OutDelivered => "Delivered",
Self::OutMdnRcvd => "Read",
}
)
}
}
impl From<MessageState> for LotState {
fn from(s: MessageState) -> Self {
use MessageState::*;
@@ -671,7 +760,7 @@ impl Lot {
self.text1 = None;
self.text1_meaning = Meaning::None;
} else {
if chat.id == DC_CHAT_ID_DEADDROP {
if chat.id.is_deaddrop() {
if let Some(contact) = contact {
self.text1 = Some(contact.get_display_name().into());
} else {
@@ -687,7 +776,7 @@ impl Lot {
}
self.text2 = Some(get_summarytext_by_raw(
msg.type_0,
msg.viewtype,
msg.text.as_ref(),
&msg.param,
SUMMARY_CHARACTERS,
@@ -720,7 +809,7 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
return ret;
}
let rawtxt = rawtxt.unwrap_or_default();
let rawtxt = dc_truncate(rawtxt.trim(), 100000, false);
let rawtxt = dc_truncate(rawtxt.trim(), 100_000, false);
let fts = dc_timestamp_to_str(msg.get_timestamp());
ret += &format!("Sent: {}", fts);
@@ -770,19 +859,7 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
}
}
ret += "State: ";
use MessageState::*;
match msg.state {
InFresh => ret += "Fresh",
InNoticed => ret += "Noticed",
InSeen => ret += "Seen",
OutDelivered => ret += "Delivered",
OutFailed => ret += "Failed",
OutMdnRcvd => ret += "Read",
OutPending => ret += "Pending",
OutPreparing => ret += "Preparing",
_ => ret += &format!("{}", msg.state),
}
ret += &format!("State: {}", msg.state);
if msg.has_location() {
ret += ", Location sent";
@@ -808,9 +885,9 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
ret += &format!("\nFile: {}, {}, bytes\n", path.display(), bytes);
}
if msg.type_0 != Viewtype::Text {
if msg.viewtype != Viewtype::Text {
ret += "Type: ";
ret += &format!("{}", msg.type_0);
ret += &format!("{}", msg.viewtype);
ret += "\n";
ret += &format!("Mimetype: {}\n", &msg.get_filemime().unwrap_or_default());
}
@@ -844,6 +921,7 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> {
"mp3" => (Viewtype::Audio, "audio/mpeg"),
"aac" => (Viewtype::Audio, "audio/aac"),
"mp4" => (Viewtype::Video, "video/mp4"),
"webm" => (Viewtype::Video, "video/webm"),
"jpg" => (Viewtype::Image, "image/jpeg"),
"jpeg" => (Viewtype::Image, "image/jpeg"),
"jpe" => (Viewtype::Image, "image/jpeg"),
@@ -874,7 +952,7 @@ pub fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
delete_poi_location(context, msg.location_id);
}
}
update_msg_chat_id(context, *msg_id, DC_CHAT_ID_TRASH);
update_msg_chat_id(context, *msg_id, ChatId::new(DC_CHAT_ID_TRASH));
job_add(
context,
Action::DeleteMsgOnImap,
@@ -886,7 +964,7 @@ pub fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
if !msg_ids.is_empty() {
context.call_cb(Event::MsgsChanged {
chat_id: 0,
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
job_kill_action(context, Action::Housekeeping);
@@ -894,12 +972,12 @@ pub fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
};
}
fn update_msg_chat_id(context: &Context, msg_id: MsgId, chat_id: u32) -> bool {
fn update_msg_chat_id(context: &Context, msg_id: MsgId, chat_id: ChatId) -> bool {
sql::execute(
context,
&context.sql,
"UPDATE msgs SET chat_id=? WHERE id=?;",
params![chat_id as i32, msg_id],
params![chat_id, msg_id],
)
.is_ok()
}
@@ -978,7 +1056,7 @@ pub fn markseen_msgs(context: &Context, msg_ids: &[MsgId]) -> bool {
if send_event {
context.call_cb(Event::MsgsChanged {
chat_id: 0,
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
}
@@ -1089,14 +1167,14 @@ pub fn exists(context: &Context, msg_id: MsgId) -> bool {
return false;
}
let chat_id: Option<u32> = context.sql.query_get_value(
let chat_id: Option<ChatId> = context.sql.query_get_value(
context,
"SELECT chat_id FROM msgs WHERE id=?;",
params![msg_id],
);
if let Some(chat_id) = chat_id {
chat_id != DC_CHAT_ID_TRASH
!chat_id.is_trash()
} else {
false
}
@@ -1134,7 +1212,7 @@ pub fn mdn_from_ext(
from_id: u32,
rfc724_mid: &str,
timestamp_sent: i64,
) -> Option<(u32, MsgId)> {
) -> Option<(ChatId, MsgId)> {
if from_id <= DC_MSG_ID_LAST_SPECIAL || rfc724_mid.is_empty() {
return None;
}
@@ -1154,7 +1232,7 @@ pub fn mdn_from_ext(
|row| {
Ok((
row.get::<_, MsgId>("msg_id")?,
row.get::<_, u32>("chat_id")?,
row.get::<_, ChatId>("chat_id")?,
row.get::<_, Chattype>("type")?,
row.get::<_, MessageState>("state")?,
))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -154,7 +154,7 @@ pub fn dc_get_oauth2_access_token(
}
// ... and POST
let response = reqwest::Client::new()
let response = reqwest::blocking::Client::new()
.post(post_url)
.form(&post_param)
.send();
@@ -165,7 +165,7 @@ pub fn dc_get_oauth2_access_token(
);
return None;
}
let mut response = response.unwrap();
let response = response.unwrap();
if !response.status().is_success() {
warn!(
context,
@@ -271,7 +271,8 @@ impl Oauth2 {
{
match domain {
"gmail.com" | "googlemail.com" => Some(OAUTH2_GMAIL),
"yandex.com" | "yandex.ru" | "yandex.ua" => Some(OAUTH2_YANDEX),
"yandex.com" | "yandex.by" | "yandex.kz" | "yandex.ru" | "yandex.ua" | "ya.ru"
| "narod.ru" => Some(OAUTH2_YANDEX),
_ => None,
}
} else {
@@ -290,12 +291,12 @@ impl Oauth2 {
// "verified_email": true,
// "picture": "https://lh4.googleusercontent.com/-Gj5jh_9R0BY/AAAAAAAAAAI/AAAAAAAAAAA/IAjtjfjtjNA/photo.jpg"
// }
let response = reqwest::Client::new().get(&userinfo_url).send();
let response = reqwest::blocking::Client::new().get(&userinfo_url).send();
if response.is_err() {
warn!(context, "Error getting userinfo: {:?}", response);
return None;
}
let mut response = response.unwrap();
let response = response.unwrap();
if !response.status().is_success() {
warn!(context, "Error getting userinfo: {:?}", response.status());
return None;
@@ -310,14 +311,19 @@ impl Oauth2 {
return None;
}
if let Ok(response) = parsed {
// serde_json::Value.as_str() removes the quotes of json-strings
let addr = response.get("email");
if addr.is_none() {
// 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") {
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.");
return None;
None
}
let addr = addr.unwrap().as_str();
addr.map(|addr| addr.to_string())
} else {
warn!(context, "Failed to parse userinfo.");
None

View File

@@ -4,50 +4,73 @@ use std::path::PathBuf;
use std::str;
use num_traits::FromPrimitive;
use serde::{Deserialize, Serialize};
use crate::blob::{BlobError, BlobObject};
use crate::context::Context;
use crate::error;
use crate::message::MsgId;
use crate::mimeparser::SystemMessage;
/// Available param keys.
#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash, PartialOrd, Ord, FromPrimitive)]
#[derive(
PartialEq, Eq, Debug, Clone, Copy, Hash, PartialOrd, Ord, FromPrimitive, Serialize, Deserialize,
)]
#[repr(u8)]
pub enum Param {
/// For messages and jobs
File = b'f',
/// For Messages
Width = b'w',
/// For Messages
Height = b'h',
/// For Messages
Duration = b'd',
/// For Messages
MimeType = b'm',
/// For Messages: message is encryoted, outgoing: guarantee E2EE or the message is not send
/// For Messages: message is encrypted, outgoing: guarantee E2EE or the message is not send
GuaranteeE2ee = b'c',
/// For Messages: decrypted with validation errors or without mutual set, if neither
/// 'c' nor 'e' are preset, the messages is only transport encrypted.
ErroneousE2ee = b'e',
/// For Messages: force unencrypted message, either `ForcePlaintext::AddAutocryptHeader` (1),
/// `ForcePlaintext::NoAutocryptHeader` (2) or 0.
ForcePlaintext = b'u',
/// For Messages
WantsMdn = b'r',
/// For Messages
Forwarded = b'a',
/// For Messages
Cmd = b'S',
/// For Messages
Arg = b'E',
/// For Messages
Arg2 = b'F',
/// For Messages
Arg3 = b'G',
/// For Messages
Arg4 = b'H',
/// For Messages
Error = b'L',
/// For Messages
AttachGroupImage = b'A',
/// For Messages: space-separated list of messaged IDs of forwarded copies.
///
/// This is used when a [crate::message::Message] is in the
@@ -58,32 +81,48 @@ pub enum Param {
/// When the original message is then finally sent this parameter
/// is used to also send all the forwarded messages.
PrepForwards = b'P',
/// For Jobs
SetLatitude = b'l',
/// For Jobs
SetLongitude = b'n',
/// For Jobs
ServerFolder = b'Z',
/// For Jobs
ServerUid = b'z',
/// For Jobs
AlsoMove = b'M',
/// For Jobs: space-separated list of message recipients
Recipients = b'R',
// For Groups
/// For Groups
Unpromoted = b'U',
// For Groups and Contacts
/// For Groups and Contacts
ProfileImage = b'i',
// For Chats
/// For Chats
Selftalk = b'K',
// For Chats
/// For Chats
Devicetalk = b'D',
// For QR
/// For QR
Auth = b's',
// For QR
/// For QR
GroupId = b'x',
// For QR
/// For QR
GroupName = b'g',
/// For MDN-sending job
MsgId = b'I',
}
/// Possible values for `Param::ForcePlaintext`.
@@ -99,7 +138,7 @@ pub enum ForcePlaintext {
/// The structure is serialized by calling `to_string()` on it.
///
/// Only for library-internal use.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Params {
inner: BTreeMap<Param, String>,
}
@@ -192,6 +231,11 @@ impl Params {
self.get(key).and_then(|s| s.parse().ok())
}
/// Get the given parameter and parse as `bool`.
pub fn get_bool(&self, key: Param) -> Option<bool> {
self.get_int(key).map(|v| v != 0)
}
/// Get the parameter behind `Param::Cmd` interpreted as `SystemMessage`.
pub fn get_cmd(&self) -> SystemMessage {
self.get_int(Param::Cmd)
@@ -250,7 +294,7 @@ impl Params {
let file = ParamsFile::from_param(context, val)?;
let blob = match file {
ParamsFile::FsPath(path) => match create {
true => BlobObject::create_from_path(context, path)?,
true => BlobObject::new_from_path(context, path)?,
false => BlobObject::from_path(context, path)?,
},
ParamsFile::Blob(blob) => blob,
@@ -275,6 +319,12 @@ impl Params {
Ok(Some(path))
}
pub fn get_msg_id(&self) -> Option<MsgId> {
self.get(Param::MsgId)
.and_then(|x| x.parse::<u32>().ok())
.map(MsgId::new)
}
/// Set the given paramter to the passed in `i32`.
pub fn set_int(&mut self, key: Param, value: i32) -> &mut Self {
self.set(key, format!("{}", value));

View File

@@ -1,14 +1,14 @@
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module
use std::collections::HashSet;
use std::convert::TryFrom;
use std::fmt;
use num_traits::FromPrimitive;
use crate::aheader::*;
use crate::chat::*;
use crate::constants::*;
use crate::context::Context;
use crate::key::*;
use crate::key::{Key, SignedPublicKey};
use crate::sql::{self, Sql};
#[derive(Debug)]
@@ -95,6 +95,7 @@ pub enum ToSave {
pub enum DegradeEvent {
/// Recoverable by an incoming encrypted mail.
EncryptionPaused = 0x01,
/// Recoverable by a new verify.
FingerprintChanged = 0x02,
}
@@ -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,21 +310,50 @@ 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)
}
// This is non-standard.
//
// According to Autocrypt 1.1.0 gossip headers SHOULD NOT
// contain encryption preference, but we include it into
// Autocrypt-Gossip and apply it one way (from
// "nopreference" to "mutual").
//
// This is compatible to standard clients, because they
// can't distinguish it from the case where we have
// contacted the client in the past and received this
// preference via Autocrypt header.
if self.last_seen_autocrypt == 0
&& self.prefer_encrypt == EncryptPreference::NoPreference
&& gossip_header.prefer_encrypt == EncryptPreference::Mutual
{
self.prefer_encrypt = EncryptPreference::Mutual;
self.to_save = Some(ToSave::All);
}
};
}
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(),
EncryptPreference::NoPreference,
public_key,
// Autocrypt 1.1.0 specification says that
// `prefer-encrypt` attribute SHOULD NOT be included,
// but we include it anyway to propagate encryption
// preference to new members in group chats.
if self.last_seen_autocrypt > 0 {
self.prefer_encrypt
} else {
EncryptPreference::NoPreference
},
);
Some(header.to_string())
} else {
@@ -332,18 +362,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(
@@ -417,7 +442,6 @@ impl<'a> Peerstate<'a> {
&self.addr,
],
)?;
reset_gossiped_timestamp(self.context, 0);
} else if self.to_save == Some(ToSave::Timestamps) {
sql::execute(
self.context,
@@ -451,8 +475,8 @@ impl<'a> Peerstate<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[test]
@@ -460,11 +484,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,
@@ -504,12 +524,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,
@@ -543,11 +558,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,7 +16,9 @@ use pgp::types::{
};
use rand::{thread_rng, CryptoRng, Rng};
use crate::error::Error;
use crate::constants::KeyGenType;
use crate::dc_tools::EmailAddress;
use crate::error::Result;
use crate::key::*;
use crate::keyring::*;
@@ -88,9 +90,7 @@ impl<'a> PublicKeyTrait for SignedPublicKeyOrSubkey<'a> {
/// Split data from PGP Armored Data as defined in https://tools.ietf.org/html/rfc4880#section-6.2.
///
/// Returns (type, headers, base64 encoded body).
pub fn split_armored_data(
buf: &[u8],
) -> Result<(BlockType, BTreeMap<String, String>, Vec<u8>), Error> {
pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, String>, Vec<u8>)> {
use std::io::Read;
let cursor = Cursor::new(buf);
@@ -113,12 +113,53 @@ pub fn split_armored_data(
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,
keygen_type: KeyGenType,
) -> std::result::Result<KeyPair, PgpKeygenError> {
let (secret_key_type, public_key_type) = match keygen_type {
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::EdDSA, PgpKeyType::ECDH),
};
let user_id = format!("<{}>", addr);
let key_params = SecretKeyParamsBuilder::default()
.key_type(PgpKeyType::Rsa(2048))
.key_type(secret_key_type)
.can_create_certificates(true)
.can_sign(true)
.primary_user_id(user_id)
@@ -141,27 +182,36 @@ pub fn create_keypair(addr: impl AsRef<str>) -> Option<(Key, Key)> {
])
.subkey(
SubkeyParamsBuilder::default()
.key_type(PgpKeyType::Rsa(2048))
.key_type(public_key_type)
.can_encrypt(true)
.passphrase(None)
.build()
.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.
@@ -194,7 +244,7 @@ pub fn pk_encrypt(
plain: &[u8],
public_keys_for_encryption: &Keyring,
private_key_for_signing: Option<&Key>,
) -> Result<String, Error> {
) -> Result<String> {
let lit_msg = Message::new_literal_bytes("", plain);
let pkeys: Vec<SignedPublicKeyOrSubkey> = public_keys_for_encryption
.keys()
@@ -236,7 +286,7 @@ pub fn pk_decrypt(
private_keys_for_decryption: &Keyring,
public_keys_for_validation: &Keyring,
ret_signature_fingerprints: Option<&mut HashSet<String>>,
) -> Result<Vec<u8>, Error> {
) -> Result<Vec<u8>> {
let (msg, _) = Message::from_armor_single(Cursor::new(ctext))?;
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption
.keys()
@@ -248,7 +298,7 @@ pub fn pk_decrypt(
.collect();
let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?;
let msgs = decryptor.collect::<Result<Vec<_>, _>>()?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
ensure!(!msgs.is_empty(), "No valid messages found");
let dec_msg = &msgs[0];
@@ -280,7 +330,7 @@ pub fn pk_decrypt(
}
/// Symmetric encryption.
pub fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String, Error> {
pub fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
let mut rng = thread_rng();
let lit_msg = Message::new_literal_bytes("", plain);
@@ -297,11 +347,11 @@ pub fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String, Error> {
pub fn symm_decrypt<T: std::io::Read + std::io::Seek>(
passphrase: &str,
ctext: T,
) -> Result<Vec<u8>, Error> {
) -> Result<Vec<u8>> {
let (enc_msg, _) = Message::from_armor_single(ctext)?;
let decryptor = enc_msg.decrypt_with_password(|| passphrase.into())?;
let msgs = decryptor.collect::<Result<Vec<_>, _>>()?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
ensure!(!msgs.is_empty(), "No valid messages found");
match msgs[0].get_content()? {
@@ -313,6 +363,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() {
@@ -340,4 +392,185 @@ 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(),
KeyGenType::Default,
)
.unwrap();
let keypair1 = create_keypair(
EmailAddress::new("two@zwo.de").unwrap(),
KeyGenType::Default,
)
.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);
}
}

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

@@ -0,0 +1,365 @@
// 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 },
],
};
// aol.md: aol.com
static ref P_AOL: Provider = Provider {
status: Status::BROKEN,
before_login_hint: "You can't use Delta Chat to login to AOL.",
after_login_hint: "Seems like you logged in anyway, congratulations! 🎉 Feel free to tell us at https://github.com/deltachat/provider-db/issues that AOL works again.",
overview_page: "https://providers.delta.chat/aol",
server: vec![
],
};
// 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 },
],
};
// fastmail.md: fastmail.com
static ref P_FASTMAIL: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "You must create an app-specific password for Delta Chat before you can log in.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/fastmail",
server: vec![
],
};
// 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 },
],
};
// protonmail.md: protonmail.com, protonmail.ch
static ref P_PROTONMAIL: Provider = Provider {
status: Status::BROKEN,
before_login_hint: "Protonmail does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to Protonmail.",
after_login_hint: "To use Delta Chat with Protonmail, the IMAP bridge must be running in the background. If you have connectivity issues, double check whether it works as expected.",
overview_page: "https://providers.delta.chat/protonmail",
server: vec![
],
};
// 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
// t-online.md: t-online.de, magenta.de
static ref P_T_ONLINE: Provider = Provider {
status: Status::PREPARATION,
before_login_hint: "To use Delta Chat with a T-Online email address, you need to create an app password in the web interface.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/t-online",
server: vec![
],
};
// testrun.md: testrun.org
static ref P_TESTRUN: Provider = Provider {
status: Status::OK,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/testrun",
server: vec![
Server { protocol: IMAP, socket: SSL, hostname: "testrun.org", port: 993, username_pattern: EMAIL },
Server { protocol: IMAP, socket: STARTTLS, hostname: "testrun.org", port: 143, username_pattern: EMAIL },
Server { protocol: SMTP, socket: STARTTLS, hostname: "testrun.org", port: 587, username_pattern: EMAIL },
],
};
// 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 },
],
};
pub static ref PROVIDER_DATA: HashMap<&'static str, &'static Provider> = [
("aktivix.org", &*P_AKTIVIX_ORG),
("aol.com", &*P_AOL),
("autistici.org", &*P_AUTISTICI_ORG),
("bluewin.ch", &*P_BLUEWIN_CH),
("example.com", &*P_EXAMPLE_COM),
("example.org", &*P_EXAMPLE_COM),
("fastmail.com", &*P_FASTMAIL),
("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),
("protonmail.com", &*P_PROTONMAIL),
("protonmail.ch", &*P_PROTONMAIL),
("t-online.de", &*P_T_ONLINE),
("magenta.de", &*P_T_ONLINE),
("testrun.org", &*P_TESTRUN),
("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();
}

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

@@ -0,0 +1,146 @@
//! [Provider database](https://providers.delta.chat/) module
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)

129
src/qr.rs
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) {
@@ -69,7 +75,7 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
(fp, &rest[1..])
}) {
Some(pair) => pair,
None => return format_err!("Invalid OPENPGP4FPR found").into(),
None => (payload, ""),
};
// replace & with \n to match expected param format
@@ -83,11 +89,11 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
let addr = if let Some(addr) = param.get(Param::Forwarded) {
match normalize_address(addr) {
Ok(addr) => addr,
Ok(addr) => Some(addr),
Err(err) => return err.into(),
}
} else {
return format_err!("Missing address").into();
None
};
// what is up with that param name?
@@ -157,7 +163,7 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
lot.state = LotState::QrFprWithoutAddr;
lot.text1 = Some(dc_format_fingerprint(&fingerprint));
}
} else {
} else if let Some(addr) = addr {
if grpid.is_some() && grpname.is_some() {
lot.state = LotState::QrAskVerifyGroup;
lot.text1 = grpname;
@@ -172,11 +178,80 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot {
lot.fingerprint = Some(fingerprint);
lot.invitenumber = invitenumber;
lot.auth = auth;
} else {
return format_err!("Missing address").into();
}
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=..`
@@ -471,4 +546,48 @@ mod tests {
assert_eq!(contact.get_addr(), "cli@deltachat.de");
assert_eq!(contact.get_name(), "Jörn P. P.");
}
#[test]
fn test_decode_openpgp_without_addr() {
let ctx = dummy_context();
let res = check_qr(
&ctx.ctx,
"OPENPGP4FPR:1234567890123456789012345678901234567890",
);
assert_eq!(res.get_state(), LotState::QrFprWithoutAddr);
assert_eq!(
res.get_text1().unwrap(),
"1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890"
);
assert_eq!(res.get_id(), 0);
let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890");
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

@@ -3,7 +3,7 @@
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use crate::aheader::EncryptPreference;
use crate::chat::{self, Chat};
use crate::chat::{self, Chat, ChatId};
use crate::config::*;
use crate::constants::*;
use crate::contact::*;
@@ -11,7 +11,8 @@ use crate::context::Context;
use crate::e2ee::*;
use crate::error::Error;
use crate::events::Event;
use crate::key::*;
use crate::headerdef::HeaderDef;
use crate::key::{dc_normalize_fingerprint, Key};
use crate::lot::LotState;
use crate::message::Message;
use crate::mimeparser::*;
@@ -64,15 +65,18 @@ macro_rules! get_qr_attr {
};
}
pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: u32) -> Option<String> {
/* =========================================================
pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: ChatId) -> Option<String> {
/*=======================================================
==== Alice - the inviter side ====
==== Step 1 in "Setup verified contact" protocol ====
========================================================= */
=======================================================*/
let fingerprint: String;
ensure_secret_key_exists(context).ok();
// invitenumber will be used to allow starting the handshake,
// auth will be used to verify the fingerprint
let invitenumber = token::lookup_or_new(context, token::Namespace::InviteNumber, group_chat_id);
let auth = token::lookup_or_new(context, token::Namespace::Auth, group_chat_id);
let self_addr = match context.get_config(Config::ConfiguredAddr) {
@@ -97,7 +101,8 @@ pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: u32) -> Option<Str
let self_name_urlencoded =
utf8_percent_encode(&self_name, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
let qr = if 0 != group_chat_id {
let qr = if !group_chat_id.is_unset() {
// parameters used: a=g=x=i=s=
if let Ok(chat) = Chat::load_from_db(context, group_chat_id) {
let group_name = chat.get_name();
let group_name_urlencoded =
@@ -117,6 +122,7 @@ pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: u32) -> Option<Str
return None;
}
} else {
// parameters used: a=n=i=s=
Some(format!(
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
fingerprint, self_addr_urlencoded, self_name_urlencoded, &invitenumber, &auth,
@@ -137,36 +143,41 @@ fn get_self_fingerprint(context: &Context) -> Option<String> {
None
}
pub fn dc_join_securejoin(context: &Context, qr: &str) -> u32 {
/// 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: u32, ongoing_allocated: bool, join_vg: bool| {
|context: &Context, contact_chat_id: ChatId, ongoing_allocated: bool, join_vg: bool| {
let mut bob = context.bob.write().unwrap();
bob.expects = 0;
let ret_chat_id = if bob.status == DC_BOB_SUCCESS {
let ret_chat_id: ChatId = if bob.status == DC_BOB_SUCCESS {
if join_vg {
chat::get_chat_id_by_grpid(
context,
bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(),
)
.unwrap_or((ChatId::new(0), false, Blocked::Not))
.0
} else {
contact_chat_id
}
} else {
0
ChatId::new(0)
};
bob.qr_scan = None;
if ongoing_allocated {
context.free_ongoing();
}
ret_chat_id as u32
ret_chat_id
};
/* ==========================================================
/*========================================================
==== Bob - the joiner's side =====
==== Step 2 in "Setup verified contact" protocol =====
========================================================== */
let mut contact_chat_id: u32 = 0;
========================================================*/
let mut contact_chat_id = ChatId::new(0);
let mut join_vg: bool = false;
info!(context, "Requesting secure-join ...",);
@@ -180,11 +191,13 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> u32 {
error!(context, "Unknown QR code.",);
return cleanup(&context, contact_chat_id, true, join_vg);
}
contact_chat_id = chat::create_by_contact_id(context, qr_scan.id).unwrap_or_default();
if contact_chat_id == 0 {
error!(context, "Unknown contact.",);
return cleanup(&context, contact_chat_id, true, join_vg);
}
contact_chat_id = match chat::create_by_contact_id(context, qr_scan.id) {
Ok(chat_id) => chat_id,
Err(_) => {
error!(context, "Unknown contact.");
return cleanup(&context, contact_chat_id, true, join_vg);
}
};
if context.shall_stop_ongoing() {
return cleanup(&context, contact_chat_id, true, join_vg);
}
@@ -208,10 +221,14 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> u32 {
.unwrap(),
contact_chat_id,
) {
// the scanned fingerprint matches Alice's key,
// we can proceed to step 4b) directly and save two mails
info!(context, "Taking protocol shortcut.");
context.bob.write().unwrap().expects = DC_VC_CONTACT_CONFIRM;
joiner_progress!(context, chat_id_2_contact_id(context, contact_chat_id), 400);
let own_fingerprint = get_self_fingerprint(context).unwrap_or_default();
// Bob -> Alice
send_handshake_msg(
context,
contact_chat_id,
@@ -230,6 +247,8 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> u32 {
);
} else {
context.bob.write().unwrap().expects = DC_VC_AUTH_REQUIRED;
// Bob -> Alice
send_handshake_msg(
context,
contact_chat_id,
@@ -240,24 +259,30 @@ pub fn dc_join_securejoin(context: &Context, qr: &str) -> u32 {
);
}
// Bob -> Alice
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(
context: &Context,
contact_chat_id: u32,
contact_chat_id: ChatId,
step: &str,
param2: impl AsRef<str>,
fingerprint: Option<String>,
grpid: impl AsRef<str>,
) {
let mut msg = Message::default();
msg.type_0 = Viewtype::Text;
msg.viewtype = Viewtype::Text;
msg.text = Some(format!("Secure-Join: {}", step));
msg.hidden = true;
msg.param.set_cmd(SystemMessage::SecurejoinMessage);
@@ -287,7 +312,7 @@ fn send_handshake_msg(
chat::send_msg(context, contact_chat_id, &mut msg).unwrap_or_default();
}
fn chat_id_2_contact_id(context: &Context, contact_chat_id: u32) -> u32 {
fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 {
let contacts = chat::get_chat_contacts(context, contact_chat_id);
if contacts.len() == 1 {
contacts[0]
@@ -299,7 +324,7 @@ fn chat_id_2_contact_id(context: &Context, contact_chat_id: u32) -> u32 {
fn fingerprint_equals_sender(
context: &Context,
fingerprint: impl AsRef<str>,
contact_chat_id: u32,
contact_chat_id: ChatId,
) -> bool {
let contacts = chat::get_chat_contacts(context, contact_chat_id);
@@ -318,80 +343,115 @@ fn fingerprint_equals_sender(
}
false
}
pub(crate) struct HandshakeMessageStatus {
pub(crate) hide_this_msg: bool,
pub(crate) delete_this_msg: bool,
pub(crate) stop_ongoing_process: bool,
pub(crate) bob_securejoin_success: Option<bool>,
#[derive(Fail, Debug)]
pub(crate) enum HandshakeError {
#[fail(display = "Can not be called with special contact ID")]
SpecialContactId,
#[fail(display = "Not a Secure-Join message")]
NotSecureJoinMsg,
#[fail(
display = "Failed to look up or create chat for contact #{}",
contact_id
)]
NoChat {
contact_id: u32,
#[cause]
cause: Error,
},
#[fail(display = "Chat for group {} not found", group)]
ChatNotFound { group: String },
#[fail(display = "No configured self address found")]
NoSelfAddr,
}
impl Default for HandshakeMessageStatus {
fn default() -> Self {
Self {
hide_this_msg: true,
delete_this_msg: false,
stop_ongoing_process: false,
bob_securejoin_success: None,
}
}
/// What to do with a Secure-Join handshake message after it was handled.
pub(crate) enum HandshakeMessage {
/// The message has been fully handled and should be removed/delete.
Done,
/// The message should be ignored/hidden, but not removed/deleted.
Ignore,
/// The message should be further processed by incoming message handling.
Propagate,
}
/// Handle incoming secure-join handshake.
///
/// This function will update the securejoin state in [Context::bob]
/// and also terminate the ongoing process using
/// [Context::stop_ongoing] as required by the protocol.
///
/// A message which results in [Err] will be hidden from the user but
/// not deleted, it may be a valid message for something else we are
/// not aware off. E.g. it could be part of a handshake performed by
/// another DC app on the same account.
///
/// When handle_securejoin_handshake() is called,
/// the message is not yet filed in the database;
/// this is done by receive_imf() later on as needed.
pub(crate) fn handle_securejoin_handshake(
context: &Context,
mimeparser: &MimeParser,
mime_message: &MimeMessage,
contact_id: u32,
) -> Result<HandshakeMessageStatus, Error> {
) -> Result<HandshakeMessage, HandshakeError> {
let own_fingerprint: String;
ensure!(
contact_id > DC_CONTACT_ID_LAST_SPECIAL,
"handle_securejoin_handshake(): called with special contact id"
);
let step = mimeparser
.lookup_field("Secure-Join")
.ok_or_else(|| format_err!("This message is not a Secure-Join message"))?;
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return Err(HandshakeError::SpecialContactId);
}
let step = mime_message
.get(HeaderDef::SecureJoin)
.ok_or(HandshakeError::NotSecureJoinMsg)?;
info!(
context,
">>>>>>>>>>>>>>>>>>>>>>>>> secure-join message \'{}\' received", step,
);
let (contact_chat_id, contact_chat_id_blocked) =
chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not).unwrap_or_default();
if contact_chat_id_blocked != Blocked::Not {
chat::unblock(context, contact_chat_id);
}
let contact_chat_id =
match chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not) {
Ok((chat_id, blocked)) => {
if blocked != Blocked::Not {
chat_id.unblock(context);
}
chat_id
}
Err(err) => {
return Err(HandshakeError::NoChat {
contact_id,
cause: err,
});
}
};
let join_vg = step.starts_with("vg-");
let mut ret = HandshakeMessageStatus::default();
match step.as_str() {
"vg-request" | "vc-request" => {
/* =========================================================
/*=======================================================
==== Alice - the inviter side ====
==== Step 3 in "Setup verified contact" protocol ====
========================================================= */
// this message may be unencrypted (Bob, the joinder and the sender, might not have Alice's key yet)
=======================================================*/
// this message may be unencrypted (Bob, the joiner and the sender, might not have Alice's key yet)
// it just ensures, we have Bobs key now. If we do _not_ have the key because eg. MitM has removed it,
// send_message() will fail with the error "End-to-end-encryption unavailable unexpectedly.", so, there is no additional check needed here.
// verify that the `Secure-Join-Invitenumber:`-header matches invitenumber written to the QR code
let invitenumber = match mimeparser.lookup_field("Secure-Join-Invitenumber") {
let invitenumber = match mime_message.get(HeaderDef::SecureJoinInvitenumber) {
Some(n) => n,
None => {
warn!(context, "Secure-join denied (invitenumber missing).",);
return Ok(ret);
warn!(context, "Secure-join denied (invitenumber missing)");
return Ok(HandshakeMessage::Ignore);
}
};
if !token::exists(context, token::Namespace::InviteNumber, &invitenumber) {
warn!(context, "Secure-join denied (bad invitenumber).",);
return Ok(ret);
warn!(context, "Secure-join denied (bad invitenumber).");
return Ok(HandshakeMessage::Ignore);
}
info!(context, "Secure-join requested.",);
inviter_progress!(context, contact_id, 300);
// Alice -> Bob
send_handshake_msg(
context,
contact_chat_id,
@@ -400,8 +460,15 @@ pub(crate) fn handle_securejoin_handshake(
None,
"",
);
Ok(HandshakeMessage::Done)
}
"vg-auth-required" | "vc-auth-required" => {
/*========================================================
==== Bob - the joiner's side =====
==== Step 4 in "Setup verified contact" protocol =====
========================================================*/
// verify that Alice's Autocrypt key and fingerprint matches the QR-code
let cond = {
let bob = context.bob.read().unwrap();
let scan = bob.qr_scan.as_ref();
@@ -411,26 +478,26 @@ pub(crate) fn handle_securejoin_handshake(
};
if cond {
warn!(context, "auth-required message out of sync.",);
warn!(context, "auth-required message out of sync.");
// no error, just aborted somehow or a mail from another handshake
return Ok(ret);
return Ok(HandshakeMessage::Ignore);
}
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(mimeparser, &scanned_fingerprint_of_alice) {
if !encrypted_and_signed(context, mime_message, &scanned_fingerprint_of_alice) {
could_not_establish_secure_connection(
context,
contact_chat_id,
if mimeparser.encrypted {
if mime_message.was_encrypted() {
"No valid signature."
} else {
"Not encrypted."
},
);
ret.stop_ongoing_process = true;
ret.bob_securejoin_success = Some(false);
return Ok(ret);
context.bob.write().unwrap().status = 0; // secure-join failed
context.stop_ongoing();
return Ok(HandshakeMessage::Ignore);
}
if !fingerprint_equals_sender(context, &scanned_fingerprint_of_alice, contact_chat_id) {
could_not_establish_secure_connection(
@@ -438,15 +505,16 @@ pub(crate) fn handle_securejoin_handshake(
contact_chat_id,
"Fingerprint mismatch on joiner-side.",
);
ret.stop_ongoing_process = true;
ret.bob_securejoin_success = Some(false);
return Ok(ret);
context.bob.write().unwrap().status = 0; // secure-join failed
context.stop_ongoing();
return Ok(HandshakeMessage::Ignore);
}
info!(context, "Fingerprint verified.",);
own_fingerprint = get_self_fingerprint(context).unwrap();
joiner_progress!(context, contact_id, 400);
context.bob.write().unwrap().expects = DC_VC_CONTACT_CONFIRM;
// Bob -> Alice
send_handshake_msg(
context,
contact_chat_id,
@@ -459,15 +527,17 @@ pub(crate) fn handle_securejoin_handshake(
"".to_string()
},
);
Ok(HandshakeMessage::Done)
}
"vg-request-with-auth" | "vc-request-with-auth" => {
/* ============================================================
/*==========================================================
==== Alice - the inviter side ====
==== Steps 5+6 in "Setup verified contact" protocol ====
==== Step 6 in "Out-of-band verified groups" protocol ====
============================================================ */
==========================================================*/
// verify that Secure-Join-Fingerprint:-header matches the fingerprint of Bob
let fingerprint = match mimeparser.lookup_field("Secure-Join-Fingerprint") {
let fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint) {
Some(fp) => fp,
None => {
could_not_establish_secure_connection(
@@ -475,16 +545,16 @@ pub(crate) fn handle_securejoin_handshake(
contact_chat_id,
"Fingerprint not provided.",
);
return Ok(ret);
return Ok(HandshakeMessage::Ignore);
}
};
if !encrypted_and_signed(mimeparser, &fingerprint) {
if !encrypted_and_signed(context, mime_message, &fingerprint) {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Auth not encrypted.",
);
return Ok(ret);
return Ok(HandshakeMessage::Ignore);
}
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id) {
could_not_establish_secure_connection(
@@ -492,11 +562,11 @@ pub(crate) fn handle_securejoin_handshake(
contact_chat_id,
"Fingerprint mismatch on inviter-side.",
);
return Ok(ret);
return Ok(HandshakeMessage::Ignore);
}
info!(context, "Fingerprint verified.",);
// verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code
let auth_0 = match mimeparser.lookup_field("Secure-Join-Auth") {
let auth_0 = match mime_message.get(HeaderDef::SecureJoinAuth) {
Some(auth) => auth,
None => {
could_not_establish_secure_connection(
@@ -504,12 +574,12 @@ pub(crate) fn handle_securejoin_handshake(
contact_chat_id,
"Auth not provided.",
);
return Ok(ret);
return Ok(HandshakeMessage::Ignore);
}
};
if !token::exists(context, token::Namespace::Auth, &auth_0) {
could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.");
return Ok(ret);
return Ok(HandshakeMessage::Ignore);
}
if mark_peer_as_verified(context, fingerprint).is_err() {
could_not_establish_secure_connection(
@@ -517,7 +587,7 @@ pub(crate) fn handle_securejoin_handshake(
contact_chat_id,
"Fingerprint mismatch on inviter-side.",
);
return Ok(ret);
return Ok(HandshakeMessage::Ignore);
}
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited);
info!(context, "Auth verified.",);
@@ -525,49 +595,76 @@ pub(crate) fn handle_securejoin_handshake(
emit_event!(context, Event::ContactsChanged(Some(contact_id)));
inviter_progress!(context, contact_id, 600);
if join_vg {
let field_grpid = mimeparser
.lookup_field("Secure-Join-Group")
.map(|s| s.as_str())
.unwrap_or_else(|| "");
let (group_chat_id, _, _) = chat::get_chat_id_by_grpid(context, field_grpid);
if group_chat_id == 0 {
error!(context, "Chat {} not found.", &field_grpid);
return Ok(ret);
} else if let Err(err) =
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
{
error!(context, "failed to add contact: {}", err);
// the vg-member-added message is special:
// this is a normal Chat-Group-Member-Added message
// with an additional Secure-Join header
let field_grpid = match mime_message.get(HeaderDef::SecureJoinGroup) {
Some(s) => s.as_str(),
None => {
warn!(context, "Missing Secure-Join-Group header");
return Ok(HandshakeMessage::Ignore);
}
};
match chat::get_chat_id_by_grpid(context, field_grpid) {
Ok((group_chat_id, _, _)) => {
if let Err(err) =
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
{
error!(context, "failed to add contact: {}", err);
}
}
Err(err) => {
error!(context, "Chat {} not found: {}", &field_grpid, err);
return Err(HandshakeError::ChatNotFound {
group: field_grpid.to_string(),
});
}
}
} else {
// Alice -> Bob
send_handshake_msg(context, contact_chat_id, "vc-contact-confirm", "", None, "");
inviter_progress!(context, contact_id, 1000);
}
Ok(HandshakeMessage::Done)
}
"vg-member-added" | "vc-contact-confirm" => {
if join_vg {
ret.hide_this_msg = false;
}
/*=======================================================
==== 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(ret);
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(ret);
return Ok(abort_retval);
}
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
let vg_expect_encrypted = if join_vg {
let group_id = get_qr_attr!(context, text2).to_string();
let (_, is_verified_group, _) = chat::get_chat_id_by_grpid(context, group_id);
// This is buggy, is_verified_group will always be
// false since the group is created by receive_imf by
// the very handshake message we're handling now. But
// only after we have returned. It does not impact
// the security invariants of secure-join however.
let (_, is_verified_group, _) = chat::get_chat_id_by_grpid(context, &group_id)
.unwrap_or((ChatId::new(0), false, Blocked::Not));
// when joining a non-verified group
// the vg-member-added message may be unencrypted
// when not all group members have keys or prefer encryption.
@@ -578,15 +675,15 @@ pub(crate) fn handle_securejoin_handshake(
true
};
if vg_expect_encrypted
&& !encrypted_and_signed(mimeparser, &scanned_fingerprint_of_alice)
&& !encrypted_and_signed(context, mime_message, &scanned_fingerprint_of_alice)
{
could_not_establish_secure_connection(
context,
contact_chat_id,
"Contact confirm message not encrypted.",
);
ret.bob_securejoin_success = Some(false);
return Ok(ret);
context.bob.write().unwrap().status = 0;
return Ok(abort_retval);
}
if mark_peer_as_verified(context, &scanned_fingerprint_of_alice).is_err() {
@@ -595,21 +692,26 @@ pub(crate) fn handle_securejoin_handshake(
contact_chat_id,
"Fingerprint mismatch on joiner-side.",
);
return Ok(ret);
return Ok(abort_retval);
}
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinJoined);
emit_event!(context, Event::ContactsChanged(None));
let cg_member_added = mimeparser
.lookup_field("Chat-Group-Member-Added")
let cg_member_added = mime_message
.get(HeaderDef::ChatGroupMemberAdded)
.map(|s| s.as_str())
.unwrap_or_else(|| "");
if join_vg && !addr_equals_self(context, cg_member_added) {
if join_vg
&& !context
.is_self_addr(cg_member_added)
.map_err(|_| HandshakeError::NoSelfAddr)?
{
info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group).");
return Ok(ret);
return Ok(abort_retval);
}
secure_connection_established(context, contact_chat_id);
context.bob.write().unwrap().expects = 0;
if join_vg {
// Bob -> Alice
send_handshake_msg(
context,
contact_chat_id,
@@ -619,48 +721,56 @@ pub(crate) fn handle_securejoin_handshake(
"",
);
}
ret.stop_ongoing_process = true;
ret.bob_securejoin_success = Some(true);
context.bob.write().unwrap().status = 1;
context.stop_ongoing();
Ok(if join_vg {
HandshakeMessage::Propagate
} else {
HandshakeMessage::Done
})
}
"vg-member-added-received" => {
/* ============================================================
/*==========================================================
==== Alice - the inviter side ====
==== Step 8 in "Out-of-band verified groups" protocol ====
============================================================ */
==========================================================*/
if let Ok(contact) = Contact::get_by_id(context, contact_id) {
if contact.is_verified(context) == VerifiedStatus::Unverified {
warn!(context, "vg-member-added-received invalid.",);
return Ok(ret);
return Ok(HandshakeMessage::Ignore);
}
inviter_progress!(context, contact_id, 800);
inviter_progress!(context, contact_id, 1000);
let field_grpid = mimeparser
.lookup_field("Secure-Join-Group")
let field_grpid = mime_message
.get(HeaderDef::SecureJoinGroup)
.map(|s| s.as_str())
.unwrap_or_else(|| "");
let (group_chat_id, _, _) = chat::get_chat_id_by_grpid(context, &field_grpid);
let (group_chat_id, _, _) = chat::get_chat_id_by_grpid(context, &field_grpid)
.map_err(|err| {
warn!(context, "Failed to lookup chat_id from grpid: {}", err);
HandshakeError::ChatNotFound {
group: field_grpid.to_string(),
}
})?;
context.call_cb(Event::SecurejoinMemberAdded {
chat_id: group_chat_id,
contact_id,
});
Ok(HandshakeMessage::Done)
} else {
warn!(context, "vg-member-added-received invalid.",);
return Ok(ret);
Ok(HandshakeMessage::Ignore)
}
}
_ => {
warn!(context, "invalid step: {}", step);
Ok(HandshakeMessage::Ignore)
}
}
if ret.hide_this_msg {
ret.delete_this_msg = true;
}
Ok(ret)
}
fn secure_connection_established(context: &Context, contact_chat_id: u32) {
fn secure_connection_established(context: &Context, contact_chat_id: ChatId) {
let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id);
let contact = Contact::get_by_id(context, contact_id);
let addr = if let Ok(ref contact) = contact {
@@ -673,7 +783,11 @@ fn secure_connection_established(context: &Context, contact_chat_id: u32) {
emit_event!(context, Event::ChatModified(contact_chat_id));
}
fn could_not_establish_secure_connection(context: &Context, contact_chat_id: u32, details: &str) {
fn could_not_establish_secure_connection(
context: &Context,
contact_chat_id: ChatId,
details: &str,
) {
let contact_id = chat_id_2_contact_id(context, contact_chat_id);
let contact = Contact::get_by_id(context, contact_id);
let msg = context.stock_string_repl_str(
@@ -716,22 +830,26 @@ fn mark_peer_as_verified(context: &Context, fingerprint: impl AsRef<str>) -> Res
* Tools: Misc.
******************************************************************************/
fn encrypted_and_signed(mimeparser: &MimeParser, expected_fingerprint: impl AsRef<str>) -> bool {
if !mimeparser.encrypted {
warn!(mimeparser.context, "Message not encrypted.",);
fn encrypted_and_signed(
context: &Context,
mimeparser: &MimeMessage,
expected_fingerprint: impl AsRef<str>,
) -> bool {
if !mimeparser.was_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(),
);

251
src/simplify.rs Normal file
View File

@@ -0,0 +1,251 @@
/// Remove standard (RFC 3676, §4.3) footer if it is found.
fn remove_message_footer<'a>(lines: &'a [&str]) -> &'a [&'a str] {
for (ix, &line) in lines.iter().enumerate() {
// quoted-printable may encode `-- ` to `-- =20` which is converted
// back to `-- `
match line {
"-- " | "-- " => return &lines[..ix],
_ => (),
}
}
lines
}
/// Remove nonstandard footer and a boolean indicating whether such
/// footer was removed.
fn remove_nonstandard_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
for (ix, &line) in lines.iter().enumerate() {
if line == "--"
|| line == "---"
|| line == "----"
|| line.starts_with("-----")
|| line.starts_with("_____")
|| line.starts_with("=====")
|| line.starts_with("*****")
|| line.starts_with("~~~~~")
{
return (&lines[..ix], true);
}
}
(lines, false)
}
fn split_lines(buf: &str) -> Vec<&str> {
buf.split('\n').collect()
}
/// Simplify message text for chat display.
/// Remove quotes, signatures, trailing empty lines etc.
pub fn simplify(mut input: String, is_chat_message: bool) -> (String, bool) {
input.retain(|c| c != '\r');
let lines = split_lines(&input);
let (lines, is_forwarded) = skip_forward_header(&lines);
let lines = remove_message_footer(lines);
let (lines, has_nonstandard_footer) = remove_nonstandard_footer(lines);
let (lines, has_bottom_quote) = if !is_chat_message {
remove_bottom_quote(lines)
} else {
(lines, false)
};
let (lines, has_top_quote) = if !is_chat_message {
remove_top_quote(lines)
} else {
(lines, false)
};
// re-create buffer from the remaining lines
let text = render_message(
lines,
has_top_quote,
has_nonstandard_footer || has_bottom_quote,
);
(text, is_forwarded)
}
/// Skips "forwarded message" header.
/// Returns message body lines and a boolean indicating whether
/// a message is forwarded or not.
fn skip_forward_header<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
if lines.len() >= 3
&& lines[0] == "---------- Forwarded message ----------"
&& lines[1].starts_with("From: ")
&& lines[2].is_empty()
{
(&lines[3..], true)
} else {
(lines, false)
}
}
fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
let mut last_quoted_line = None;
for (l, line) in lines.iter().enumerate().rev() {
if is_plain_quote(line) {
last_quoted_line = Some(l)
} else if !is_empty_line(line) {
break;
}
}
if let Some(mut l_last) = last_quoted_line {
if l_last > 1 && is_empty_line(lines[l_last - 1]) {
l_last -= 1
}
if l_last > 1 {
let line = lines[l_last - 1];
if is_quoted_headline(line) {
l_last -= 1
}
}
(&lines[..l_last], true)
} else {
(lines, false)
}
}
fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
let mut last_quoted_line = None;
let mut has_quoted_headline = false;
for (l, line) in lines.iter().enumerate() {
if is_plain_quote(line) {
last_quoted_line = Some(l)
} else if !is_empty_line(line) {
if is_quoted_headline(line) && !has_quoted_headline && last_quoted_line.is_none() {
has_quoted_headline = true
} else {
/* non-quoting line found */
break;
}
}
}
if let Some(last_quoted_line) = last_quoted_line {
(&lines[last_quoted_line + 1..], true)
} else {
(lines, false)
}
}
fn render_message(lines: &[&str], is_cut_at_begin: bool, is_cut_at_end: bool) -> String {
let mut ret = String::new();
if is_cut_at_begin {
ret += "[...]";
}
/* we write empty lines only in case and non-empty line follows */
let mut pending_linebreaks = 0;
let mut empty_body = true;
for line in lines {
if is_empty_line(line) {
pending_linebreaks += 1
} else {
if !empty_body {
if pending_linebreaks > 2 {
pending_linebreaks = 2
}
while 0 != pending_linebreaks {
ret += "\n";
pending_linebreaks -= 1
}
}
// the incoming message might contain invalid UTF8
ret += line;
empty_body = false;
pending_linebreaks = 1
}
}
if is_cut_at_end && (!is_cut_at_begin || !empty_body) {
ret += " [...]";
}
ret
}
/**
* Tools
*/
fn is_empty_line(buf: &str) -> bool {
// XXX: can it be simplified to buf.chars().all(|c| c.is_whitespace())?
//
// Strictly speaking, it is not equivalent (^A is not whitespace, but less than ' '),
// but having control sequences in email body?!
//
// See discussion at: https://github.com/deltachat/deltachat-core-rust/pull/402#discussion_r317062392
for c in buf.chars() {
if c > ' ' {
return false;
}
}
true
}
fn is_quoted_headline(buf: &str) -> bool {
/* This function may be called for the line _directly_ before a quote.
The function checks if the line contains sth. like "On 01.02.2016, xy@z wrote:" in various languages.
- Currently, we simply check if the last character is a ':'.
- Checking for the existence of an email address may fail (headlines may show the user's name instead of the address) */
buf.len() <= 80 && buf.ends_with(':')
}
fn is_plain_quote(buf: &str) -> bool {
buf.starts_with('>')
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
// proptest does not support [[:graphical:][:space:]] regex.
fn test_simplify_plain_text_fuzzy(input in "[!-~\t \n]+") {
let (output, _is_forwarded) = simplify(input, true);
assert!(output.split('\n').all(|s| s != "-- "));
}
}
#[test]
fn test_simplify_trim() {
let input = "line1\n\r\r\rline2".to_string();
let (plain, is_forwarded) = simplify(input, false);
assert_eq!(plain, "line1\nline2");
assert!(!is_forwarded);
}
#[test]
fn test_simplify_forwarded_message() {
let input = "---------- Forwarded message ----------\r\nFrom: test@example.com\r\n\r\nForwarded message\r\n-- \r\nSignature goes here".to_string();
let (plain, is_forwarded) = simplify(input, false);
assert_eq!(plain, "Forwarded message");
assert!(is_forwarded);
}
#[test]
fn test_simplify_utilities() {
assert!(is_empty_line(" \t"));
assert!(is_empty_line(""));
assert!(is_empty_line(" \r"));
assert!(!is_empty_line(" x"));
assert!(is_plain_quote("> hello world"));
assert!(is_plain_quote(">>"));
assert!(!is_plain_quote("Life is pain"));
assert!(!is_plain_quote(""));
}
#[test]
fn test_remove_top_quote() {
let (lines, has_top_quote) = remove_top_quote(&["> first", "> second"]);
assert!(lines.is_empty());
assert!(has_top_quote);
let (lines, has_top_quote) = remove_top_quote(&["> first", "> second", "not a quote"]);
assert_eq!(lines, &["not a quote"]);
assert!(has_top_quote);
let (lines, has_top_quote) = remove_top_quote(&["not a quote", "> first", "> second"]);
assert_eq!(lines, &["not a quote", "> first", "> second"]);
assert!(!has_top_quote);
}
}

View File

@@ -2,31 +2,49 @@
pub mod send;
use lettre::smtp::client::net::*;
use lettre::*;
use std::time::{Duration, Instant};
use async_smtp::smtp::client::net::*;
use async_smtp::*;
use crate::constants::*;
use crate::context::Context;
use crate::events::Event;
use crate::login_param::{dc_build_tls_config, LoginParam};
use crate::login_param::{dc_build_tls, LoginParam};
use crate::oauth2::*;
/// SMTP write and read timeout in seconds.
const SMTP_TIMEOUT: u64 = 30;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "Bad parameters")]
BadParameters,
#[fail(display = "Invalid login address {}: {}", address, error)]
InvalidLoginAddress {
address: String,
#[cause]
error: lettre::error::Error,
error: error::Error,
},
#[fail(display = "SMTP failed to connect: {:?}", _0)]
ConnectionFailure(#[cause] lettre::smtp::error::Error),
ConnectionFailure(#[cause] smtp::error::Error),
#[fail(display = "SMTP: failed to setup connection {:?}", _0)]
ConnectionSetupFailure(#[cause] lettre::smtp::error::Error),
ConnectionSetupFailure(#[cause] smtp::error::Error),
#[fail(display = "SMTP: oauth2 error {:?}", _0)]
Oauth2Error { address: String },
#[fail(display = "TLS error")]
Tls(#[cause] async_native_tls::Error),
}
impl From<async_native_tls::Error> for Error {
fn from(err: async_native_tls::Error) -> Error {
Error::Tls(err)
}
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -34,10 +52,15 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Default, DebugStub)]
pub struct Smtp {
#[debug_stub(some = "SmtpTransport")]
transport: Option<lettre::smtp::SmtpTransport>,
transport_connected: bool,
transport: Option<smtp::SmtpTransport>,
/// Email address we are sending from.
from: Option<EmailAddress>,
/// Timestamp of last successful send/receive network interaction
/// (eg connect or send succeeded). On initialization and disconnect
/// it is set to None.
last_success: Option<Instant>,
}
impl Smtp {
@@ -48,22 +71,36 @@ impl Smtp {
/// Disconnect the SMTP transport and drop it entirely.
pub fn disconnect(&mut self) {
if self.transport.is_none() || !self.transport_connected {
return;
if let Some(mut transport) = self.transport.take() {
async_std::task::block_on(transport.close()).ok();
}
let mut transport = self.transport.take().unwrap();
transport.close();
self.transport_connected = false;
self.last_success = None;
}
/// Check if a connection already exists.
/// Return true if smtp was connected but is not known to
/// have been successfully used the last 60 seconds
pub fn has_maybe_stale_connection(&self) -> bool {
if let Some(last_success) = self.last_success {
Instant::now().duration_since(last_success).as_secs() > 60
} else {
false
}
}
/// Check whether we are connected.
pub fn is_connected(&self) -> bool {
self.transport.is_some()
self.transport
.as_ref()
.map(|t| t.is_connected())
.unwrap_or_default()
}
/// Connect using the provided login params
/// Connect using the provided login params.
pub fn connect(&mut self, context: &Context, lp: &LoginParam) -> Result<()> {
async_std::task::block_on(self.inner_connect(context, lp))
}
async fn inner_connect(&mut self, context: &Context, lp: &LoginParam) -> Result<()> {
if self.is_connected() {
warn!(context, "SMTP already connected.");
return Ok(());
@@ -84,7 +121,7 @@ impl Smtp {
let domain = &lp.send_server;
let port = lp.send_port as u16;
let tls_config = dc_build_tls_config(lp.smtp_certificate_checks);
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) {
@@ -99,21 +136,21 @@ impl Smtp {
}
let user = &lp.send_user;
(
lettre::smtp::authentication::Credentials::new(
smtp::authentication::Credentials::new(
user.to_string(),
access_token.unwrap_or_default(),
),
vec![lettre::smtp::authentication::Mechanism::Xoauth2],
vec![smtp::authentication::Mechanism::Xoauth2],
)
} else {
// plain
let user = lp.send_user.clone();
let pw = lp.send_pw.clone();
(
lettre::smtp::authentication::Credentials::new(user, pw),
smtp::authentication::Credentials::new(user, pw),
vec![
lettre::smtp::authentication::Mechanism::Plain,
lettre::smtp::authentication::Mechanism::Login,
smtp::authentication::Mechanism::Plain,
smtp::authentication::Mechanism::Login,
],
)
};
@@ -121,28 +158,32 @@ impl Smtp {
let security = if 0
!= lp.server_flags & (DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_PLAIN) as i32
{
lettre::smtp::ClientSecurity::Opportunistic(tls_parameters)
smtp::ClientSecurity::Opportunistic(tls_parameters)
} else {
lettre::smtp::ClientSecurity::Wrapper(tls_parameters)
smtp::ClientSecurity::Wrapper(tls_parameters)
};
let client = lettre::smtp::SmtpClient::new((domain.as_str(), port), security)
let client = smtp::SmtpClient::with_security((domain.as_str(), port), security)
.await
.map_err(Error::ConnectionSetupFailure)?;
let client = client
.smtp_utf8(true)
.credentials(creds)
.authentication_mechanism(mechanism)
.connection_reuse(lettre::smtp::ConnectionReuseParameters::ReuseUnlimited);
let mut trans = client.transport();
trans.connect().map_err(Error::ConnectionFailure)?;
.connection_reuse(smtp::ConnectionReuseParameters::ReuseUnlimited)
.timeout(Some(Duration::from_secs(SMTP_TIMEOUT)));
let mut trans = client.into_transport();
trans.connect().await.map_err(Error::ConnectionFailure)?;
self.transport = Some(trans);
self.transport_connected = true;
self.last_success = Some(Instant::now());
context.call_cb(Event::SmtpConnected(format!(
"SMTP-LOGIN as {} ok",
lp.send_user,
)));
Ok(())
}
}

View File

@@ -1,7 +1,7 @@
//! # SMTP message sending
use super::Smtp;
use lettre::*;
use async_smtp::*;
use crate::context::Context;
use crate::events::Event;
@@ -11,9 +11,11 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "Envelope error: {}", _0)]
EnvelopeError(#[cause] lettre::error::Error),
EnvelopeError(#[cause] async_smtp::error::Error),
#[fail(display = "Send error: {}", _0)]
SendError(#[cause] lettre::smtp::error::Error),
SendError(#[cause] async_smtp::smtp::error::Error),
#[fail(display = "SMTP has no transport")]
NoTransport,
}
@@ -21,7 +23,7 @@ pub enum Error {
impl Smtp {
/// Send a prepared mail to recipients.
/// On successful send out Ok() is returned.
pub fn send(
pub async fn send(
&mut self,
context: &Context,
recipients: Vec<EmailAddress>,
@@ -36,22 +38,23 @@ impl Smtp {
.collect::<Vec<String>>()
.join(",");
if let Some(ref mut transport) = self.transport {
let envelope =
Envelope::new(self.from.clone(), recipients).map_err(Error::EnvelopeError)?;
let mail = SendableEmail::new(
envelope,
format!("{}", job_id), // only used for internal logging
message,
);
let envelope =
Envelope::new(self.from.clone(), recipients).map_err(Error::EnvelopeError)?;
let mail = SendableEmail::new(
envelope,
format!("{}", job_id), // only used for internal logging
message,
);
transport.send(mail).map_err(Error::SendError)?;
if let Some(ref mut transport) = self.transport {
transport.send(mail).await.map_err(Error::SendError)?;
context.call_cb(Event::SmtpMessageSent(format!(
"Message len={} was smtp-sent to {}",
message_len, recipients_display
)));
self.transport_connected = true;
self.last_success = Some(std::time::Instant::now());
Ok(())
} else {
warn!(

View File

@@ -7,7 +7,7 @@ use std::time::Duration;
use rusqlite::{Connection, OpenFlags, Statement, NO_PARAMS};
use thread_local_object::ThreadLocal;
use crate::chat::update_saved_messages_icon;
use crate::chat::{update_device_icon, update_saved_messages_icon};
use crate::constants::ShowEmails;
use crate::context::Context;
use crate::dc_tools::*;
@@ -113,16 +113,16 @@ impl Sql {
self.with_conn(|conn| conn.execute(sql, params).map_err(Into::into))
}
fn with_conn<T, G>(&self, g: G) -> Result<T>
pub fn with_conn<T, G>(&self, g: G) -> Result<T>
where
G: FnOnce(&Connection) -> Result<T>,
G: FnOnce(&mut Connection) -> Result<T>,
{
let res = match &*self.pool.read().unwrap() {
Some(pool) => {
let conn = pool.get()?;
let mut conn = pool.get()?;
// Only one process can make changes to the database at one time.
// busy_timeout defines, that if a seconds process wants write access,
// busy_timeout defines, that if a second process wants write access,
// this second process will wait some milliseconds
// and try over until it gets write access or the given timeout is elapsed.
// If the second process does not get write access within the given timeout,
@@ -130,7 +130,7 @@ impl Sql {
// (without a busy_timeout, sqlite3_step() would return SQLITE_BUSY _at once_)
conn.busy_timeout(Duration::from_secs(10))?;
g(&conn)
g(&mut conn)
}
None => Err(Error::SqlNoConnection),
};
@@ -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
@@ -343,7 +360,7 @@ impl Sql {
.and_then(|r| r.parse().ok())
}
fn start_stmt(&self, stmt: impl AsRef<str>) {
pub fn start_stmt(&self, stmt: impl AsRef<str>) {
if let Some(query) = self.in_use.get_cloned() {
let bt = backtrace::Backtrace::new();
eprintln!("old query: {}", query);
@@ -365,6 +382,7 @@ fn table_exists(conn: &Connection, name: impl AsRef<str>) -> Result<bool> {
Ok(exists)
}
#[allow(clippy::cognitive_complexity)]
fn open(
context: &Context,
sql: &Sql,
@@ -544,7 +562,7 @@ fn open(
// --------------------------------------------------------------------
let mut dbversion = dbversion_before_update;
let mut recalc_fingerprints = 0;
let mut recalc_fingerprints = false;
let mut update_icons = false;
if dbversion < 1 {
@@ -687,7 +705,7 @@ fn open(
"CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);",
params![],
)?;
recalc_fingerprints = 1;
recalc_fingerprints = true;
dbversion = 34;
sql.set_raw_config_int(context, "dbversion", 34)?;
}
@@ -848,7 +866,6 @@ fn open(
if exists_before_update && sql.get_raw_config_int(context, "bcc_self").is_none() {
sql.set_raw_config_int(context, "bcc_self", 1)?;
}
update_icons = true;
sql.set_raw_config_int(context, "dbversion", 59)?;
}
if dbversion < 60 {
@@ -859,12 +876,34 @@ fn open(
)?;
sql.set_raw_config_int(context, "dbversion", 60)?;
}
if dbversion < 61 {
info!(context, "[migration] v61");
sql.execute(
"ALTER TABLE contacts ADD COLUMN selfavatar_sent INTEGER DEFAULT 0;",
NO_PARAMS,
)?;
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)?;
}
if dbversion < 63 {
info!(context, "[migration] v63");
sql.execute("UPDATE chats SET grpid='' WHERE type=100", NO_PARAMS)?;
sql.set_raw_config_int(context, "dbversion", 63)?;
}
// (2) updates that require high-level objects
// (the structure is complete now and all objects are usable)
// --------------------------------------------------------------------
if 0 != recalc_fingerprints {
if recalc_fingerprints {
info!(context, "[migration] recalc fingerprints");
sql.query_map(
"SELECT addr FROM acpeerstates;",
@@ -884,6 +923,7 @@ fn open(
}
if update_icons {
update_saved_messages_icon(context)?;
update_device_icon(context)?;
}
}

View File

@@ -185,6 +185,9 @@ pub enum StockMessage {
Recipients don't need to install Delta Chat, visit websites or sign up anywhere - \
however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
WelcomeMessage = 71,
#[strum(props(fallback = "Unknown Sender for this chat. See 'info' for more details."))]
UnknownSenderForChat = 72,
}
/*
@@ -544,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

@@ -2,20 +2,19 @@
//!
//! This module is only compiled for test runs.
use libc::uintptr_t;
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,
}
@@ -26,12 +25,12 @@ 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 {
Some(cb) => cb,
None => Box::new(|_, _| 0),
None => Box::new(|_, _| ()),
};
let ctx = Context::new(cb, "FakeOs".into(), dbfile).unwrap();
TestContext { ctx, dir }
@@ -42,41 +41,63 @@ 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) -> uintptr_t {
pub(crate) fn logging_cb(_ctx: &Context, evt: Event) {
match evt {
Event::Info(msg) => println!("I: {}", msg),
Event::Warning(msg) => println!("W: {}", msg),
Event::Error(msg) => println!("E: {}", msg),
_ => (),
}
0
}
/// 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

@@ -6,6 +6,7 @@
use deltachat_derive::*;
use crate::chat::ChatId;
use crate::context::Context;
use crate::dc_tools::*;
use crate::sql;
@@ -27,28 +28,28 @@ impl Default for Namespace {
/// Creates a new token and saves it into the database.
/// Returns created token.
pub fn save(context: &Context, namespace: Namespace, foreign_id: u32) -> String {
pub fn save(context: &Context, namespace: Namespace, foreign_id: ChatId) -> String {
// foreign_id may be 0
let token = dc_create_id();
sql::execute(
context,
&context.sql,
"INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);",
params![namespace, foreign_id as i32, &token, time()],
params![namespace, foreign_id, &token, time()],
)
.ok();
token
}
pub fn lookup(context: &Context, namespace: Namespace, foreign_id: u32) -> Option<String> {
pub fn lookup(context: &Context, namespace: Namespace, foreign_id: ChatId) -> Option<String> {
context.sql.query_get_value::<_, String>(
context,
"SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;",
params![namespace, foreign_id as i32],
params![namespace, foreign_id],
)
}
pub fn lookup_or_new(context: &Context, namespace: Namespace, foreign_id: u32) -> String {
pub fn lookup_or_new(context: &Context, namespace: Namespace, foreign_id: ChatId) -> String {
lookup(context, namespace, foreign_id).unwrap_or_else(|| save(context, namespace, foreign_id))
}

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