Compare commits

...

178 Commits

Author SHA1 Message Date
B. Petersen
e93cbae879 stop timings 2021-04-22 20:15:04 +02:00
Hocuri
a1ef32170d Make logging less verbose 2021-04-22 18:03:37 +02:00
Hocuri
a4486d8c30 Fix #2335 (delete job flooding) (#2372)
* Fix #2335 (delete job flooding)

The problem was:

- You are offline, and an ephemeral message is due to delete.
- load_imap_deletion_job() is called and returns a deletion job for it.
- The job fails because, well, we are offline.
- The job is saved into the database.
- load_imap_deletion_job() is called again, and so on.

* Add test
2021-04-22 16:37:22 +02:00
link2xt
7bdae8b2c5 README.md: replace shields.io with official CircleCI badge
Also remove appveyor, it is not used anymore.
2021-04-21 07:23:48 +03:00
link2xt
75999c5d5a test_account.py: fix syntax error on python 3.5
It was introduced in 553d3936a9
2021-04-21 02:49:20 +03:00
B. Petersen
34ffa4e7ea add a test to check LIMIT on global searches 2021-04-20 13:51:49 +02:00
B. Petersen
3f1623eab1 LIMIT global search 2021-04-20 13:51:49 +02:00
link2xt
99373774aa search_msgs: do not match contact names
ct.name was insufficient, as authname, overridden name and email address
fallback were not taken into account.

Dropping this condition increases performance by 25% according to the
benchmark.

Also add a test for search_msgs.
2021-04-20 12:21:04 +02:00
link2xt
acd51a7058 Sort global message search result only by ID
It reduces the time by ~20% according to `search_msgs` benchmark.

Sorting by IDs is sufficient for global search as IDs increase in the
order of message reception.
2021-04-20 00:35:21 +03:00
B. Petersen
61bf0b208c add some tests for constants 2021-04-19 23:09:27 +03:00
link2xt
efd0314872 dc_receive_imf: remove unnecessary check for empty folder name
This check dates back to C core, where it checked for NULL, not empty string.
2021-04-19 23:09:17 +03:00
link2xt
ef89bc64c9 Add search_msgs benchmark 2021-04-19 23:09:00 +03:00
link2xt
6d4ec75a7b Benchmark reading contact list 2021-04-19 23:09:00 +03:00
link2xt
8af47de5a4 Benchmark adding 500 contacts from address book 2021-04-19 23:09:00 +03:00
link2xt
c7345c16f8 README: update CI status badges 2021-04-19 01:23:59 +03:00
link2xt
a4b14c6b98 ci: update configs to use scripts from scripts/ directory 2021-04-18 21:57:04 +03:00
link2xt
321354531d Move set_core_version.py into scripts/
Also make it executable.
2021-04-18 21:57:04 +03:00
link2xt
5b0f07f9a7 Move run-integration-tests.sh into scripts/ 2021-04-18 21:57:04 +03:00
link2xt
87cb5de8b1 Rename ci_scripts/ into scripts/ 2021-04-18 21:57:04 +03:00
link2xt
baae31117f server_params.rs: increase test coverage 2021-04-18 21:56:10 +03:00
Hocuri
553d3936a9 Some general Python test improvements (#2316)
This PR originally contained a fix for sqlx which turned out not not to be necessary. But on the way, I made some general improvements:

- Under some circumstances, a "normal" test failure led to a timeout, without printing a decent error message. See e.g. https://app.circleci.com/pipelines/github/deltachat/deltachat-core-rust/8069/workflows/ba2a9949-b4ad-4ceb-a930-073bba05e2db/jobs/30965.
  (The problem was: if there is an exception in dc_account_extra_configure(), when trying to handle the exception the line account.log("===================", e, "===================") was called, which can't work as log() only expects one parameter)
- When a test fails: Call `dump_account_info()` even if there is no direct_imap
- In test_import_export_online_all(), add another 100KB file to the backup. This adds resilience against future size changes of the sql db file: The test tests the smoothness of the progress bar. And if there are there are not enough about-equally-sized files, the progress bar can't be smooth.
2021-04-18 19:20:31 +02:00
link2xt
004fb76864 Remove println! calls from test_group_with_removed_message_id()
They were accidentally added in 6bb5721f29

Given that they are full of typos, they were probably not meant to be commited.
2021-04-18 18:37:49 +03:00
link2xt
09bc8fc603 dc_tools: remove dead code from the test
Since temporary directory is used, files from previous tests can't exist in blobdir.
2021-04-18 02:57:17 +03:00
link2xt
8f1bb38a3b Fix a comment typo 2021-04-18 02:56:34 +03:00
B. Petersen
2688f233b8 add timeinfo for 'listmsgs' repl command 2021-04-18 00:51:51 +02:00
B. Petersen
7be8fb7245 bumb version to 1.53.0 2021-04-18 00:51:51 +02:00
B. Petersen
a9b8776342 update changelog for 1.53.0 2021-04-18 00:51:51 +02:00
link2xt
9a34fe5c70 sql: enable shared cache 2021-04-17 23:47:21 +03:00
link2xt
e35a8d4415 sql: use sqlite3_last_insert_rowid instead of SELECT 2021-04-17 22:59:34 +03:00
Asiel Díaz Benítez
59dea29e88 Merge pull request #2333 from deltachat/adb-issue-2328
Add html API
2021-04-17 13:55:48 -04:00
Asiel Díaz Benítez
cfdc841c7e Update python/tests/test_account.py
Co-authored-by: Hocuri <hocuri@gmx.de>
2021-04-17 12:45:50 -04:00
Asiel Díaz Benítez
2974affaeb Merge pull request #2334 from deltachat/adb-issue-2329
Add "override_sender_name" API
2021-04-16 15:23:31 -04:00
link2xt
69dae4c006 Switch to release version of sqlx 2021-04-16 22:05:21 +03:00
link2xt
a795ae98ee Test saving and loading of LoginParam 2021-04-16 21:47:39 +03:00
link2xt
ac54301923 mimefactory: resultify is_file_size_okay 2021-04-16 21:47:29 +03:00
B. Petersen
9ecb6d9b15 test dehtml for pre-tag (wrote that little test to test the new coverage script :)[D 2021-04-15 01:49:12 +03:00
link2xt
ac9394cb16 dehtml.rs: test </i> tag 2021-04-15 00:30:50 +03:00
link2xt
edb9ea0e83 format_flowed.rs: increase line coverage to 100% 2021-04-15 00:30:50 +03:00
link2xt
4c1315446e Add coverage script 2021-04-15 00:30:50 +03:00
B. Petersen
cc6b02f037 remove additional tags: check from ci, it is 'master branch || has tags' so that does not make much sense, generation should be done on master only 2021-04-14 16:55:45 +02:00
B. Petersen
e13bb8fbd4 bump version to 1.52.0 2021-04-14 16:55:45 +02:00
B. Petersen
eaca4446aa update changelog to 1.52 2021-04-14 16:55:45 +02:00
B. Petersen
f3fb26c066 add a test to search for one-to-one-chats coming without authnames 2021-04-14 14:09:35 +02:00
B. Petersen
1a1416e446 change chat names correctly on contact name change
the user-given contact name may be set to an empty string;
in this case the authname or the email is used for the contact
and also for the name of possibly existing chats.

this works well for `dc_chat_get_name()` as that just uses
`dc_contact_get_display_name()` for single-chats.

it did not work for `dc_get_chatlist(query)` as that
uses the database for performance reasons -
however, in the database, the empty string is written
instead of the display name is written for a chat.

this is fixed by this pr by also using
dc_contact_get_display_name() when updating the chats-table
(similar to `dc_create_chat_by_contact_id()`)
2021-04-14 14:09:35 +02:00
B. Petersen
0afc07f6e7 add a test to search for one-to-one-chats 2021-04-14 14:09:35 +02:00
adbenitez
e6d2b1052c fix typo 2021-04-13 04:23:36 -04:00
adbenitez
19c1e6efc3 try to fix test 2021-04-13 04:11:15 -04:00
adbenitez
26d9addc5d improve test, test has_html() 2021-04-13 04:11:15 -04:00
adbenitez
6601015a09 add test_html_message() 2021-04-13 04:11:15 -04:00
adbenitez
36653928f7 add html API 2021-04-13 04:11:15 -04:00
Asiel Díaz Benítez
dda3c605c6 Merge pull request #2326 from deltachat/adb-fix-message-id
make Message.id a dynamic property
2021-04-13 04:08:50 -04:00
adbenitez
2e015e685f add aditional check 2021-04-13 03:53:37 -04:00
adbenitez
d4a1858d41 try to fix test 2021-04-13 03:53:21 -04:00
adbenitez
d6a6ba01e4 fix test 2021-04-13 02:38:42 -04:00
adbenitez
27714f596e add test_message_override_sender_name() 2021-04-13 02:14:53 -04:00
link2xt
d4e065ee84 Update once_cell, base64, itertools, strum and strum_macros 2021-04-13 02:18:15 +03:00
link2xt
bc222af661 Cargo.toml: sort dependencies alphabetically
This is what we do in rPGP too.
2021-04-13 02:18:15 +03:00
adbenitez
f6136f0ecc fix Chat.prepare_message() 2021-04-12 14:59:15 -04:00
adbenitez
b2517d3060 make Message.id a property 2021-04-12 14:59:15 -04:00
link2xt
244260a978 Fix nightly clippy and rustc errors 2021-04-12 21:33:52 +03:00
adbenitez
dc6fb7d481 add "override_sender_name" API 2021-04-11 21:18:46 -04:00
Asiel Díaz Benítez
f17320a9cb Merge pull request #2332 from deltachat/adb-issue-2327
Add sticker viewtype
2021-04-11 20:49:49 -04:00
adbenitez
d1237c9f8d add Message.is_sticker() 2021-04-11 20:37:47 -04:00
adbenitez
b9beaee7d4 update Message.new_empty() to allow view_type="sticker" and also allow using
message type directly (ex. `const.DC_MSG_STICKER`) so if new message types are
added, they can be used direcly without needing the python API to be updated.
2021-04-11 20:37:47 -04:00
adbenitez
258856c23a add sticker type 2021-04-11 20:37:47 -04:00
adbenitez
72ddd33adf avoid for loop 2021-04-11 20:37:47 -04:00
link2xt
1cd53aafff Add support for "Mixed Up" MIME format
This is an PGP/MIME format produced by Microsoft Exchange and ProtonMail IMAP/SMTP Bridge,
described in detail in https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html

This patch adds seamless support for "Mixed Up" Encryption, repairing
mangled Autocrypt messages without notifying the user.
2021-04-11 19:50:59 +03:00
link2xt
4d2ac5a3a2 ci: switch to v2 of actions/checkout 2021-04-11 14:45:27 +03:00
link2xt
146db48c35 ci: use DCC_NEW_TMP_EMAIL for remote python tests 2021-04-11 14:45:27 +03:00
link2xt
9529d76d82 ci: update ci_scripts README 2021-04-11 14:45:27 +03:00
link2xt
b5f2752e41 python: remove DCC_PY_LIVECONFIG references from all scripts
This variable is not used anymore.
2021-04-11 14:45:27 +03:00
link2xt
ce4675e9f7 ci: move remote python tests from CircleCI to GitHub Actions 2021-04-11 14:45:27 +03:00
link2xt
f0bd129636 ci: fix syntax of git --format in run-doxygen.sh
git version 2.31.0 throws fatal error on --format without "="
2021-04-11 14:45:27 +03:00
link2xt
dfe3cabb14 circleci: remove remote_tests_rust
Rust tests are already running on GitHub Actions, this is duplicate work.
2021-04-11 14:45:27 +03:00
link2xt
09735b808e circleci: remove unused jobs 2021-04-11 14:45:27 +03:00
link2xt
37f68459f6 sql: make all queries persistent and update to upstream sqlx
&str queries are not persistent by default.  To make queries persistent,
they have to be constructed with sqlx::query.

Upstream sqlx does not contain the change that make all queries
persistent, but it is not needed anymore. but
2021-04-10 22:24:12 +02:00
Hocuri
3707471266 Add alias support 2 (#2297)
fix  #2073
fix #2292 (I think)

- Messages can be assigned to any chat by the References and In-Reply-To, also to 1:1 chats; this has higher priority than the group id because with ad-hoc groups, it can happen that two devices have different group ids for the same conversation thread.
- If `From` is not in the chat (we call this "shadow sender"), `OverrideSenderDisplayname` is set. This communicates to the UI that:
  - A `~`should be added in front of the sender's displayname.
  - Also in 1:1 chats, the sender's displayname and avatar is shown, as if this was a group.

  The "Unknown sender for this chat" messages are completely removed for unprotected groups.

For protected chats, everything stays as it was before.

POSTPONED:

- Maybe (if it turns out to be still necessary):
  - The ad-hoc group id is computed by the the References, instead of the member list, as it is currently done
  - How do we prevent that the message can't be assigned to the correct chat as the parent message was deleted?
2021-04-10 22:06:22 +02:00
Hocuri
5394327bf6 More logging for "core spams imap events"
TODO: revert
2021-04-10 17:08:41 +03:00
Hocuri
df277b374d Ignore unknown classical emails from spam folder (#2311) 2021-04-10 10:45:47 +02:00
link2xt
53dba3c1ba Merge in sqlx fixes 2021-04-08 22:54:58 +03:00
link2xt
6540ee60e5 Update Cargo.lock 2021-04-08 22:05:15 +03:00
link2xt
66b5084a1d Switch to /deltachat/ org fork of sqlx 2021-04-08 21:58:04 +03:00
link2xt
f76aaf3205 sql: enable virtual statement cache on the reader pool
A follow-up to 720135a915
2021-04-07 21:43:34 +03:00
Hocuri
179a2a50e6 Parse <blockquote> tags for better quote detection (#2313) 2021-04-07 18:45:00 +02:00
link2xt
720135a915 Update sqlx to enable statement cache 2021-04-07 12:41:23 +03:00
Friedel Ziegelmayer
6bb5721f29 feat: improve internal sql interface
Switches from rusqlite to sqlx to have a fully async based interface
to sqlite.

Co-authored-by: B. Petersen <r10s@b44t.com>
Co-authored-by: Hocuri <hocuri@gmx.de>
Co-authored-by: link2xt <link2xt@testrun.org>
2021-04-06 16:06:11 +02:00
link2xt
4dedc2d8ce Fix a comment typo 2021-03-27 21:11:34 +03:00
link2xt
ede9bdc018 Reduce required cmake version to 3.16 2021-03-27 00:17:04 +03:00
holger krekel
11823d3b45 use master for tag-buids of upload wheels job 2021-03-23 22:20:14 +01:00
missytake
734ea8ab1b Merge pull request #2314 from deltachat/py51release
prepare 1.51.0 release
2021-03-23 19:42:39 +01:00
holger krekel
7017a050cb prepare 1.51.0 release 2021-03-23 18:55:34 +01:00
B. Petersen
96e57e7ef3 bump version to 1.51.0 2021-03-23 18:51:26 +01:00
B. Petersen
02bc334af5 update changelog for 1.51 2021-03-23 18:51:26 +01:00
Simon Laux
c8fea9c577 Merge pull request #2303 from deltachat/add_cmake_build_to_gitignore
add /build directory to .gitignore
2021-03-21 17:12:03 +01:00
link2xt
cdc1063d83 Do not reset user status after receiving a read receipt
Read receipts never contain the signature, so previously receiving it
cleared the status.
2021-03-21 18:54:08 +03:00
Simon Laux
704a902cc5 add build directory to gitignore
(libdeltachat generated with cmake)
2021-03-20 18:23:30 +01:00
B. Petersen
36aef6499d update provider database 2021-03-18 21:55:33 +01:00
B. Petersen
4ba9c2fafa fix clippy error on generating rust code from python 2021-03-18 21:55:33 +01:00
Hocuri
0de8b6a7e5 Update uid_next if the server rewinded it
fix #2188

Also, if we notice that the server started reusing old UIDs, _also_ do a `ResyncFolders`, because the server likely forgot to change uid_validity
2021-03-18 16:14:56 +03:00
link2xt
04f816be31 qr: return QrFprMismatch on fingerprint mismatch
Previously QrFprWithoutAddr was returned incorrectly.

Also fix spelling error ("Missmatch").
2021-03-15 21:39:10 +03:00
Hocuri
7bc919fad5 Save subject for messages 2: Outgoing messages (#2283)
* Save subject for sent-out messages

* Test that subject is saved (outgoing)

* Correctly set subject when forwarding messages, add test for this
2021-03-14 15:07:49 +01:00
B. Petersen
db3f87dd77 add a test that fails if 'References:'-header is missing 2021-03-14 14:38:53 +01:00
B. Petersen
f43555b41c set References header to Message-ID on top-level messages to add some resilience against smtp-server changing Message-ID header 2021-03-14 14:38:53 +01:00
Hocuri
98fc559536 Even nicer logging: Add ok_or_log() and more (#2284)
Co-authored-by: Floris Bruynooghe <flub@devork.be>
2021-03-13 21:06:37 +01:00
B. Petersen
4ab90f7069 add DoxygenLayout.xml file
`DoxygenLayout.xml` is picked up by doxygen automatically (as `Doxyfile`)
and its template can be generated by `doxygen -l DoxygenLayout.xml`.

after that, the following tweaks were done:
- remove all xml-parts but header (other defaults are fine so far)
- rename "Modules" header to "Constants" (there are no modules :)
- remove useless subitems from "Classes", eg. by name only shows as "d"
- reorder tabs so that "Classes" becomes the first one
2021-03-13 22:19:12 +03:00
Hocuri
99b2d79312 When forwarding a message that is an impersonating message, the forwarder should not be impersonating
fix #2287
2021-03-12 11:47:02 +01:00
B. Petersen
6963fd877d allow one additional boolean
the function is not complex and is only called once.
refactoring that seems to be a bit too much effort
and at least out of scope of this pr.
2021-03-12 11:30:25 +01:00
B. Petersen
045fbab7cd remove subsequent images inside multipart/related 2021-03-12 11:30:25 +01:00
B. Petersen
f69bcc71ed mark child-parts of multipart/related as such 2021-03-12 11:30:25 +01:00
B. Petersen
c1fddebc06 add test for multipart/related mails 2021-03-12 11:30:25 +01:00
Hocuri
04891238d4 save subject for messages (#2274)
save subject for messages:

- new api `dc_msg_get_subject()`,

- when quoting, use the subject of the quoted message as the new subject, instead of the
last subject in the chat
2021-03-07 16:57:12 +01:00
B. Petersen
8703da83f5 comment in more detail about DC_DESIRED_TEXT_LEN and use old limit that had worked for some while okayish 2021-03-04 23:26:02 +01:00
B. Petersen
4ae86b4e61 truncate long texts and make the whole text accessible via has_html()/get_html() 2021-03-04 23:26:02 +01:00
B. Petersen
e418d89c79 add failing test to test truncating 2021-03-04 23:26:02 +01:00
B. Petersen
6f4090fbf6 make clippy happy and avoid unneeded evaluation 2021-03-04 23:24:59 +01:00
B. Petersen
29cbbf9ce8 let get_html() return first instead of last text
usually, there is at most one text/html and one text/plain part.

multiple text/plain parts,
are known only by mailinglist software that wants to add a footer
but cannot alter the original content part -
maybe because it is html and things are hard to stitch
or maybe because the content part is cryptographically signed
and cannot be altered therefore.
2021-03-04 23:24:59 +01:00
B. Petersen
cd3c2a6c6c add test with multiple text parts
the test has multiple text parts because
the content text is signed and
the mailinglist software wants to add some text
(and cannot alter signed part without breaking the signature)
2021-03-04 23:24:59 +01:00
B. Petersen
27a7fae9c6 add a test checking get_fresh_msgs() in combination with mute_until 2021-03-03 12:02:58 +01:00
B. Petersen
1deaf87b24 fix mute-condition in get_fresh_msgs() 2021-03-03 12:02:58 +01:00
B. Petersen
75adbd2c8f add mute options to repl tool 2021-03-03 12:02:58 +01:00
Hocuri
165c57f0a4 Rust-tests: Don't panic while panicking 2021-03-03 11:58:21 +01:00
Hocuri
476e613377 Trash messages more thoroughly (#2273)
Esp. remove some information for newly-arriving messages
2021-03-02 12:04:53 +01:00
Hocuri
2a39dc06e9 Fix imex race condition, (#2255)
fix #2254: if the DB was closed without calling stop_io() and then an interrupt arrives (e.g. incoming message), the db was corrupted.

* Add result.log() for logging with less boilerplate code

* Bugfix: Resultify housekeeping() to make it abort if the db is closed instead of just deleting everything

* Require the UI to call dc_stop_io() before backup export

* Prepare a bit better for closed-db: Resultify get_uidvalidity and get_uid_next and let job::load_next() wait until the db is open

About the bug (before this PR):
if the DB was closed without calling stop_io() and then an interrupt arrives (e.g. incoming message):
- I don't know if it downloads the message, but of course at some point the process of receiving the message will fail
- In my test, DC is just in the process of moving a message when the imex starts, but then can't delete the job or update the msg server_uid
- Then, when job::load_next() is called, no job can be loaded. That's why it calls `load_housekeeping_job()`. As `load_housekeeping_job()` can't load the time of the last housekeeping, it assumes we never ran housekeeping and returns a new Housekeeping job, which is immediately executed.
- housekeeping can't find any blobs referenced in the db and therefore deletes almost all blobs.
2021-03-02 10:25:02 +01:00
link2xt
a698a8dd84 Update to Rust 1.50
Also run rustfmt, fix new clippy warnings.
2021-03-01 16:48:33 +03:00
B. Petersen
5c2d6c22a0 remove additional text parts if we think they belong to a mailinglist footer 2021-02-28 21:03:39 +01:00
B. Petersen
40d7f3ff71 add failing test for mailinglists with footer in an extra mimepart 2021-02-28 21:03:39 +01:00
link2xt
5535475cc9 Reduce required cmake version 2021-02-28 18:19:03 +03:00
B. Petersen
c3dd47beba add test for muting/unmuting wrt fresh messages 2021-02-27 21:08:53 +01:00
B. Petersen
f9c5ad817b resultify get_fresh_msgs(), this will make eg. test fail on bad sql 2021-02-27 21:08:53 +01:00
B. Petersen
81cd577bf0 exclude muted chats from notified-list 2021-02-27 21:08:53 +01:00
B. Petersen
f789de7044 get mailinglist name from From: if sender indicates a notification list 2021-02-27 00:03:30 +01:00
B. Petersen
b035a721ef add a failing test for mailing list names hidden in 'From' 2021-02-27 00:03:30 +01:00
link2xt
f8755b505e Fix clippy::unnecessary_wraps warnings
This lint is enabled by default in 1.50 toolchain.
2021-02-25 12:35:45 +03:00
B. Petersen
38169b2aad check From: address without creating/altering a contact-record if it is not SELF 2021-02-24 14:53:52 +01:00
link2xt
6dab25e5fb Add CMakeLists.txt
This allows to install the library system-wide or use it with build systems that support CMake, such as kdesrc-build.
2021-02-24 00:07:07 +03:00
B. Petersen
f973b75d6a unblock mailinglists via existing block-api
blocked mailinglists addresses are added to the contact table
before blocked contact list is created -
(this allows unblocking of blocked lists in previous testing releases,
however, more importantly, it keeps all blocking/unblocking code inside
contacts)

on unblocking such a contact,
the corresponding chat is unblocked as well.
2021-02-23 17:57:26 +01:00
B. Petersen
a9aeea0ffc add failing test trying to unblock mailinglist 2021-02-23 17:57:26 +01:00
B. Petersen
79e72418bb resultify Contact::get_all_blocked() 2021-02-23 17:57:26 +01:00
B. Petersen
01af3b7547 fix sorting of blocked contacts (successor of #2206) 2021-02-23 17:57:26 +01:00
bjoern
6d93d7af63 add decision- and blocking-functions to repl, cleanup (#2258)
* deprecate mostly unused dc_get_blocked_cnt() api

instead, the size returned by get_blocked_contacts() should be checked,
this is safer and allows easier adaption of blocking rules.

ui or python seems not to use dc_get_blocked_cnt(),
however, there is one test in node,
therefore, the function will continue working for now
(by just returning Contact::get_all_blocked().len() then)

* add decision api to repl tool

* add block/unblock api to repl tool

* unify usage of @deprecated doxygen command
2021-02-22 14:58:49 +01:00
B. Petersen
6792523fcd perfer X-Microsoft-Original-Message-ID also for delete-check and uid-sync 2021-02-19 00:25:46 +03:00
B. Petersen
0fa90a81e5 prefer X-Microsoft-Original-Message-ID, if set
outlooks SMTP-server change the Message-ID of messages
and put the original Message-ID to X-Microsoft-Original-Message-ID.

the changed Message-ID has some issues:

- outgoing messages with bcc_self enabled are shown twice
  as the self-copy got the changed Message-ID while the database uses
  the original one

- read receipts do not work as they refer to the changed message id

- in general, sender and recipient see different Message-IDs

the issues can be fixed by

(1) let all receivers use the original Message-ID

	this is what this pr is doing
    and this should fix all issues with delta-to-delta communication,
	including groups, group-images etc.
    there may be issues left in communication
	with other MUAs as they are using another Message-ID.

(2) ftr: updating the Message-ID in the database of the sender to the new one

	this requires bcc_self always enabled (which is not the case)
    and may also result easily in race conditions
    (Bob answers before Alice sees its self-sent message),
    however, has the advantage of better compatibility with other MUA.

if needed, the compatibility with other MUA could be improved by remembering
both Messages-IDs, maybe we could treat the modified as References or so,
however, i think, this could be part of another PR if we know better about
real, in the wild issues.
2021-02-19 00:25:46 +03:00
Floris Bruynooghe
2c613b3837 Fix typo 2021-02-18 21:39:27 +01:00
Floris Bruynooghe
036c9cd513 Wait for join-group to finish using ongoing channel
The ongoing mechanism actually has a channel to wait on instead of
sleeping in a loop.  Let's use it.
2021-02-18 21:39:27 +01:00
Floris Bruynooghe
24cb6aa9a4 Fix terminating the ongoing process in securejoin
When securejoin allocated an ongoing process it was never freed
again.  This fixes this and also adds test coverage for the right
ongoing behaviour.
2021-02-18 21:39:27 +01:00
holger krekel
e1fec6a460 try fixing upload docs step (#2249)
run the upload wheels activity on our build machine -- it requires python 3.6 or greater and CI's "machine" only provides python3.5
2021-02-17 23:08:40 +01:00
Floris Bruynooghe
04a978687f Errors are ok, apparently 2021-02-17 19:22:14 +01:00
Floris Bruynooghe
f464e43ba9 Handle errors properly
The other db-loading branches of this code handle an error, there's no
reason this bit should be skipping this.
2021-02-17 19:22:14 +01:00
Floris Bruynooghe
21e67c79a1 Remove obsolete ChatId::is_unset check
The chat::lookup_by_contact_id call is already resultified, the
database can not contain this since the auto-increment counter is
bumped to 7 by the time the database tables are created.
2021-02-17 19:22:14 +01:00
Hocuri
6132cc2a42 Update rust-email, test that no empty lines are produced (#2119)
* Add a failing test for #2118

* Update rust-email to fix #2118
2021-02-17 13:34:54 +01:00
link2xt
57a6f27c87 Remove dc_tools::listflags_has
It was not even doing what the documentation says: the implementation
worked correctly only for bitindex=1 and bitindex=2.
2021-02-16 15:33:53 +03:00
holger krekel
374ee7c1fe this test sometimes fails maybe due to the timeout and not enough randomness for RSA2048 2021-02-16 12:07:03 +01:00
link2xt
88a9a13795 tox.ini: pin sphinx version to fix CI 2021-02-16 11:19:20 +01:00
bjoern
046a2a8eae set correct name for mailchimp mailinglists (#2243)
* add test for mailchimp mailinglists

* pass MimeMessage to create_or_lookup_mailinglist() (as of the other create*() routines) to allow more flexible name processing; document the function

* get mailing list name for mailchimp from From:-header

* make clippy happy

* add comment to '.list-id.mcsv.net' suffix
2021-02-16 10:24:44 +01:00
B. Petersen
dab91574f2 adapt repl-tool to new mailinglist api 2021-02-15 23:11:02 +01:00
B. Petersen
f77beaf4fc remove parameter having always the same value from create_or_lookup_mailinglist() 2021-02-15 21:38:31 +01:00
B. Petersen
ffe68cadec update CHANGELOG.md 2021-02-15 21:38:31 +01:00
B. Petersen
d4e90c7fff add a test for handling mailinglists without ListId-header 2021-02-15 21:38:31 +01:00
B. Petersen
0c2b3e838e support mailinglists based on Sender: header 2021-02-15 21:38:31 +01:00
B. Petersen
e7c6667347 introduce MailinglistType for more exhaustive checking 2021-02-15 21:38:31 +01:00
B. Petersen
0b3fb9c0a3 prepend subject whenever we think sth. is a mailinglist, not only for a subset of mailing lists 2021-02-15 21:38:31 +01:00
link2xt
2865ced3c0 color: prevent rare overflow if some component is exactly 1.0 2021-02-15 20:04:59 +03:00
Simon Laux
309bea8e2a fix qr doc comment for vcard 2021-02-15 15:13:32 +03:00
B. Petersen
3ead349ccf add recent PRs between 1.50 and now to CHANGELOG 2021-02-15 11:07:39 +01:00
Hocuri
cda2fc4fea Assert that sentbox and mvbox folders stay configured during test_fetch_existing
Now I know why the tests failed before 48c58a7 (i.e. after c923670) but
not on master (i.e. before c923670):

because of a bug, scan_folders() set the configured_sentbox to None if
it was set before. If it was None before, it restored the correct value.

On master, there was another bug that led to two runs of
scan_folders() being started at the same time. Therefore, the first run
set configured_sentbox to None, the second one restored the correct
value.

c923670 fixed the latter bug, so that only one run of scan_folders() was
started. Therefore, configured_sentbox stayed None incorrectly and
test_fetch_existing() failed.

48c58a7 fixed the former bug.

This commit adds checks to test_fetch_existing(), so that it definitely
checks for the former bug.
2021-02-14 20:41:06 +01:00
Hocuri
95a0481b63 scan_folders() bugfix: Don't exclude watched folders from being set as sent/spam folder
This does fix a bug and it makes the tests pass, but I'm not sure why it
makes the tests pass; maybe there is a race condition that made the
tests fail and my commit just leads to another timing.
2021-02-14 20:41:06 +01:00
Hocuri
7d27c2bfea Remove confusing "Not scanning" log msg 2021-02-14 20:41:06 +01:00
Hocuri
c0023cb54d Only scan on the Inbox thread
This should prevent race conditions when multiple threads fetch at once.
2021-02-14 20:41:06 +01:00
Hocuri
e3f7b31501 Untested: If we can't find the newest backup file by opening the sql file, find it by name comparison 2021-02-14 19:48:15 +01:00
Hocuri
35b0f00a88 legacy (.bak) import: If backup files can't be opened, just try one that couldn't be opened
fix https://github.com/deltachat/deltachat-android/issues/1768
2021-02-14 19:48:15 +01:00
B. Petersen
b505d2666b if incoming Sender:-header ist set, renaming of contacts is prevented as for List-Id:-header 2021-02-14 18:45:31 +01:00
B. Petersen
1f59b5cd15 on sending, set Sender:-header if we have an overridden name 2021-02-14 18:45:31 +01:00
B. Petersen
5c684eb3c1 add dc_msg_set_override_sender_name() api
with mailinglists, we already receive and handle per-message-names,
this api allows this also eg. for bots based on the deltachat api.
2021-02-14 18:45:31 +01:00
link2xt
57841cdcc0 Remove &Context from Peerstate and MimeParser
Passing Context around explicitly removes the need for explicit lifetimes.
2021-02-14 14:19:55 +03:00
link2xt
7404e8c85f Return PartBuilder vector from MimeFactory.render_message()
This will allow MimeFactory.render() to put protected headers
into main_part and wrap it into single-part multipart/mixed if
protected headers are too large to put into the outermost IMF.
2021-02-14 14:19:55 +03:00
link2xt
a24b607640 refactor: less mut variables in MimeFactory::render() 2021-02-14 14:19:55 +03:00
link2xt
a88893f262 clippy: fix needless_borrow 2021-02-14 00:55:23 +03:00
holger krekel
c620d3e215 another try 2021-02-13 20:51:07 +01:00
121 changed files with 11207 additions and 6384 deletions

View File

@@ -7,116 +7,13 @@ executors:
doxygen:
docker:
- image: hrektts/doxygen
python:
docker:
- image: 3.7.7-stretch
restore-workspace: &restore-workspace
attach_workspace:
at: /mnt
restore-cache: &restore-cache
restore_cache:
keys:
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
- repo-source-{{ .Branch }}-{{ .Revision }}
commands:
test_target:
parameters:
target:
type: string
steps:
- *restore-workspace
- *restore-cache
- run:
name: Test (<< parameters.target >>)
command: TARGET=<< parameters.target >> ci_scripts/run-rust-test.sh
no_output_timeout: 15m
jobs:
cargo_fetch:
executor: default
steps:
- checkout
- restore_cache:
keys:
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
- run: rustup install $(cat rust-toolchain)
- 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 fetch
- run: rustc +stable --version
- run: rustc +$(cat rust-toolchain) --version
# make sure this git repo doesn't grow too big
- run: git gc
- persist_to_workspace:
root: /mnt
paths:
- crate
- save_cache:
key: cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
paths:
- "~/.cargo"
- "~/.rustup"
rustfmt:
executor: default
steps:
- *restore-workspace
- *restore-cache
- run:
name: Run cargo fmt
command: cargo fmt --all -- --check
test_macos:
macos:
xcode: "10.0.0"
working_directory: ~/crate
steps:
- run:
name: Configure environment variables
command: |
echo 'export PATH="${HOME}/.cargo/bin:${HOME}/.bin:${PATH}"' >> $BASH_ENV
echo 'export CIRCLE_ARTIFACTS="/tmp"' >> $BASH_ENV
- checkout
- run:
name: Install Rust
command: |
curl https://sh.rustup.rs -sSf | sh -s -- -y
- run: rustup install $(cat rust-toolchain)
- run: rustup default $(cat rust-toolchain)
- run: cargo fetch
- run:
name: Test
command: TARGET=x86_64-apple-darwin ci_scripts/run-rust-test.sh
test_x86_64-unknown-linux-gnu:
executor: default
steps:
- test_target:
target: "x86_64-unknown-linux-gnu"
test_i686-unknown-linux-gnu:
executor: default
steps:
- test_target:
target: "i686-unknown-linux-gnu"
test_aarch64-linux-android:
executor: default
steps:
- test_target:
target: "aarch64-linux-android"
build_doxygen:
executor: doxygen
steps:
- checkout
- run: bash ci_scripts/run-doxygen.sh
- run: bash scripts/run-doxygen.sh
- run: mkdir -p workspace/c-docs
- run: cp -av deltachat-ffi/{html,xml} workspace/c-docs/
- persist_to_workspace:
@@ -130,7 +27,7 @@ jobs:
- checkout
# the following commands on success produces
# workspace/{wheelhouse,py-docs} as artefact directories
- run: bash ci_scripts/remote_python_packaging.sh
- run: bash scripts/remote_python_packaging.sh
- persist_to_workspace:
root: workspace
paths:
@@ -138,62 +35,24 @@ jobs:
- py-docs
- wheelhouse
remote_tests_rust:
machine: true
steps:
- checkout
- run: ci_scripts/remote_tests_rust.sh
remote_tests_python:
machine: true
steps:
- checkout
- run: ci_scripts/remote_tests_python.sh
upload_docs_wheels:
machine: true
steps:
- checkout
- attach_workspace:
at: workspace
- run: pyenv versions
- run: pyenv global 3.6.0
- run: ls -laR workspace
- run: ci_scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse workspace/c-docs
clippy:
executor: default
steps:
- *restore-workspace
- *restore-cache
- run:
name: Run cargo clippy
command: cargo clippy
- run: scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse workspace/c-docs
workflows:
version: 2.1
test:
jobs:
# - cargo_fetch
- remote_tests_rust:
filters:
tags:
only: /.*/
- remote_tests_python:
filters:
tags:
only: /.*/
- remote_python_packaging:
filters:
branches:
only: master
tags:
only: /.*/
- upload_docs_wheels:
requires:
@@ -202,38 +61,8 @@ workflows:
filters:
branches:
only: master
tags:
only: /.*/
# - rustfmt:
# requires:
# - cargo_fetch
# - clippy:
# requires:
# - cargo_fetch
- build_doxygen:
filters:
branches:
only: master
tags:
only: /.*/
# Linux Desktop 64bit
# - test_x86_64-unknown-linux-gnu:
# requires:
# - cargo_fetch
# Linux Desktop 32bit
# - test_i686-unknown-linux-gnu:
# requires:
# - cargo_fetch
# Android 64bit
# - test_aarch64-linux-android:
# requires:
# - cargo_fetch
# Desktop Apple
# - test_macos:
# requires:
# - cargo_fetch

View File

@@ -14,11 +14,11 @@ jobs:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.49.0
toolchain: 1.50.0
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
@@ -29,10 +29,10 @@ jobs:
run_clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.49.0
toolchain: 1.50.0
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
@@ -72,7 +72,7 @@ jobs:
# macOS disabled due to random failures related to caching
#os: [ubuntu-latest, windows-latest, macOS-latest]
os: [ubuntu-latest, windows-latest]
rust: [1.49.0]
rust: [1.50.0]
experimental: [false]
# include:
# - os: ubuntu-latest

21
.github/workflows/remote_tests.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Remote tests
on: [push]
jobs:
remote_tests_python:
name: Remote Python tests
runs-on: ubuntu-latest
env:
CIRCLE_BRANCH: ${{ github.ref }}
CIRCLE_JOB: remote_tests_python
CIRCLE_BUILD_NUM: ${{ github.run_number }}
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
steps:
- uses: actions/checkout@v2
- run: mkdir -m 700 -p ~/.ssh
- run: touch ~/.ssh/id_ed25519
- run: chmod 600 ~/.ssh/id_ed25519
- run: 'echo "$SSH_KEY" | base64 -d > ~/.ssh/id_ed25519'
shell: bash
env:
SSH_KEY: ${{ secrets.SSH_KEY }}
- run: scripts/remote_tests_python.sh

3
.gitignore vendored
View File

@@ -1,5 +1,6 @@
/target
**/*.rs.bk
/build
# ignore vi temporaries
*~
@@ -25,3 +26,5 @@ deltachat-ffi/html
deltachat-ffi/xml
.rsynclist
coverage/

View File

@@ -1,5 +1,176 @@
# Changelog
## 1.53.0
- fix sqlx performance regression #2355 2356
- add a `ci_scripts/coverage.sh` #2333 #2334
- refactorings and tests #2348 #2349 #2350
- improve python bindings #2332 #2326
## 1.52.0
- database library changed from rusqlite to sqlx #2089 #2331 #2336 #2340
- add alias support: UIs should check for `dc_msg_get_override_sender_name()`
also in single-chats now and display divergent names and avatars #2297
- parse blockquote-tags for better quote detection #2313
- ignore unknown classical emails from spam folder #2311
- support "Mixed Up” encryption repairing #2321
- fix single chat search #2344
- fix nightly clippy and rustc errors #2341
- update dependencies #2350
- improve ci #2342
- improve python bindings #2332 #2326
## 1.51.0
- breaking change: You have to call `dc_stop_io()`/`dc_start_io()`
before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`:
fix race condition and db corruption
when a message was received during backup #2253
- save subject for messages: new api `dc_msg_get_subject()`,
when quoting, use the subject of the quoted message as the new subject,
instead of the last subject in the chat #2274 #2283
- new apis to get full or html message,
`dc_msg_has_html()` and `dc_get_msg_html()` #2125 #2151 #2264 #2279
- new chat type and apis for the new mailing list support,
`DC_CHAT_TYPE_MAILINGLIST`, `dc_msg_get_real_chat_id()`,
`dc_msg_get_override_sender_name()` #1964 #2181 #2185 #2195 #2211 #2210 #2240
#2241 #2243 #2258 #2259 #2261 #2267 #2270 #2272 #2290
- new api `dc_decide_on_contact_request()`,
deprecated `dc_create_chat_by_msg_id()` and `dc_marknoticed_contact()` #1964
- new flag `DC_GCM_INFO_ONLY` for api `dc_get_chat_msgs()` #2132
- new api `dc_get_chat_encrinfo()` #2186
- new api `dc_contact_get_status()`, returning the recent footer #2218 #2307
- improve contact name update rules,
add api `dc_contact_get_auth_name()` #2206 #2212 #2225
- new api for bots: `dc_msg_set_html()` #2153
- new api for bots: `dc_msg_set_override_sender_name()` #2231
- api removed: `dc_is_io_running()` #2139
- api removed: `dc_contact_get_first_name()` #2165 #2171
- improve compatibility with providers changing the Message-ID
(as Outlook.com) #2250 #2265
- correctly show emails that were sent to an alias and then bounced
- implement Consistent Color Generation (XEP-0392),
that results in contact colors be be changed #2228 #2229 #2239
- fetch recent existing messages
and create corresponding chats after configure #2106
- improve e-mail compatibility
by scanning all folders from time to time #2067 #2152 #2158 #2184 #2215 #2224
- better support videochat-services not supporting random rooms #2191
- export backups as .tar files #2023
- scale avatars based on media_quality, fix avatar rotation #2063
- compare ephemeral timer to parent message to deal with reordering better #2100
- better ephemeral system messages #2183
- read quotes out of html messages #2104
- prepend subject to messages with attachments, if needed #2111
- run housekeeping at least once a day #2114
- resolve MX domain only once per OAuth2 provider #2122
- configure provider based on MX record #2123 #2134
- make transient bad destination address error permanent
after n tries #2126 #2202
- enable strict TLS for known providers by default #2121
- improve and harden secure join #2154 #2161 #2251
- update `dc_get_info()` to return more information #2156
- prefer In-Reply-To/References
over group-id stored in Message-ID #2164 #2172 #2173
- apply gossiped encryption preference to new peerstates #2174
- fix: do not return quoted messages from the trash chat #2221
- fix: allow emojis for location markers #2177
- fix encoding of Chat-Group-Name-Changed messages that could even lead to
messages not being delivered #2141
- fix error when no temporary directory is available #1929
- fix marking read receipts as seen #2117
- fix read-notification for mixed-case addresses #2103
- fix decoding of attachment filenames #2080 #2094 #2102
- fix downloading ranges of message #2061
- fix parsing quoted encoded words in From: header #2193 #2204
- fix import/export race condition #2250
- fix: exclude muted chats from notified-list #2269 #2275
- fix: update uid_next if the server rewind it #2288
- fix: return error on fingerprint mismatch on qr-scan #2295
- fix ci #2217 #2226 #2244 #2245 #2249 #2277 #2286
- try harder on backup opening #2148
- trash messages more thoroughly #2273
- nicer logging #2284
- add CMakeLists.txt #2260
- switch to rust 1.50, update toolchains, deps #2150 #2155 #2165 #2107 #2262 #2271
- improve python bindings #2113 #2115 #2133 #2214
- improve documentation #2143 #2160 #2175 #2146
- refactorings #2110 #2136 #2135 #2168 #2178 #2189 #2190 #2198 #2197 #2201 #2196
#2200 #2230 #2262 #2203
- update provider-database #2299
## 1.50.0
- do not fetch emails in between inbox_watch disabled and enabled again #2087

28
CMakeLists.txt Normal file
View File

@@ -0,0 +1,28 @@
cmake_minimum_required(VERSION 3.16)
project(deltachat)
find_program(CARGO cargo)
add_custom_command(
OUTPUT
"target/release/libdeltachat.a"
"target/release/libdeltachat.so"
"target/release/pkgconfig/deltachat.pc"
COMMAND PREFIX=${CMAKE_INSTALL_PREFIX} ${CARGO} build --package deltachat_ffi --release
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
add_custom_target(
lib_deltachat
ALL
DEPENDS
"target/release/libdeltachat.a"
"target/release/libdeltachat.so"
"target/release/pkgconfig/deltachat.pc"
)
include(GNUInstallDirs)
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/libdeltachat.so" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)

905
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.51.0-alpha.0"
version = "1.53.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -12,83 +12,80 @@ debug = 0
lto = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
libc = "0.2.51"
pgp = { version = "0.7.0", default-features = false }
hex = "0.4.0"
sha-1 = "0.9.3"
sha2 = "0.9.0"
rand = "0.7.0"
smallvec = "1.0.0"
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
num-derive = "0.3.0"
num-traits = "0.2.6"
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
ansi_term = { version = "0.12.1", optional = true }
anyhow = "1.0.28"
async-imap = "0.4.0"
async-native-tls = { version = "0.3.3" }
async-std = { version = "~1.8.0", features = ["unstable"] }
base64 = "0.12"
charset = "0.1"
percent-encoding = "2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4.6"
indexmap = "1.3.0"
kamadak-exif = "0.5"
once_cell = "1.4.1"
regex = "1.1.6"
rusqlite = { version = "0.24", features = ["bundled"] }
r2d2_sqlite = "0.17.0"
r2d2 = "0.8.5"
strum = "0.19.0"
strum_macros = "0.19.0"
backtrace = "0.3.33"
byteorder = "1.3.1"
itertools = "0.9.0"
quick-xml = "0.18.1"
escaper = "0.1.0"
bitflags = "1.1.0"
sanitize-filename = "0.3.0"
stop-token = { version = "0.1.1", features = ["unstable"] }
mailparse = "0.13.0"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
native-tls = "0.2.3"
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
futures = "0.3.4"
thiserror = "1.0.14"
anyhow = "1.0.28"
async-trait = "0.1.31"
url = "2.1.1"
async-smtp = { git = "https://github.com/async-email/async-smtp", rev="2275fd8d13e39b2c58d6605c786ff06ff9e05708" }
async-std-resolver = "0.19.5"
async-std = { version = "~1.8.0", features = ["unstable"] }
async-tar = "0.3.0"
uuid = { version = "0.8", features = ["serde", "v4"] }
rust-hsluv = "0.1.4"
pretty_env_logger = { version = "0.4.0", optional = true }
log = {version = "0.4.8", optional = true }
rustyline = { version = "4.1.0", optional = true }
ansi_term = { version = "0.12.1", optional = true }
async-trait = "0.1.31"
backtrace = "0.3.33"
base64 = "0.13"
bitflags = "1.1.0"
byteorder = "1.3.1"
charset = "0.1"
chrono = "0.4.6"
dirs = { version = "3.0.1", optional=true }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
escaper = "0.1.0"
futures = "0.3.4"
hex = "0.4.0"
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
indexmap = "1.3.0"
itertools = "0.10.0"
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2.51"
log = {version = "0.4.8", optional = true }
mailparse = "0.13.0"
native-tls = "0.2.3"
num_cpus = "1.13.0"
num-derive = "0.3.0"
num-traits = "0.2.6"
once_cell = "1.4.1"
percent-encoding = "2.0"
pgp = { version = "0.7.0", default-features = false }
pretty_env_logger = { version = "0.4.0", optional = true }
quick-xml = "0.18.1"
rand = "0.7.0"
regex = "1.1.6"
rust-hsluv = "0.1.4"
rustyline = { version = "4.1.0", optional = true }
sanitize-filename = "0.3.0"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.9.3"
sha2 = "0.9.0"
smallvec = "1.0.0"
sqlx = { git = "https://github.com/deltachat/sqlx", branch = "master", features = ["runtime-async-std-native-tls", "sqlite"] }
# keep in sync with sqlx
libsqlite3-sys = { version = "0.22.0", default-features = false, features = [ "pkg-config", "vcpkg", "bundled" ] }
stop-token = { version = "0.1.1", features = ["unstable"] }
strum = "0.20.0"
strum_macros = "0.20.1"
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
thiserror = "1.0.14"
toml = "0.5.6"
url = "2.1.1"
uuid = { version = "0.8", features = ["serde", "v4"] }
[dev-dependencies]
tempfile = "3.0"
ansi_term = "0.12.0"
async-std = { version = "1.6.4", features = ["unstable", "attributes"] }
criterion = "0.3"
futures-lite = "1.7.0"
log = "0.4.11"
pretty_assertions = "0.6.1"
pretty_env_logger = "0.4.0"
proptest = "0.10"
async-std = { version = "1.6.4", features = ["unstable", "attributes"] }
futures-lite = "1.7.0"
criterion = "0.3"
ansi_term = "0.12.0"
tempfile = "3.0"
[workspace]
members = [
"deltachat-ffi",
"deltachat_derive",
]
[[example]]
@@ -106,10 +103,17 @@ required-features = ["repl"]
name = "create_account"
harness = false
[[bench]]
name = "contacts"
harness = false
[[bench]]
name = "search_msgs"
harness = false
[features]
default = []
internals = []
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored"]
nightly = ["pgp/nightly"]

View File

@@ -2,7 +2,9 @@
> Deltachat-core written in Rust
[![CircleCI build status][circle-shield]][circle] [![Appveyor build status][appveyor-shield]][appveyor]
[![Rust CI](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
[![Remote tests](https://github.com/deltachat/deltachat-core-rust/actions/workflows/remote_tests.yml/badge.svg)](https://github.com/deltachat/deltachat-core-rust/actions/workflows/remote_tests.yml)
[![CircleCI](https://circleci.com/gh/deltachat/deltachat-core-rust.svg?style=shield)](https://circleci.com/gh/deltachat/deltachat-core-rust/)
## Installing Rust and Cargo
@@ -17,7 +19,7 @@ $ curl https://sh.rustup.rs -sSf | sh
Compile and run Delta Chat Core command line utility, using `cargo`:
```
$ RUST_LOG=info cargo run --example repl --features repl -- ~/deltachat-db
$ RUST_LOG=repl=info cargo run --example repl --features repl -- ~/deltachat-db
```
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
@@ -95,7 +97,7 @@ $ cargo build -p deltachat_ffi --release
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
- `RUST_LOG=info,async_imap=trace,async_smtp=trace`: enable IMAP and
- `RUST_LOG=repl=info,async_imap=trace,async_smtp=trace`: enable IMAP and
SMTP tracing in addition to info messages.
### Expensive tests
@@ -111,11 +113,6 @@ $ cargo test -- --ignored
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
- `nightly`: Enable nightly only performance and security related features.
[circle-shield]: https://img.shields.io/circleci/project/github/deltachat/deltachat-core-rust/master.svg?style=flat-square
[circle]: https://circleci.com/gh/deltachat/deltachat-core-rust/
[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:

39
benches/contacts.rs Normal file
View File

@@ -0,0 +1,39 @@
use async_std::task::block_on;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::contact::Contact;
use deltachat::context::Context;
use tempfile::tempdir;
async fn address_book_benchmark(n: u32, read_count: u32) {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let id = 100;
let context = Context::new("FakeOS".into(), dbfile.into(), id)
.await
.unwrap();
let book = (0..n)
.map(|i| format!("Name {}\naddr{}@example.org\n", i, i))
.collect::<Vec<String>>()
.join("");
Contact::add_address_book(&context, book).await.unwrap();
let query: Option<&str> = None;
for _ in 0..read_count {
Contact::get_all(&context, 0, query).await.unwrap();
}
}
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("create 500 contacts", |b| {
b.iter(|| block_on(async { address_book_benchmark(black_box(500), black_box(0)).await }))
});
c.bench_function("create 100 contacts and read it 1000 times", |b| {
b.iter(|| block_on(async { address_book_benchmark(black_box(100), black_box(1000)).await }))
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

29
benches/search_msgs.rs Normal file
View File

@@ -0,0 +1,29 @@
use async_std::task::block_on;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::context::Context;
use std::path::Path;
async fn search_benchmark(path: impl AsRef<Path>) {
let dbfile = path.as_ref();
let id = 100;
let context = Context::new("FakeOS".into(), dbfile.into(), id)
.await
.unwrap();
for _ in 0..10u32 {
context.search_msgs(None, "hello").await.unwrap();
}
}
fn criterion_benchmark(c: &mut Criterion) {
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
// messages, such as your primary account.
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
c.bench_function("search hello", |b| {
b.iter(|| block_on(async { search_benchmark(black_box(&path)).await }))
});
}
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -1,57 +0,0 @@
#!/bin/bash
if [ -z "$DEVPI_LOGIN" ] ; then
echo "required: password for 'dc' user on https://m.devpi/net/dc index"
exit 0
fi
set -xe
PYDOCDIR=${1:?directory with python docs}
WHEELHOUSEDIR=${2:?directory with pre-built wheels}
DOXYDOCDIR=${3:?directory where doxygen docs to be found}
export BRANCH=${CIRCLE_BRANCH:?specify branch for uploading purposes}
# 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}
echo -----------------------
echo upload wheels
echo -----------------------
# Bundle external shared libraries into the wheels
pushd $WHEELHOUSEDIR
pip3 install -U pip setuptools
pip3 install devpi-client
devpi use https://m.devpi.net
devpi login dc --password $DEVPI_LOGIN
N_BRANCH=${BRANCH//[\/]}
devpi use dc/$N_BRANCH || {
devpi index -c $N_BRANCH
devpi use dc/$N_BRANCH
}
devpi index $N_BRANCH bases=/root/pypi
devpi upload deltachat*
popd
# remove devpi non-master dc indices if thy are too old
python ci_scripts/cleanup_devpi_indices.py

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -ex
cd deltachat-ffi
PROJECT_NUMBER=$(git log -1 --format "%h (%cd)") doxygen

View File

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

View File

@@ -0,0 +1,38 @@
<doxygenlayout version="1.0">
<!-- Generated by doxygen 1.8.20 -->
<!-- Navigation index tabs for HTML output -->
<navindex>
<tab type="mainpage" visible="yes" title=""/>
<tab type="classes" visible="yes" title="">
<tab type="classlist" visible="no" title="" intro=""/>
<tab type="classindex" visible="no" title=""/>
<tab type="hierarchy" visible="no" title="" intro=""/>
<tab type="classmembers" visible="no" title="" intro=""/>
</tab>
<tab type="modules" visible="yes" title="Constants" intro="Here is a list of constants:"/>
<tab type="pages" visible="yes" title="" intro=""/>
<tab type="namespaces" visible="yes" title="">
<tab type="namespacelist" visible="yes" title="" intro=""/>
<tab type="namespacemembers" visible="yes" title="" intro=""/>
</tab>
<tab type="interfaces" visible="yes" title="">
<tab type="interfacelist" visible="yes" title="" intro=""/>
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
<tab type="interfacehierarchy" visible="yes" title="" intro=""/>
</tab>
<tab type="structs" visible="yes" title="">
<tab type="structlist" visible="yes" title="" intro=""/>
<tab type="structindex" visible="$ALPHABETICAL_INDEX" title=""/>
</tab>
<tab type="exceptions" visible="yes" title="">
<tab type="exceptionlist" visible="yes" title="" intro=""/>
<tab type="exceptionindex" visible="$ALPHABETICAL_INDEX" title=""/>
<tab type="exceptionhierarchy" visible="yes" title="" intro=""/>
</tab>
<tab type="files" visible="yes" title="">
<tab type="filelist" visible="yes" title="" intro=""/>
<tab type="globals" visible="yes" title="" intro=""/>
</tab>
<tab type="examples" visible="yes" title="" intro=""/>
</navindex>
</doxygenlayout>

View File

@@ -672,8 +672,6 @@ dc_chatlist_t* dc_get_chatlist (dc_context_t* context, int flags,
// handle chats
/**
* DEPRECATED Use dc_decide_on_contact_request().
*
* Create a normal chat or a group chat by a messages ID that comes typically
* from the deaddrop, DC_CHAT_ID_DEADDROP (1).
*
@@ -693,7 +691,7 @@ dc_chatlist_t* dc_get_chatlist (dc_context_t* context, int flags,
* same group may be shown or not - so, all in all, it is fine to show the
* contact name only.
*
* @deprecated Use dc_decide_on_contact_request() instead
* @deprecated Deprecated 2021-02-07, use dc_decide_on_contact_request() instead
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param msg_id The message ID to create the chat for.
@@ -1041,8 +1039,12 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch
/**
* Get the number of _fresh_ messages in a chat. Typically used to implement
* a badge with a number in the chatlist.
* Get the number of _fresh_ messages in a chat.
* Typically used to implement a badge with a number in the chatlist.
*
* If the specified chat is muted,
* the UI should show the badge counter "less obtrusive",
* eg. using "gray" instead of "red" color.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
@@ -1069,11 +1071,20 @@ int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t ch
*/
int dc_estimate_deletion_cnt (dc_context_t* context, int from_server, int64_t seconds);
/**
* Returns the message IDs of all _fresh_ messages of any chat.
* Typically used for implementing notification summaries.
* Typically used for implementing notification summaries
* or badge counters eg. on the app-icon.
* The list is already sorted and starts with the most recent fresh message.
*
* Messages belonging to muted chats are not returned,
* as they should not be notified
* and also a badge counters should not include messages of muted chats.
*
* To get the number of fresh messages for a single chat, muted or not,
* use dc_get_fresh_msg_cnt().
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
@@ -1261,6 +1272,12 @@ uint32_t dc_get_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id);
* search results may just hilite the corresponding messages and present a
* prev/next button.
*
* For global search, result is limited to 1000 messages,
* this allows incremental search done fast.
* So, when getting exactly 1000 results, the result may be truncated;
* the UIs may display sth. as "1000+ messages found" in this case.
* Chat search (if a chat_id is set) is not limited.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id ID of the chat to search messages in.
@@ -1428,13 +1445,21 @@ int dc_set_chat_profile_image (dc_context_t* context, uint32_t ch
/**
* Set mute duration of a chat.
*
* The UI can then call dc_chat_is_muted() when receiving a new message to decide whether it should trigger an notification.
* The UI can then call dc_chat_is_muted() when receiving a new message
* to decide whether it should trigger an notification.
*
* Muted chats should not sound or vibrate
* and should not show a visual notification in the system area.
* Moreover, muted chats should be excluded from global badge counter
* (dc_get_fresh_msgs() skips muted chats therefore)
* and the in-app, per-chat badge counter should use a less obtrusive color.
*
* 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 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 object.
* @return 1=success, 0=error
*/
@@ -1479,6 +1504,8 @@ char* dc_get_msg_info (dc_context_t* context, uint32_t ms
* this removes the need for the UI
* to deal with different formatting options of PLAIN-parts.
*
* As the title of the full-message-view, you can use the subject (see dc_msg_get_subject()).
*
* **Note:** The returned HTML-code may contain scripts,
* external images that may be misused as hidden read-receipts and so on.
* Taking care of these parts
@@ -1547,8 +1574,6 @@ void dc_forward_msgs (dc_context_t* context, const uint3
/**
* DEPRECATED
*
* Mark all messages sent by the given contact as _noticed_.
* This function is typically used to ignore a user in the deaddrop temporarily ("Not now" button).
*
@@ -1557,7 +1582,7 @@ void dc_forward_msgs (dc_context_t* context, const uint3
*
* See also dc_marknoticed_chat() and dc_markseen_msgs()
*
* @deprecated Use dc_decide_on_contact_request() if the user just hit "Not now" on a button in the deaddrop,
* @deprecated Deprecated 2021-02-07, use dc_decide_on_contact_request() if the user just hit "Not now" on a button in the deaddrop,
* dc_marknoticed_chat() if the user has entered a chat
* and dc_markseen_msgs() if the user actually _saw_ a message.
* @memberof dc_context_t
@@ -1757,6 +1782,7 @@ dc_array_t* dc_get_contacts (dc_context_t* context, uint32_t fl
/**
* Get the number of blocked contacts.
*
* @deprecated Deprecated 2021-02-22, use dc_array_get_cnt() on dc_get_blocked_contacts() instead.
* @memberof dc_context_t
* @param context The context object.
* @return The number of blocked contacts.
@@ -1840,6 +1866,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
/**
* Import/export things.
* During backup import/export IO must not be started, if needed stop IO using dc_stop_io() first.
* What to do is defined by the _what_ parameter which may be one of the following:
*
* - **DC_IMEX_EXPORT_BACKUP** (11) - Export a backup to the directory given as `param1`.
@@ -3325,6 +3352,25 @@ int64_t dc_msg_get_sort_timestamp (const dc_msg_t* msg);
char* dc_msg_get_text (const dc_msg_t* msg);
/**
* Get the subject of the email.
* If there is no subject associated with the message, an empty string is returned.
* NULL is never returned.
*
* You usually don't need this; if the core thinks that the subject might contain important
* information, it automatically prepends it to the message text.
*
* This function was introduced so that you can use the subject as the title for the
* full-message-view (see dc_get_msg_html()).
*
* For outgoing messages, the subject is not stored and an empty string is returned.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return The subject. The result must be released using dc_str_unref(). Never returns NULL.
*/
char* dc_msg_get_subject (const dc_msg_t* msg);
/**
* Find out full path, file name and extension of the file associated with a
* message.
@@ -3507,17 +3553,31 @@ char* dc_msg_get_summarytext (const dc_msg_t* msg, int approx_c
/**
* Get the name that should be shown over the message (in a group chat) instead of the contact
* display name.
* display name, or NULL.
*
* If this returns non-NULL, put a `~` before the override-sender-name and show the
* override-sender-name and the sender's avatar even in 1:1 chats.
*
* In mailing lists, sender display name and sender address do not always belong together.
* In this case, this function gives you the name that should actually be shown over the message.
*
* Also, sometimes, we need to indicate a different sender in 1:1 chats:
* Suppose that our user writes an email to support@delta.chat, which forwards to
* Bob <bob@delta.chat>, and Bob replies.
*
* Then, Bob's reply is shown in our 1:1 chat with support@delta.chat and the override-sender-name is
* set to `Bob`. The UI should show the sender name as `~Bob` and show the avatar, just
* as in group messages. If the user then taps on the avatar, they can see that this message
* comes from bob@delta.chat.
*
* You should show a `~` before the override-sender-name in chats, so that the user can
* see that this isn't the sender's actual name.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return the name to show over this message or NULL.
* If this returns NULL, call `dc_contact_get_display_name()`.
* The returned string must be released using dc_str_unref().
* Returns an empty string on errors, never returns NULL.
*/
char* dc_msg_get_override_sender_name(const dc_msg_t* msg);
@@ -3816,6 +3876,21 @@ void dc_msg_set_text (dc_msg_t* msg, const char* text);
void dc_msg_set_html (dc_msg_t* msg, const char* html);
/**
* Set different sender name for a message.
* This overrides the name set by the dc_set_config()-option `displayname`.
*
* Usually, this function is not needed
* when implementing pure messaging functions.
* However, it might be useful for bots eg. building bridges to other networks.
*
* @memberof dc_msg_t
* @param msg The message object.
* @param name The name to send along with the message.
*/
void dc_msg_set_override_sender_name(dc_msg_t* msg, const char* name);
/**
* Set the file associated with a message object.
* This does not alter any information in the database
@@ -5270,7 +5345,7 @@ void dc_event_unref(dc_event_t* event);
/// Used to build the string returned by dc_get_contact_encrinfo().
#define DC_STR_E2E_AVAILABLE 25
/// DEPRECATED 2021-02-07
/// @deprecated Deprecated 2021-02-07, this string is no longer needed.
#define DC_STR_ENCR_TRANSP 27
/// "No encryption."
@@ -5460,9 +5535,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages.
#define DC_STR_EPHEMERAL_WEEK 80
/// DEPRECATED
///
/// DC_STR_EPHEMERAL_WEEKS is used instead.
/// @deprecated Deprecated 2021-01-30, DC_STR_EPHEMERAL_WEEKS is used instead.
#define DC_STR_EPHEMERAL_FOUR_WEEKS 81
/// "Video chat invitation"

View File

@@ -24,7 +24,6 @@ use std::time::{Duration, SystemTime};
use async_std::task::{block_on, spawn};
use num_traits::{FromPrimitive, ToPrimitive};
use deltachat::accounts::Accounts;
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, Origin};
@@ -34,6 +33,7 @@ use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::stock_str::StockMessage;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
mod dc_array;
@@ -156,7 +156,14 @@ pub unsafe extern "C" fn dc_get_config(
}
let ctx = &*context;
match config::Config::from_str(&to_string_lossy(key)) {
Ok(key) => block_on(async move { ctx.get_config(key).await.unwrap_or_default().strdup() }),
Ok(key) => block_on(async move {
ctx.get_config(key)
.await
.log_err(ctx, "Can't get config")
.unwrap_or_default()
.unwrap_or_default()
.strdup()
}),
Err(_) => {
warn!(ctx, "dc_get_config(): invalid key");
"".strdup()
@@ -225,8 +232,13 @@ pub unsafe extern "C" fn dc_get_info(context: *const dc_context_t) -> *mut libc:
}
let ctx = &*context;
block_on(async move {
let info = ctx.get_info().await;
render_info(info).unwrap_or_default().strdup()
match ctx.get_info().await {
Ok(info) => render_info(info).unwrap_or_default().strdup(),
Err(err) => {
warn!(ctx, "failed to get info: {}", err);
"".strdup()
}
}
})
}
@@ -283,7 +295,12 @@ pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c
}
let ctx = &*context;
block_on(async move { ctx.is_configured().await as libc::c_int })
block_on(async move {
ctx.is_configured()
.await
.log_err(ctx, "failed to get configured state")
.unwrap_or_default() as libc::c_int
})
}
#[no_mangle]
@@ -768,7 +785,12 @@ pub unsafe extern "C" fn dc_set_draft(
Some(&mut ffi_msg.message)
};
block_on(ChatId::new(chat_id).set_draft(&ctx, msg))
block_on(async move {
ChatId::new(chat_id)
.set_draft(&ctx, msg)
.await
.unwrap_or_log_default(ctx, "failed to set draft");
});
}
#[no_mangle]
@@ -863,6 +885,7 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
Box::into_raw(Box::new(
chat::get_chat_msgs(&ctx, ChatId::new(chat_id), flags, marker_flag)
.await
.unwrap_or_log_default(ctx, "failed to get chat msgs")
.into(),
))
})
@@ -876,7 +899,12 @@ pub unsafe extern "C" fn dc_get_msg_cnt(context: *mut dc_context_t, chat_id: u32
}
let ctx = &*context;
block_on(async move { ChatId::new(chat_id).get_msg_cnt(&ctx).await as libc::c_int })
block_on(async move {
ChatId::new(chat_id)
.get_msg_cnt(&ctx)
.await
.unwrap_or_log_default(ctx, "failed to get msg count") as libc::c_int
})
}
#[no_mangle]
@@ -890,7 +918,12 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
}
let ctx = &*context;
block_on(async move { ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await as libc::c_int })
block_on(async move {
ChatId::new(chat_id)
.get_fresh_msg_cnt(&ctx)
.await
.unwrap_or_log_default(ctx, "failed to get fresh msg cnt") as libc::c_int
})
}
#[no_mangle]
@@ -925,6 +958,8 @@ pub unsafe extern "C" fn dc_get_fresh_msgs(
let arr = dc_array_t::from(
ctx.get_fresh_msgs()
.await
.log_err(ctx, "Failed to get fresh messages")
.unwrap_or_default()
.iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(),
@@ -986,6 +1021,7 @@ pub unsafe extern "C" fn dc_get_chat_media(
or_msg_type3,
)
.await
.unwrap_or_log_default(ctx, "Failed get_chat_media")
.into(),
))
})
@@ -1027,7 +1063,7 @@ pub unsafe extern "C" fn dc_get_next_media(
or_msg_type3,
)
.await
.map(|msg_id| msg_id.to_u32())
.map(|msg_id| msg_id.map(|id| id.to_u32()).unwrap_or_default())
.unwrap_or(0)
})
}
@@ -1120,7 +1156,11 @@ pub unsafe extern "C" fn dc_get_chat_contacts(
let ctx = &*context;
block_on(async move {
let arr = dc_array_t::from(chat::get_chat_contacts(&ctx, ChatId::new(chat_id)).await);
let arr = dc_array_t::from(
chat::get_chat_contacts(&ctx, ChatId::new(chat_id))
.await
.unwrap_or_log_default(ctx, "Failed get_chat_contacts"),
);
Box::into_raw(Box::new(arr))
})
}
@@ -1146,6 +1186,7 @@ pub unsafe extern "C" fn dc_search_msgs(
let arr = dc_array_t::from(
ctx.search_msgs(chat_id, to_string_lossy(query))
.await
.unwrap_or_log_default(ctx, "Failed search_msgs")
.iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>(),
@@ -1259,7 +1300,8 @@ pub unsafe extern "C" fn dc_set_chat_name(
chat_id: u32,
name: *const libc::c_char,
) -> libc::c_int {
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL as u32 || name.is_null() {
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() || name.is_null()
{
eprintln!("ignoring careless call to dc_set_chat_name()");
return 0;
}
@@ -1279,7 +1321,7 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
chat_id: u32,
image: *const libc::c_char,
) -> libc::c_int {
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL as u32 {
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() {
eprintln!("ignoring careless call to dc_set_chat_profile_image()");
return 0;
}
@@ -1404,7 +1446,12 @@ pub unsafe extern "C" fn dc_get_msg_info(
}
let ctx = &*context;
block_on(message::get_msg_info(&ctx, MsgId::new(msg_id))).strdup()
block_on(async move {
message::get_msg_info(&ctx, MsgId::new(msg_id))
.await
.unwrap_or_log_default(ctx, "failed to get msg id")
.strdup()
})
}
#[no_mangle]
@@ -1418,7 +1465,9 @@ pub unsafe extern "C" fn dc_get_msg_html(
}
let ctx = &*context;
block_on(MsgId::new(msg_id).get_html(&ctx)).strdup()
block_on(MsgId::new(msg_id).get_html(&ctx))
.unwrap_or_log_default(ctx, "Failed get_msg_html")
.strdup()
}
#[no_mangle]
@@ -1433,10 +1482,13 @@ pub unsafe extern "C" fn dc_get_mime_headers(
let ctx = &*context;
block_on(async move {
message::get_mime_headers(&ctx, MsgId::new(msg_id))
let mime = message::get_mime_headers(&ctx, MsgId::new(msg_id))
.await
.map(|s| s.strdup())
.unwrap_or_else(ptr::null_mut)
.unwrap_or_log_default(ctx, "failed to get mime headers");
if mime.is_empty() {
return ptr::null_mut();
}
mime.strdup()
})
}
@@ -1453,7 +1505,8 @@ pub unsafe extern "C" fn dc_delete_msgs(
let ctx = &*context;
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
block_on(message::delete_msgs(&ctx, &msg_ids))
block_on(message::delete_msgs(&ctx, &msg_ids));
info!(&ctx, "verbose (issue 2335): ffi called dc_delete_msgs()");
}
#[no_mangle]
@@ -1466,7 +1519,7 @@ pub unsafe extern "C" fn dc_forward_msgs(
if context.is_null()
|| msg_ids.is_null()
|| msg_cnt <= 0
|| chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL as u32
|| chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32()
{
eprintln!("ignoring careless call to dc_forward_msgs()");
return;
@@ -1562,14 +1615,12 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr(
}
let ctx = &*context;
block_on(Contact::lookup_id_by_addr(
&ctx,
to_string_lossy(addr),
Origin::IncomingReplyTo,
))
.ok()
.flatten()
.unwrap_or_default()
block_on(async move {
Contact::lookup_id_by_addr(&ctx, to_string_lossy(addr), Origin::IncomingReplyTo)
.await
.unwrap_or_log_default(ctx, "failed to lookup id")
.unwrap_or(0)
})
}
#[no_mangle]
@@ -1640,7 +1691,12 @@ pub unsafe extern "C" fn dc_get_blocked_cnt(context: *mut dc_context_t) -> libc:
}
let ctx = &*context;
block_on(Contact::get_blocked_cnt(&ctx)) as libc::c_int
block_on(async move {
Contact::get_all_blocked(&ctx)
.await
.unwrap_or_log_default(ctx, "failed to get blocked count")
.len() as libc::c_int
})
}
#[no_mangle]
@@ -1655,7 +1711,10 @@ pub unsafe extern "C" fn dc_get_blocked_contacts(
block_on(async move {
Box::into_raw(Box::new(dc_array_t::from(
Contact::get_all_blocked(&ctx).await,
Contact::get_all_blocked(&ctx)
.await
.log_err(&ctx, "Can't get blocked contacts")
.unwrap_or_default(),
)))
})
}
@@ -1920,7 +1979,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
chat_id: u32,
seconds: libc::c_int,
) {
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL as u32 || seconds < 0 {
if context.is_null() || chat_id <= constants::DC_CHAT_ID_LAST_SPECIAL.to_u32() || seconds < 0 {
eprintln!("ignoring careless call to dc_send_locations_to_chat()");
return;
}
@@ -2000,7 +2059,8 @@ pub unsafe extern "C" fn dc_get_locations(
timestamp_begin as i64,
timestamp_end as i64,
)
.await;
.await
.unwrap_or_log_default(ctx, "Failed get_locations");
Box::into_raw(Box::new(dc_array_t::from(res)))
})
}
@@ -2381,8 +2441,12 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut
block_on(async move {
match ffi_chat.chat.get_profile_image(&ctx).await {
Some(p) => p.to_string_lossy().strdup(),
None => ptr::null_mut(),
Ok(Some(p)) => p.to_string_lossy().strdup(),
Ok(None) => ptr::null_mut(),
Err(err) => {
error!(ctx, "failed to get profile image: {:?}", err);
ptr::null_mut()
}
}
})
}
@@ -2396,7 +2460,7 @@ pub unsafe extern "C" fn dc_chat_get_color(chat: *mut dc_chat_t) -> u32 {
let ffi_chat = &*chat;
let ctx = &*ffi_chat.context;
block_on(ffi_chat.chat.get_color(&ctx))
block_on(ffi_chat.chat.get_color(&ctx)).unwrap_or_log_default(ctx, "Failed get_color")
}
#[no_mangle]
@@ -2687,6 +2751,16 @@ pub unsafe extern "C" fn dc_msg_get_text(msg: *mut dc_msg_t) -> *mut libc::c_cha
ffi_msg.message.get_text().unwrap_or_default().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_subject(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_subject()");
return "".strdup();
}
let ffi_msg = &*msg;
ffi_msg.message.get_subject().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
@@ -3028,6 +3102,21 @@ pub unsafe extern "C" fn dc_msg_set_html(msg: *mut dc_msg_t, html: *const libc::
ffi_msg.message.set_html(to_opt_string_lossy(html))
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_set_override_sender_name(
msg: *mut dc_msg_t,
name: *const libc::c_char,
) {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_set_override_sender_name()");
return;
}
let ffi_msg = &mut *msg;
ffi_msg
.message
.set_override_sender_name(to_opt_string_lossy(name))
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_set_file(
msg: *mut dc_msg_t,
@@ -3282,6 +3371,7 @@ pub unsafe extern "C" fn dc_contact_get_profile_image(
.contact
.get_profile_image(&ctx)
.await
.unwrap_or_log_default(ctx, "failed to get profile image")
.map(|p| p.to_string_lossy().strdup())
.unwrap_or_else(std::ptr::null_mut)
})
@@ -3415,15 +3505,11 @@ pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
}
trait ResultExt<T, E> {
/// Like `log_err()`, but:
/// - returns the default value instead of an Err value.
/// - emits an error instead of a warning for an [Err] result. This means
/// that the error will be shown to the user in a small pop-up.
fn unwrap_or_log_default(self, context: &context::Context, message: &str) -> T;
/// Log a warning to a [ContextWrapper] for an [Err] result.
///
/// Does nothing for an [Ok].
///
/// You can do this as soon as the wrapper exists, it does not
/// have to be open (which is required for the `warn!()` macro).
fn log_err(self, wrapper: &Context, message: &str) -> Result<T, E>;
}
impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
@@ -3436,14 +3522,6 @@ impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
}
}
}
fn log_err(self, ctx: &Context, message: &str) -> Result<T, E> {
self.map_err(|err| {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
warn!(ctx, "{}: {:#}", message, err);
err
})
}
}
trait ResultNullableExt<T> {
@@ -3525,6 +3603,7 @@ pub unsafe extern "C" fn dc_provider_get_status(provider: *const dc_provider_t)
}
#[no_mangle]
#[allow(clippy::needless_return)]
pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
if provider.is_null() {
eprintln!("ignoring careless call to dc_provider_unref()");

View File

@@ -241,7 +241,7 @@ pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
/// [OsStrExt::to_c_string] requires valid Unicode on Windows, this
/// requires that the pointer contains valid UTF-8 on Windows.
///
/// Because this returns a reference the [Path] silce can not outlive
/// Because this returns a reference the [Path] slice can not outlive
/// the original pointer.
///
/// [Path]: std::path::Path

View File

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

View File

@@ -1,43 +0,0 @@
#![recursion_limit = "128"]
extern crate proc_macro;
use crate::proc_macro::TokenStream;
use quote::quote;
// For now, assume (not check) that these macroses are applied to enum without
// data. If this assumption is violated, compiler error will point to
// generated code, which is not very user-friendly.
#[proc_macro_derive(ToSql)]
pub fn to_sql_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
let gen = quote! {
impl rusqlite::types::ToSql for #name {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let num = *self as i64;
let value = rusqlite::types::Value::Integer(num);
let output = rusqlite::types::ToSqlOutput::Owned(value);
std::result::Result::Ok(output)
}
}
};
gen.into()
}
#[proc_macro_derive(FromSql)]
pub fn from_sql_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
let gen = quote! {
impl rusqlite::types::FromSql for #name {
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
let inner = rusqlite::types::FromSql::column_result(col)?;
Ok(num_traits::FromPrimitive::from_i64(inner).unwrap_or_default())
}
}
};
gen.into()
}

View File

@@ -4,7 +4,9 @@ use std::str::FromStr;
use anyhow::{bail, ensure, Error};
use async_std::path::Path;
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, ProtectionStatus};
use deltachat::chat::{
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
};
use deltachat::chatlist::*;
use deltachat::constants::*;
use deltachat::contact::*;
@@ -13,14 +15,16 @@ use deltachat::dc_receive_imf::*;
use deltachat::dc_tools::*;
use deltachat::imex::*;
use deltachat::location;
use deltachat::log::LogExt;
use deltachat::lot::LotState;
use deltachat::message::{self, Message, MessageState, MsgId};
use deltachat::message::{self, ContactRequestDecision, Message, MessageState, MsgId};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
use deltachat::EventType;
use deltachat::{config, provider};
use std::fs;
use std::time::{Duration, SystemTime};
/// Reset database tables.
/// Argument is a bitmask, executing single or multiple actions in one call.
@@ -30,7 +34,7 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 1 {
context
.sql()
.execute("DELETE FROM jobs;", paramsv![])
.execute(sqlx::query("DELETE FROM jobs;"))
.await
.unwrap();
println!("(1) Jobs reset.");
@@ -38,7 +42,7 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 2 {
context
.sql()
.execute("DELETE FROM acpeerstates;", paramsv![])
.execute(sqlx::query("DELETE FROM acpeerstates;"))
.await
.unwrap();
println!("(2) Peerstates reset.");
@@ -46,7 +50,7 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 4 {
context
.sql()
.execute("DELETE FROM keypairs;", paramsv![])
.execute(sqlx::query("DELETE FROM keypairs;"))
.await
.unwrap();
println!("(4) Private keypairs reset.");
@@ -54,35 +58,34 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 8 {
context
.sql()
.execute("DELETE FROM contacts WHERE id>9;", paramsv![])
.execute(sqlx::query("DELETE FROM contacts WHERE id>9;"))
.await
.unwrap();
context
.sql()
.execute("DELETE FROM chats WHERE id>9;", paramsv![])
.execute(sqlx::query("DELETE FROM chats WHERE id>9;"))
.await
.unwrap();
context
.sql()
.execute("DELETE FROM chats_contacts;", paramsv![])
.execute(sqlx::query("DELETE FROM chats_contacts;"))
.await
.unwrap();
context
.sql()
.execute("DELETE FROM msgs WHERE id>9;", paramsv![])
.execute(sqlx::query("DELETE FROM msgs WHERE id>9;"))
.await
.unwrap();
context
.sql()
.execute(
.execute(sqlx::query(
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
paramsv![],
)
))
.await
.unwrap();
context
.sql()
.execute("DELETE FROM leftgrps;", paramsv![])
.execute(sqlx::query("DELETE FROM leftgrps;"))
.await
.unwrap();
println!("(8) Rest but server config reset.");
@@ -116,11 +119,11 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
real_spec = spec.to_string();
context
.sql()
.set_raw_config(context, "import_spec", Some(&real_spec))
.set_raw_config("import_spec", Some(&real_spec))
.await
.unwrap();
} else {
let rs = context.sql().get_raw_config(context, "import_spec").await;
let rs = context.sql().get_raw_config("import_spec").await.unwrap();
if rs.is_none() {
error!(context, "Import: No file or folder given.");
return false;
@@ -171,8 +174,11 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
let contact = Contact::get_by_id(context, msg.get_from_id())
.await
.expect("invalid contact");
let contact_name = contact.get_name();
let contact_name = if let Some(name) = msg.get_override_sender_name() {
format!("~{}", name)
} else {
contact.get_display_name().to_string()
};
let contact_id = contact.get_id();
let statestr = match msg.get_state() {
@@ -194,7 +200,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
contact_id,
msgtext.unwrap_or_default(),
if msg.has_html() { "[HAS-HTML]" } else { "" },
if msg.get_from_id() == 1 as libc::c_uint {
if msg.get_from_id() == 1 {
""
} else if msg.get_state() == MessageState::InSeen {
"[SEEN]"
@@ -252,15 +258,11 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error>
}
async fn log_contactlist(context: &Context, contacts: &[u32]) {
let mut contacts = contacts.to_vec();
if !contacts.contains(&1) {
contacts.push(1);
}
for contact_id in contacts {
let line;
let mut line2 = "".to_string();
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
let name = contact.get_name();
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
let name = contact.get_display_name();
let addr = contact.get_addr();
let verified_state = contact.is_verified(context).await;
let verified_str = if VerifiedStatus::Unverified != verified_state {
@@ -289,14 +291,14 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) {
let peerstate = Peerstate::from_addr(context, &addr)
.await
.expect("peerstate error");
if peerstate.is_some() && contact_id != 1 as libc::c_uint {
if peerstate.is_some() && *contact_id != 1 {
line2 = format!(
", prefer-encrypt={}",
peerstate.as_ref().unwrap().prefer_encrypt
);
}
println!("Contact#{}: {}{}", contact_id, line, line2);
println!("Contact#{}: {}{}", *contact_id, line, line2);
}
}
}
@@ -356,7 +358,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
listarchived\n\
chat [<chat-id>|0]\n\
createchat <contact-id>\n\
createchatbymsg <msg-id>\n\
creategroup <name>\n\
createprotected <name>\n\
addmember <contact-id>\n\
@@ -380,9 +381,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
unarchive <chat-id>\n\
pin <chat-id>\n\
unpin <chat-id>\n\
mute <chat-id> [<seconds>]\n\
unmute <chat-id>\n\
protect <chat-id>\n\
unprotect <chat-id>\n\
delchat <chat-id>\n\
===========================Contact requests==\n\
decidestartchat <msg-id>\n\
decideblock <msg-id>\n\
decidenotnow <msg-id>\n\
===========================Message commands==\n\
listmsgs <query>\n\
msginfo <msg-id>\n\
@@ -398,6 +405,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
contactinfo <contact-id>\n\
delcontact <contact-id>\n\
cleanupcontacts\n\
block <contact-id>\n\
unblock <contact-id>\n\
listblocked\n\
======================================Misc.==\n\
getqr [<chat-id>]\n\
getbadqr\n\
@@ -503,7 +513,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
context.maybe_network().await;
}
"housekeeping" => {
sql::housekeeping(&context).await;
sql::housekeeping(&context).await.ok_or_log(&context);
}
"listchats" | "listarchived" | "chats" => {
let listflags = if arg0 == "listarchived" { 0x01 } else { 0 };
@@ -515,9 +525,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
None,
)
.await?;
let time_needed = std::time::SystemTime::now()
.duration_since(time_start)
.unwrap_or_default();
let time_needed = time_start.elapsed().unwrap_or_default();
let cnt = chatlist.len();
if cnt > 0 {
@@ -528,11 +536,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
println!(
"{}#{}: {} [{} fresh] {}{}",
"{}#{}: {} [{} fresh] {}{}{}",
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
chat.get_id().get_fresh_msg_cnt(&context).await,
chat.get_id().get_fresh_msg_cnt(&context).await?,
if chat.is_muted() { "🔇" } else { "" },
match chat.visibility {
ChatVisibility::Normal => "",
ChatVisibility::Archived => "📦",
@@ -593,7 +602,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "Failed to select chat");
let sel_chat = sel_chat.as_ref().unwrap();
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await;
let time_start = std::time::SystemTime::now();
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await?;
let time_needed = time_start.elapsed().unwrap_or_default();
let msglist: Vec<MsgId> = msglist
.into_iter()
.map(|x| match x {
@@ -603,27 +615,30 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
})
.collect();
let members = chat::get_chat_contacts(&context, sel_chat.id).await;
let members = chat::get_chat_contacts(&context, sel_chat.id).await?;
let subtitle = if sel_chat.is_device_talk() {
"device-talk".to_string()
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
let contact = Contact::get_by_id(&context, members[0]).await?;
contact.get_addr().to_string()
} else if sel_chat.get_type() == Chattype::Mailinglist && !members.is_empty() {
"mailinglist".to_string()
} else {
format!("{} member(s)", members.len())
};
println!(
"{}#{}: {} [{}]{}{} {}",
"{}#{}: {} [{}]{}{}{} {}",
chat_prefix(sel_chat),
sel_chat.get_id(),
sel_chat.get_name(),
subtitle,
if sel_chat.is_muted() { "🔇" } else { "" },
if sel_chat.is_sending_locations() {
"📍"
} else {
""
},
match sel_chat.get_profile_image(&context).await {
match sel_chat.get_profile_image(&context).await? {
Some(icon) => match icon.to_str() {
Some(icon) => format!(" Icon: {}", icon),
_ => " Icon: Err".to_string(),
@@ -643,24 +658,53 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!(
"{} messages.",
sel_chat.get_id().get_msg_cnt(&context).await
sel_chat.get_id().get_msg_cnt(&context).await?
);
let time_noticed_start = std::time::SystemTime::now();
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
println!(
"{:?} to create this list, {:?} to mark all messages as noticed.",
time_needed, time_noticed_needed
);
}
"createchat" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id: libc::c_int = arg1.parse()?;
let chat_id = chat::create_by_contact_id(&context, contact_id as u32).await?;
let contact_id: u32 = arg1.parse()?;
let chat_id = chat::create_by_contact_id(&context, contact_id).await?;
println!("Single#{} created successfully.", chat_id,);
}
"createchatbymsg" => {
"decidestartchat" | "createchatbymsg" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
let msg_id = MsgId::new(arg1.parse()?);
let chat_id = chat::create_by_msg_id(&context, msg_id).await?;
let chat = Chat::load_from_db(&context, chat_id).await?;
println!("{}#{} created successfully.", chat_prefix(&chat), chat_id,);
match message::decide_on_contact_request(
&context,
msg_id,
ContactRequestDecision::StartChat,
)
.await
{
Some(chat_id) => {
let chat = Chat::load_from_db(&context, chat_id).await?;
println!("{}#{} created successfully.", chat_prefix(&chat), chat_id);
}
None => println!("Cannot crate chat."),
}
}
"decidenotnow" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
let msg_id = MsgId::new(arg1.parse()?);
message::decide_on_contact_request(&context, msg_id, ContactRequestDecision::NotNow)
.await;
}
"decideblock" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing");
let msg_id = MsgId::new(arg1.parse()?);
message::decide_on_contact_request(&context, msg_id, ContactRequestDecision::Block)
.await;
}
"creategroup" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
@@ -680,11 +724,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected");
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id_0: libc::c_int = arg1.parse()?;
let contact_id_0: u32 = arg1.parse()?;
if chat::add_contact_to_chat(
&context,
sel_chat.as_ref().unwrap().get_id(),
contact_id_0 as u32,
contact_id_0,
)
.await
{
@@ -696,11 +740,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"removemember" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id_1: libc::c_int = arg1.parse()?;
let contact_id_1: u32 = arg1.parse()?;
chat::remove_contact_from_chat(
&context,
sel_chat.as_ref().unwrap().get_id(),
contact_id_1 as u32,
contact_id_1,
)
.await?;
@@ -726,7 +770,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected.");
let contacts =
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await;
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
println!("Memberlist:");
log_contactlist(&context, &contacts).await;
@@ -751,7 +795,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
0,
0,
)
.await;
.await?;
let default_marker = "-".to_string();
for location in &locations {
let marker = location.marker.as_ref().unwrap_or(&default_marker);
@@ -863,10 +907,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
None
};
let msglist = context.search_msgs(chat, arg1).await;
let time_start = std::time::SystemTime::now();
let msglist = context.search_msgs(chat, arg1).await?;
let time_needed = time_start.elapsed().unwrap_or_default();
log_msglist(&context, &msglist).await?;
println!("{} messages.", msglist.len());
println!("{:?} to create this list", time_needed);
}
"draft" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -879,7 +926,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.unwrap()
.get_id()
.set_draft(&context, Some(&mut draft))
.await;
.await?;
println!("Draft saved.");
} else {
sel_chat
@@ -887,7 +934,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.unwrap()
.get_id()
.set_draft(&context, None)
.await;
.await?;
println!("Draft deleted.");
}
}
@@ -910,7 +957,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Viewtype::Gif,
Viewtype::Video,
)
.await;
.await?;
println!("{} images or videos: ", images.len());
for (i, data) in images.iter().enumerate() {
if 0 == i {
@@ -936,6 +983,24 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
)
.await?;
}
"mute" | "unmute" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
let duration = match arg0 {
"mute" => {
if arg2.is_empty() {
MuteDuration::Forever
} else {
SystemTime::now()
.checked_add(Duration::from_secs(arg2.parse()?))
.map_or(MuteDuration::Forever, MuteDuration::Until)
}
}
"unmute" => MuteDuration::NotMuted,
_ => unreachable!("arg0={:?}", arg0),
};
chat::set_muted(&context, chat_id, duration).await?;
}
"protect" | "unprotect" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
@@ -958,7 +1023,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?);
let res = message::get_msg_info(&context, id).await;
let res = message::get_msg_info(&context, id).await?;
println!("{}", res);
}
"html" => {
@@ -967,12 +1032,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let file = dirs::home_dir()
.unwrap_or_default()
.join(format!("msg-{}.html", id.to_u32()));
let html = id.get_html(&context).await.unwrap_or_default();
let html = id.get_html(&context).await?.unwrap_or_default();
fs::write(&file, html)?;
println!("HTML written to: {:#?}", file);
}
"listfresh" => {
let msglist = context.get_fresh_msgs().await;
let msglist = context.get_fresh_msgs().await?;
log_msglist(&context, &msglist).await?;
print!("{} fresh messages.", msglist.len());
@@ -1004,9 +1069,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let contacts = Contact::get_all(
&context,
if arg0 == "listverified" {
0x1 | 0x2
DC_GCL_VERIFIED_ONLY | DC_GCL_ADD_SELF
} else {
0x2
DC_GCL_ADD_SELF
},
Some(arg1),
)
@@ -1027,14 +1092,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"contactinfo" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
let contact_id: u32 = arg1.parse()?;
let contact = Contact::get_by_id(&context, contact_id).await?;
let name_n_addr = contact.get_name_n_addr();
let mut res = format!(
"Contact info for: {}:\nIcon: {}\n",
name_n_addr,
match contact.get_profile_image(&context).await {
match contact.get_profile_image(&context).await? {
Some(image) => image.to_str().unwrap().to_string(),
None => "NoIcon".to_string(),
}
@@ -1064,6 +1129,21 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
Contact::delete(&context, arg1.parse()?).await?;
}
"block" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
Contact::block(&context, contact_id).await;
}
"unblock" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
Contact::unblock(&context, contact_id).await;
}
"listblocked" => {
let contacts = Contact::get_all_blocked(&context).await?;
log_contactlist(&context, &contacts).await;
println!("{} blocked contacts.", contacts.len());
}
"checkqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
let res = check_qr(&context, arg1).await;
@@ -1108,7 +1188,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
// let r = context.emit_event(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
// println!(
// "Sending event {:?}({}), received value {}.",
// event, event as usize, r as libc::c_int,
// event, event as usize, r,
// );
// }
"fileinfo" => {

View File

@@ -168,12 +168,14 @@ const DB_COMMANDS: [&str; 9] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 28] = [
const CHAT_COMMANDS: [&str; 34] = [
"listchats",
"listarchived",
"chat",
"createchat",
"createchatbymsg",
"decidestartchat",
"decideblock",
"decidenotnow",
"creategroup",
"createverified",
"addmember",
@@ -196,6 +198,10 @@ const CHAT_COMMANDS: [&str; 28] = [
"unarchive",
"pin",
"unpin",
"mute",
"unmute",
"protect",
"unprotect",
"delchat",
];
const MESSAGE_COMMANDS: [&str; 6] = [
@@ -206,13 +212,16 @@ const MESSAGE_COMMANDS: [&str; 6] = [
"markseen",
"delmsg",
];
const CONTACT_COMMANDS: [&str; 6] = [
const CONTACT_COMMANDS: [&str; 9] = [
"listcontacts",
"listverified",
"addcontact",
"contactinfo",
"delcontact",
"cleanupcontacts",
"block",
"unblock",
"listblocked",
];
const MISC_COMMANDS: [&str; 10] = [
"getqr",
@@ -381,7 +390,7 @@ async fn handle_cmd(
ctx.configure().await?;
}
"oauth2" => {
if let Some(addr) = ctx.get_config(config::Config::Addr).await {
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
let oauth2_url =
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await;
if oauth2_url.is_none() {

View File

@@ -1,3 +1,10 @@
1.51.0
------
- adapt python bindings and APIs to core51 release
(see CHANGELOG of https://github.com/deltachat/deltachat-core-rust/blob/1.51.0/CHANGELOG.md#1510
for more details on all core changes)
1.44.0
------

View File

@@ -142,7 +142,7 @@ This docker image can be used to run tests and build Python wheels for all inter
$ docker run -e DCC_NEW_TMP_EMAIL \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh
deltachat/coredeps scripts/run_all.sh
Optionally build your own docker image
@@ -151,9 +151,9 @@ Optionally build your own docker image
If you want to build your own custom docker image you can do this::
$ cd deltachat-core # cd to deltachat-core checkout directory
$ docker build -t deltachat/coredeps ci_scripts/docker_coredeps
$ docker build -t deltachat/coredeps scripts/docker_coredeps
This will use the ``ci_scripts/docker_coredeps/Dockerfile`` to build
This will use the ``scripts/docker_coredeps/Dockerfile`` to build
up docker image called ``deltachat/coredeps``. You can afterwards
find it with::

View File

@@ -150,6 +150,7 @@ def extract_defines(flags):
| DC_CHAT
| DC_PROVIDER
| DC_KEY_GEN
| DC_IMEX
) # End of prefix matching
_[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
) # Close the capturing group, this contains

View File

@@ -89,6 +89,22 @@ class Account(object):
d[key.lower()] = value
return d
def dump_account_info(self, logfile):
def log(*args, **kwargs):
kwargs["file"] = logfile
print(*args, **kwargs)
log("=============== " + self.get_config("displayname") + " ===============")
cursor = 0
for name, val in self.get_info().items():
entry = "{}={}".format(name.upper(), val)
if cursor + len(entry) > 80:
log("")
cursor = 0
log(entry, end=" ")
cursor += len(entry) + 1
log("")
def set_stock_translation(self, id, string):
""" set stock translation string.
@@ -412,23 +428,23 @@ class Account(object):
Note that the account does not have to be started.
"""
return self._export(path, imex_cmd=1)
return self._export(path, imex_cmd=const.DC_IMEX_EXPORT_SELF_KEYS)
def export_all(self, path):
"""return new file containing a backup of all database state
(chats, contacts, keys, media, ...). The file is created in the
the `path` directory.
Note that the account does not have to be started.
Note that the account has to be stopped; call stop_io() if necessary.
"""
export_files = self._export(path, 11)
export_files = self._export(path, const.DC_IMEX_EXPORT_BACKUP)
if len(export_files) != 1:
raise RuntimeError("found more than one new file")
return export_files[0]
def _export(self, path, imex_cmd):
with self.temp_plugin(ImexTracker()) as imex_tracker:
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
self.imex(path, imex_cmd)
return imex_tracker.wait_finish()
def import_self_keys(self, path):
@@ -438,7 +454,7 @@ class Account(object):
Note that the account does not have to be started.
"""
self._import(path, imex_cmd=2)
self._import(path, imex_cmd=const.DC_IMEX_IMPORT_SELF_KEYS)
def import_all(self, path):
"""import delta chat state from the specified backup `path` (a file).
@@ -446,13 +462,16 @@ class Account(object):
The account must be in unconfigured state for import to attempted.
"""
assert not self.is_configured(), "cannot import into configured account"
self._import(path, imex_cmd=12)
self._import(path, imex_cmd=const.DC_IMEX_IMPORT_BACKUP)
def _import(self, path, imex_cmd):
with self.temp_plugin(ImexTracker()) as imex_tracker:
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
self.imex(path, imex_cmd)
imex_tracker.wait_finish()
def imex(self, path, imex_cmd):
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
def initiate_key_transfer(self):
"""return setup code after a Autocrypt setup message
has been successfully sent to our own e-mail address ("self-sent message").
@@ -577,6 +596,28 @@ class Account(object):
raise ValueError("account not configured, cannot start io")
lib.dc_start_io(self._dc_context)
def maybe_network(self):
"""This function should be called when there is a hint
that the network is available again,
e.g. as a response to system event reporting network availability.
The library will try to send pending messages out immediately.
Moreover, to have a reliable state
when the app comes to foreground with network available,
it may be reasonable to call the function also at that moment.
It is okay to call the function unconditionally when there is
network available, however, calling the function
_without_ having network may interfere with the backoff algorithm
and will led to let the jobs fail faster, with fewer retries
and may avoid messages being sent out.
Finally, if the context was created by the dc_accounts_t account manager
(currently not implemented in the Python bindings),
use dc_accounts_maybe_network() instead of this function
"""
lib.dc_maybe_network(self._dc_context)
def configure(self, reconfigure=False):
""" Start configuration process and return a Configtracker instance
on which you can block with wait_finish() to get a True/False success

View File

@@ -254,17 +254,19 @@ class Chat(object):
return Message.from_db(self.account, sent_id)
def prepare_message(self, msg):
""" create a new prepared message.
""" prepare a message for sending.
:param msg: the message to be prepared.
:returns: :class:`deltachat.message.Message` instance.
:returns: a :class:`deltachat.message.Message` instance.
This is the same object that was passed in, which
has been modified with the new state of the core.
"""
msg_id = lib.dc_prepare_msg(self.account._dc_context, self.id, msg._dc_msg)
if msg_id == 0:
raise ValueError("message could not be prepared")
# invalidate passed in message which is not safe to use anymore
msg._dc_msg = msg.id = None
return Message.from_db(self.account, msg_id)
# modify message in place to avoid bad state for the caller
msg._dc_msg = Message.from_db(self.account, msg_id)._dc_msg
return msg
def prepare_message_file(self, path, mime_type=None, view_type="file"):
""" prepare a message for sending and return the resulting Message instance.

View File

@@ -47,8 +47,9 @@ def dc_account_extra_configure(account):
except Exception as e:
# Uncaught exceptions here would lead to a timeout without any note written to the log
account.log("=============================== CAN'T RESET ACCOUNT: ===============================")
account.log("===================", e, "===================")
# start with DC_EVENT_WARNING so that the line is printed in yellow and won't be overlooked when reading
account.log("DC_EVENT_WARNING =================== DIRECT_IMAP CAN'T RESET ACCOUNT: ===================")
account.log("DC_EVENT_WARNING =================== " + str(e) + " ===================")
@deltachat.global_hookimpl
@@ -172,21 +173,6 @@ class DirectImap:
def get_unread_cnt(self):
return len(self.get_unread_messages())
def dump_account_info(self, logfile):
def log(*args, **kwargs):
kwargs["file"] = logfile
print(*args, **kwargs)
cursor = 0
for name, val in self.account.get_info().items():
entry = "{}={}".format(name.upper(), val)
if cursor + len(entry) > 80:
log("")
cursor = 0
log(entry, end=" ")
cursor += len(entry) + 1
log("")
def dump_imap_structures(self, dir, logfile):
assert not self._idling
stream = io.StringIO()

View File

@@ -21,8 +21,8 @@ class Message(object):
assert isinstance(dc_msg, ffi.CData)
assert dc_msg != ffi.NULL
self._dc_msg = dc_msg
self.id = lib.dc_msg_get_id(dc_msg)
assert self.id is not None and self.id >= 0, repr(self.id)
msg_id = self.id
assert msg_id is not None and msg_id >= 0, repr(msg_id)
def __eq__(self, other):
return self.account == other.account and self.id == other.id
@@ -46,9 +46,13 @@ class Message(object):
def new_empty(cls, account, view_type):
""" create a non-persistent message.
:param: view_type is "text", "audio", "video", "file"
:param: view_type is the message type code or one of the strings:
"text", "audio", "video", "file", "sticker"
"""
view_type_code = get_viewtype_code_from_name(view_type)
if isinstance(view_type, int):
view_type_code = view_type
else:
view_type_code = get_viewtype_code_from_name(view_type)
return Message(account, ffi.gc(
lib.dc_msg_new(account._dc_context, view_type_code),
lib.dc_msg_unref
@@ -68,6 +72,11 @@ class Message(object):
self._dc_msg = ffi.gc(lib.dc_get_msg(ctx, self.id), lib.dc_msg_unref)
return Chat(self.account, chat_id)
@props.with_doc
def id(self):
"""id of this message. """
return lib.dc_msg_get_id(self._dc_msg)
@props.with_doc
def text(self):
"""unicode text of this messages (might be empty if not a text message). """
@@ -77,6 +86,23 @@ class Message(object):
"""set text of this message. """
lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
@props.with_doc
def html(self):
"""html text of this messages (might be empty if not an html message). """
return from_dc_charpointer(
lib.dc_get_msg_html(self.account._dc_context, self.id)) or ""
def has_html(self):
"""return True if this message has an html part, False otherwise."""
return lib.dc_msg_has_html(self._dc_msg)
def set_html(self, html_text):
"""set the html part of this message.
It is possible to have text and html part at the same time.
"""
lib.dc_msg_set_html(self._dc_msg, as_dc_charpointer(html_text))
@props.with_doc
def filename(self):
"""filename if there was an attachment, otherwise empty string. """
@@ -227,6 +253,20 @@ class Message(object):
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
return Chat(self.account, chat_id)
@props.with_doc
def override_sender_name(self):
"""the name that should be shown over the message instead of the contact display name.
Usually used to impersonate someone else.
"""
return from_dc_charpointer(
lib.dc_msg_get_override_sender_name(self._dc_msg))
def set_override_sender_name(self, name):
"""set different sender name for a message. """
lib.dc_msg_set_override_sender_name(
self._dc_msg, as_dc_charpointer(name))
def get_sender_chat(self):
"""return the 1:1 chat with the sender of this message.
@@ -339,6 +379,10 @@ class Message(object):
""" return True if it's a gif message. """
return self._view_type == const.DC_MSG_GIF
def is_sticker(self):
""" return True if it's a sticker message. """
return self._view_type == const.DC_MSG_STICKER
def is_audio(self):
""" return True if it's an audio message. """
return self._view_type == const.DC_MSG_AUDIO
@@ -359,21 +403,22 @@ class Message(object):
# some code for handling DC_MSG_* view types
_view_type_mapping = {
const.DC_MSG_TEXT: 'text',
const.DC_MSG_IMAGE: 'image',
const.DC_MSG_GIF: 'gif',
const.DC_MSG_AUDIO: 'audio',
const.DC_MSG_VIDEO: 'video',
const.DC_MSG_FILE: 'file'
'text': const.DC_MSG_TEXT,
'image': const.DC_MSG_IMAGE,
'gif': const.DC_MSG_GIF,
'audio': const.DC_MSG_AUDIO,
'video': const.DC_MSG_VIDEO,
'file': const.DC_MSG_FILE,
'sticker': const.DC_MSG_STICKER,
}
def get_viewtype_code_from_name(view_type_name):
for code, value in _view_type_mapping.items():
if value == view_type_name:
return code
code = _view_type_mapping.get(view_type_name)
if code is not None:
return code
raise ValueError("message typecode not found for {!r}, "
"available {!r}".format(view_type_name, list(_view_type_mapping.values())))
"available {!r}".format(view_type_name, list(_view_type_mapping.keys())))
#

View File

@@ -414,13 +414,13 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
def dump_imap_summary(self, logfile):
for ac in self._accounts:
ac.dump_account_info(logfile=logfile)
imap = getattr(ac, "direct_imap", None)
if imap is not None:
try:
imap.idle_done()
except Exception:
pass
imap.dump_account_info(logfile=logfile)
imap.dump_imap_structures(tmpdir, logfile=logfile)
def get_accepted_chat(self, ac1, ac2):

View File

@@ -476,6 +476,7 @@ class TestOfflineChat:
contact = msg.get_sender_contact()
assert contact == ac1.get_self_contact()
assert not backupdir.listdir()
ac1.stop_io()
path = ac1.export_all(backupdir.strpath)
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account()
@@ -652,7 +653,7 @@ class TestOnlineAccount:
config={"key_gen_type": str(const.DC_KEY_GEN_ED25519)}
)
# rsa key gen can be slow especially on CI, adjust timeout
ac1._evtracker.set_timeout(120)
ac1._evtracker.set_timeout(240)
acfactory.wait_configure_and_start_io()
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -815,6 +816,48 @@ class TestOnlineAccount:
assert open(msg.filename).read() == content
assert msg.filename.endswith(basename)
def test_html_message(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
chat = acfactory.get_accepted_chat(ac1, ac2)
html_text = "<p>hello HTML world</p>"
lp.sec("ac1: prepare and send text message to ac2")
msg1 = chat.send_text("message0")
assert not msg1.has_html()
assert msg1.html == ""
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message0"
assert not msg2.has_html()
assert msg2.html == ""
lp.sec("ac1: prepare and send HTML+text message to ac2")
msg1 = Message.new_empty(ac1, "text")
msg1.set_text("message1")
msg1.set_html(html_text)
msg1 = chat.send_msg(msg1)
assert msg1.has_html()
assert html_text in msg1.html
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message1"
assert msg2.has_html()
assert html_text in msg2.html
lp.sec("ac1: prepare and send HTML-only message to ac2")
msg1 = Message.new_empty(ac1, "text")
msg1.set_html(html_text)
msg1 = chat.send_msg(msg1)
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert "<p>" not in msg2.text
assert "hello HTML world" in msg2.text
assert msg2.has_html()
assert html_text in msg2.html
def test_mvbox_sentbox_threads(self, acfactory, lp):
lp.sec("ac1: start with mvbox thread")
ac1 = acfactory.get_online_configuring_account(mvbox=True, move=True, sentbox=True)
@@ -990,6 +1033,38 @@ class TestOnlineAccount:
except queue.Empty:
pass # mark_seen_messages() has generated events before it returns
def test_message_override_sender_name(self, acfactory, lp):
ac1, ac2 = acfactory.get_two_online_accounts()
chat = acfactory.get_accepted_chat(ac1, ac2)
overridden_name = "someone else"
ac1.set_config("displayname", "ac1")
lp.sec("sending text message with overridden name from ac1 to ac2")
msg1 = Message.new_empty(ac1, "text")
msg1.set_override_sender_name(overridden_name)
msg1.set_text("message1")
msg1 = chat.send_msg(msg1)
assert msg1.override_sender_name == overridden_name
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message1"
assert msg2.get_sender_contact().name == ac1.get_config("displayname")
assert msg2.override_sender_name == overridden_name
lp.sec("sending normal text message from ac1 to ac2")
msg1 = Message.new_empty(ac1, "text")
msg1.set_text("message2")
msg1 = chat.send_msg(msg1)
assert not msg1.override_sender_name
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message2"
assert msg2.get_sender_contact().name == ac1.get_config("displayname")
assert not msg2.override_sender_name
@pytest.mark.parametrize("mvbox_move", [True, False])
def test_markseen_message_and_mdn(self, acfactory, mvbox_move):
# Please only change this test if you are very sure that it will still catch the issues it catches now.
@@ -1497,21 +1572,47 @@ class TestOnlineAccount:
original_image_path = data.get_path("d.png")
chat1.send_image(original_image_path)
# Add another 100KB file that ensures that the progress is smooth enough
path = tmpdir.join("attachment.txt")
with open(path, "w") as file:
file.truncate(100000)
chat1.send_file(path.strpath)
def assert_account_is_proper(ac):
contacts = ac.get_contacts(query="some1")
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == "some1@example.org"
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 3
assert messages[0].text == "msg1"
assert messages[1].filemime == "image/png"
assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size
ac.set_config("displayname", "new displayname")
assert ac.get_config("displayname") == "new displayname"
assert_account_is_proper(ac1)
backupdir = tmpdir.mkdir("backup")
lp.sec("export all to {}".format(backupdir))
with ac1.temp_plugin(ImexTracker()) as imex_tracker:
path = ac1.export_all(backupdir.strpath)
assert os.path.exists(path)
ac1.stop_io()
ac1.imex(backupdir.strpath, const.DC_IMEX_EXPORT_BACKUP)
# check progress events for export
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
assert imex_tracker.wait_progress(250, progress_upper_limit=499)
assert imex_tracker.wait_progress(500, progress_upper_limit=749)
assert imex_tracker.wait_progress(750, progress_upper_limit=999)
assert imex_tracker.wait_progress(1000)
t = time.time()
paths = imex_tracker.wait_finish()
assert len(paths) == 1
path = paths[0]
assert os.path.exists(path)
ac1.start_io()
lp.sec("get fresh empty account")
ac2 = acfactory.get_unconfigured_account()
@@ -1530,23 +1631,11 @@ class TestOnlineAccount:
assert imex_tracker.wait_progress(750, progress_upper_limit=999)
assert imex_tracker.wait_progress(1000)
contacts = ac2.get_contacts(query="some1")
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == "some1@example.org"
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2
assert messages[0].text == "msg1"
assert messages[1].filemime == "image/png"
assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size
assert_account_is_proper(ac1)
assert_account_is_proper(ac2)
# wait until a second passed since last backup
# because get_latest_backupfile() shall return the latest backup
# from a UI it's unlikely anyone manages to export two
# backups in one second.
time.sleep(max(0, 1 - (time.time() - t)))
lp.sec("Second-time export all to {}".format(backupdir))
ac1.stop_io()
path2 = ac1.export_all(backupdir.strpath)
assert os.path.exists(path2)
assert path2 != path
@@ -2178,9 +2267,25 @@ class TestOnlineAccount:
chat12 = acfactory.get_accepted_chat(ac1, ac2)
ac1.set_config("selfstatus", "New status")
chat12.send_text("hi")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hi"
assert msg.get_sender_contact().status == "New status"
msg_received = ac2._evtracker.wait_next_incoming_message()
assert msg_received.text == "hi"
assert msg_received.get_sender_contact().status == "New status"
# Send a reply from ac2 to ac1 so ac1 can send a read receipt.
reply_msg = msg_received.chat.send_text("reply")
reply_msg_received = ac1._evtracker.wait_next_incoming_message()
assert reply_msg_received.text == "reply"
# Send read receipt from ac1 to ac2.
# It does not contain the signature.
ac1.mark_seen_messages([reply_msg_received])
ev = ac2._evtracker.get_matching("DC_EVENT_MSG_READ")
assert ev.data1 == reply_msg.chat.id
assert ev.data2 == reply_msg.id
assert reply_msg.is_out_mdn_received()
# Test that the status is not cleared as a result of receiving a read receipt.
assert msg_received.get_sender_contact().status == "New status"
ac1.set_config("selfstatus", "")
chat12.send_text("hello")
@@ -2278,6 +2383,14 @@ class TestOnlineAccount:
Also, the newest existing emails from each folder are fetched during onboarding.
Additionally tests that bcc_self messages moved to the mvbox/sentbox are marked as read."""
def assert_folders_configured(ac):
"""There was a bug that scan_folders() set the configured folders to None under some circumstances.
So, check that they are still configured:"""
assert ac.get_config("configured_sentbox_folder") == "Sent"
if mvbox_move:
assert ac.get_config("configured_mvbox_folder")
ac1 = acfactory.get_online_configuring_account(mvbox=mvbox_move, move=mvbox_move)
ac1.set_config("sentbox_move", "1")
ac2 = acfactory.get_online_configuring_account()
@@ -2294,6 +2407,7 @@ class TestOnlineAccount:
# ConfiguredSentboxFolder and do nothing.
ac1._configtracker = ac1.configure(reconfigure=True)
acfactory.wait_configure_and_start_io()
assert_folders_configured(ac1)
if mvbox_move:
ac1.direct_imap.select_config_folder("mvbox")
@@ -2305,22 +2419,28 @@ class TestOnlineAccount:
ac1.set_config("bcc_self", "1")
chat = acfactory.get_accepted_chat(ac1, ac2)
chat.send_text("message text")
assert_folders_configured(ac1)
# now wait until the bcc_self message arrives
# Also test that bcc_self messages moved to the mvbox are marked as read.
assert ac1.direct_imap.idle_wait_for_seen()
assert_folders_configured(ac1)
ac1_clone = acfactory.clone_online_account(ac1)
ac1_clone.set_config("fetch_existing_msgs", "1")
ac1_clone._configtracker.wait_finish()
ac1_clone.start_io()
assert_folders_configured(ac1_clone)
ac1_clone._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
ac2_addr = ac2.get_config("addr")
assert any(c.addr == ac2_addr for c in ac1_clone.get_contacts())
assert_folders_configured(ac1_clone)
msg = ac1_clone._evtracker.wait_next_messages_changed()
assert msg.text == "message text"
assert_folders_configured(ac1)
assert_folders_configured(ac1_clone)
def test_fetch_existing_msgs_group_and_single(self, acfactory, lp):
"""There was a bug concerning fetch-existing-msgs:

View File

@@ -13,7 +13,6 @@ passenv =
TRAVIS
DCC_RS_DEV
DCC_RS_TARGET
DCC_PY_LIVECONFIG
DCC_NEW_TMP_EMAIL
CARGO_TARGET_DIR
RUSTC_WRAPPER
@@ -47,7 +46,9 @@ commands =
[testenv:doc]
changedir=doc
deps =
sphinx
# With Python 3.7 and Sphinx 3.5.0, it throws an exception.
# Pin the version to the working one.
sphinx==3.4.3
breathe
commands =
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html

View File

@@ -1 +1 @@
1.49.0
1.50.0

View File

@@ -1,11 +1,15 @@
# Continuous Integration Scripts for Delta Chat
Continuous Integration, run through CircleCI and an own build machine.
Continuous Integration, run through [GitHub
Actions](https://docs.github.com/actions),
[CircleCI](https://app.circleci.com/) and an own build machine.
## Description of scripts
- `../.github/workflows` contains jobs run by GitHub Actions.
- `../.circleci/config.yml` describing the build jobs that are run
by Circle-CI
by CircleCI.
- `remote_tests_python.sh` rsyncs to a build machine and runs
`run-python-test.sh` remotely on the build machine.
@@ -26,8 +30,8 @@ There is experimental support for triggering a remote Python or Rust test run
from your local checkout/branch. You will need to be authorized to login to
the build machine (ask your friendly sysadmin on #deltachat freenode) to type::
ci_scripts/manual_remote_tests.sh rust
ci_scripts/manual_remote_tests.sh python
scripts/manual_remote_tests.sh rust
scripts/manual_remote_tests.sh python
This will **rsync** your current checkout to the remote build machine
(no need to commit before) and then run either rust or python tests.
@@ -41,6 +45,6 @@ python tests and build wheels (binary packages for Python)
You can build the docker images yourself locally
to avoid the relatively large download::
cd ci_scripts # where all CI things are
cd scripts # where all CI things are
docker build -t deltachat/coredeps docker-coredeps
docker build -t deltachat/doxygen docker-doxygen

75
scripts/ci_upload.sh Executable file
View File

@@ -0,0 +1,75 @@
#!/bin/bash
if [ -z "$DEVPI_LOGIN" ] ; then
echo "required: password for 'dc' user on https://m.devpi/net/dc index"
exit 0
fi
set -xe
PYDOCDIR=${1:?directory with python docs}
WHEELHOUSEDIR=${2:?directory with pre-built wheels}
DOXYDOCDIR=${3:?directory where doxygen docs to be found}
SSHTARGET=ci@b1.delta.chat
# if CIRCLE_BRANCH is not set we are called for a tag with empty CIRCLE_BRANCH variable.
export BRANCH=${CIRCLE_BRANCH:master}
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}/wheelhouse
# 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}
echo -----------------------
echo upload wheels
echo -----------------------
# Bundle external shared libraries into the wheels
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $SSHTARGET mkdir -p $BUILDDIR
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null scripts/cleanup_devpi_indices.py $SSHTARGET:$BUILDDIR
rsync -avz \
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
$WHEELHOUSEDIR \
$SSHTARGET:$BUILDDIR
ssh $SSHTARGET <<_HERE
set +x -e
# make sure all processes exit when ssh dies
shopt -s huponexit
# we rely on the "venv" virtualenv on the remote account to exist
source venv/bin/activate
cd $BUILDDIR
devpi use https://m.devpi.net
devpi login dc --password $DEVPI_LOGIN
N_BRANCH=${BRANCH//[\/]}
devpi use dc/\$N_BRANCH || {
devpi index -c \$N_BRANCH
devpi use dc/\$N_BRANCH
}
devpi index \$N_BRANCH bases=/root/pypi
devpi upload wheelhouse/deltachat*
# remove devpi non-master dc indices if thy are too old
# this script was copied above
python cleanup_devpi_indices.py
_HERE

26
scripts/coverage.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/sh
set -eu
if ! which grcov 2>/dev/null 1>&2; then
echo >&2 '`grcov` not found. Check README at https://github.com/mozilla/grcov for setup instructions.'
echo >&2 'Run `cargo install grcov` to build `grcov` from source.'
exit 1
fi
# Allow `-Z` flags without using nightly Rust.
export RUSTC_BOOTSTRAP=1
# We are using `-Zprofile` instead of source-based coverage [1]
# (`-Zinstrument-coverage`) due to a bug resulting in empty reports [2].
#
# [1] https://blog.rust-lang.org/inside-rust/2020/11/12/source-based-code-coverage.html
# [2] https://github.com/mozilla/grcov/issues/595
export CARGO_INCREMENTAL=0
export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
export RUSTDOCFLAGS="-Cpanic=abort"
cargo clean
cargo build
cargo test
grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing -o ./coverage/

View File

@@ -3,9 +3,9 @@
set -e -x
# Install Rust
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.49.0-x86_64-unknown-linux-gnu -y
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.50.0-x86_64-unknown-linux-gnu -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/1.49.0-x86_64-unknown-linux-gnu/share
rm -rf /root/.rustup/toolchains/1.50.0-x86_64-unknown-linux-gnu/share

View File

@@ -6,4 +6,4 @@ export CIRCLE_BUILD_NUM=$USER
export CIRCLE_BRANCH=`git branch | grep \* | cut -d ' ' -f2`
export CIRCLE_PROJECT_REPONAME=$(basename `git rev-parse --show-toplevel`)
time bash ci_scripts/$CIRCLE_JOB.sh
time bash scripts/$CIRCLE_JOB.sh

View File

@@ -45,7 +45,6 @@ if [ -n "$TESTS" ]; then
# messages and rust's imap code likely has concurrency problems)
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "not qr"
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "qr"
unset DCC_PY_LIVECONFIG
unset DCC_NEW_TMP_EMAIL
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
tox --workdir "$TOXWORKDIR" -e auditwheels

View File

@@ -30,14 +30,13 @@ ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
set +x -e
shopt -s huponexit
cd $BUILDDIR
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
set -x
# run everything else inside docker
docker run -e DCC_NEW_TMP_EMAIL -e DCC_PY_LIVECONFIG \
docker run -e DCC_NEW_TMP_EMAIL \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh
deltachat/coredeps scripts/run_all.sh
_HERE

View File

@@ -29,7 +29,6 @@ ssh $SSHTARGET <<_HERE
export RUSTC_WRAPPER=\`which sccache\`
cd $BUILDDIR
export TARGET=release
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
#we rely on tox/virtualenv being available in the host
@@ -43,5 +42,5 @@ ssh $SSHTARGET <<_HERE
source \$HOME/venv/bin/activate
which python
bash ci_scripts/run-python-test.sh
bash scripts/run-python-test.sh
_HERE

View File

@@ -25,6 +25,6 @@ ssh $SSHTARGET <<_HERE
export TARGET=x86_64-unknown-linux-gnu
export RUSTC_WRAPPER=sccache
bash ci_scripts/run-rust-test.sh
bash scripts/run-rust-test.sh
_HERE

6
scripts/run-doxygen.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -ex
cd deltachat-ffi
PROJECT_NUMBER=$(git log -1 --format="%h (%cd)") doxygen

View File

@@ -4,11 +4,11 @@
# purposes. Any arguments are passed straight to tox. E.g. to run
# only one environment run with:
#
# ./run-integration-tests.sh -e py35
# scripts/run-integration-tests.sh -e py35
#
# To also run with `pytest -x` use:
#
# ./run-integration-tests.sh -e py35 -- -x
# scripts/run-integration-tests.sh -e py35 -- -x
export DCC_RS_DEV=$(pwd)
export DCC_RS_TARGET=${DCC_RS_TARGET:-release}
@@ -23,9 +23,6 @@ if [ $? != 0 ]; then
fi
pushd python
if [ -e "./liveconfig" -a -z "$DCC_PY_LIVECONFIG" ]; then
export DCC_PY_LIVECONFIG=liveconfig
fi
tox "$@"
ret=$?
popd

View File

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

0
set_core_version.py → scripts/set_core_version.py Normal file → Executable file
View File

View File

@@ -135,15 +135,25 @@ impl Accounts {
let old_id = self.config.get_selected_account().await;
// create new account
let account_config = self.config.new_account(&self.dir).await?;
let account_config = self
.config
.new_account(&self.dir)
.await
.context("failed to create new account")?;
let new_dbfile = account_config.dbfile().into();
let new_blobdir = Context::derive_blobdir(&new_dbfile);
let res = {
fs::create_dir_all(&account_config.dir).await?;
fs::rename(&dbfile, &new_dbfile).await?;
fs::rename(&blobdir, &new_blobdir).await?;
fs::create_dir_all(&account_config.dir)
.await
.context("failed to create dir")?;
fs::rename(&dbfile, &new_dbfile)
.await
.context("failed to rename dbfile")?;
fs::rename(&blobdir, &new_blobdir)
.await
.context("failed to rename blobdir")?;
Ok(())
};
@@ -502,7 +512,10 @@ mod tests {
let ctx = accounts.get_selected_account().await;
assert_eq!(
"me@mail.com",
ctx.get_config(crate::config::Config::Addr).await.unwrap()
ctx.get_config(crate::config::Config::Addr)
.await
.unwrap()
.unwrap()
);
}

View File

@@ -58,7 +58,7 @@ impl<'a> BlobObject<'a> {
) -> std::result::Result<BlobObject<'a>, BlobError> {
let blobdir = context.get_blobdir();
let (stem, ext) = BlobObject::sanitise_name(suggested_name.as_ref());
let (name, mut file) = BlobObject::create_new_file(&blobdir, &stem, &ext).await?;
let (name, mut file) = BlobObject::create_new_file(blobdir, &stem, &ext).await?;
file.write_all(data)
.await
.map_err(|err| BlobError::WriteFailure {
@@ -384,7 +384,7 @@ impl<'a> BlobObject<'a> {
let blob_abs = self.to_abs_path();
let img_wh =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await)
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
MediaQuality::Balanced => BALANCED_AVATAR_SIZE,
@@ -403,7 +403,7 @@ impl<'a> BlobObject<'a> {
}
let img_wh =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await)
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
MediaQuality::Balanced => BALANCED_IMAGE_SIZE,
@@ -514,6 +514,10 @@ pub enum BlobError {
WrongBlobdir { blobdir: PathBuf, src: PathBuf },
#[error("Blob has a badname {}", .blobname.display())]
WrongName { blobname: PathBuf },
#[error("Sql: {0}")]
Sql(#[from] crate::sql::Error),
#[error("{0}")]
Other(#[from] anyhow::Error),
}
#[cfg(test)]

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
//! # Chat list module
use anyhow::{bail, ensure, Result};
use async_std::prelude::*;
use sqlx::Row;
use crate::chat;
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
@@ -110,17 +112,6 @@ impl Chatlist {
let mut add_archived_link_item = false;
let process_row = |row: &rusqlite::Row| {
let chat_id: ChatId = row.get(0)?;
let msg_id: MsgId = row.get(1).unwrap_or_default();
Ok((chat_id, msg_id))
};
let process_rows = |rows: rusqlite::MappedRows<_>| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
};
let skip_id = if flag_for_forwarding {
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
.await
@@ -130,6 +121,13 @@ impl Chatlist {
ChatId::new(0)
};
let process_row = |row: sqlx::Result<sqlx::sqlite::SqliteRow>| {
let row = row?;
let chat_id: ChatId = row.try_get(0)?;
let msg_id: MsgId = row.try_get(1).unwrap_or_default();
Ok((chat_id, msg_id))
};
// select with left join and minimum:
//
// - the inner select must use `hidden` and _not_ `m.hidden`
@@ -145,10 +143,10 @@ impl Chatlist {
// tg do the same) for the deaddrop, however, they should
// really be hidden, however, _currently_ the deaddrop is not
// shown at all permanent in the chatlist.
let mut ids = if let Some(query_contact_id) = query_contact_id {
let mut ids: Vec<_> = if let Some(query_contact_id) = query_contact_id {
// show chats shared with a given contact
context.sql.query_map(
"SELECT c.id, m.id
context.sql.fetch(
sqlx::query("SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
@@ -162,11 +160,9 @@ impl Chatlist {
AND c.blocked=0
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
process_row,
process_rows,
).await?
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;"
).bind(MessageState::OutDraft).bind(query_contact_id).bind(ChatVisibility::Pinned)
).await?.map(process_row).collect::<sqlx::Result<_>>().await?
} else if flag_archived_only {
// show archived chats
// (this includes the archived device-chat; we could skip it,
@@ -174,8 +170,9 @@ impl Chatlist {
// and adapting the number requires larger refactorings and seems not to be worth the effort)
context
.sql
.query_map(
"SELECT c.id, m.id
.fetch(
sqlx::query(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
@@ -190,11 +187,13 @@ impl Chatlist {
AND c.archived=1
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft],
process_row,
process_rows,
)
.bind(MessageState::OutDraft),
)
.await?
.map(process_row)
.collect::<sqlx::Result<_>>()
.await?
} else if let Some(query) = query {
let query = query.trim().to_string();
ensure!(!query.is_empty(), "missing query");
@@ -208,8 +207,9 @@ impl Chatlist {
let str_like_cmd = format!("%{}%", query);
context
.sql
.query_map(
"SELECT c.id, m.id
.fetch(
sqlx::query(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
@@ -224,11 +224,15 @@ impl Chatlist {
AND c.name LIKE ?3
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, skip_id, str_like_cmd],
process_row,
process_rows,
)
.bind(MessageState::OutDraft)
.bind(skip_id)
.bind(str_like_cmd),
)
.await?
.map(process_row)
.collect::<sqlx::Result<_>>()
.await?
} else {
// show normal chatlist
let sort_id_up = if flag_for_forwarding {
@@ -239,7 +243,8 @@ impl Chatlist {
} else {
ChatId::new(0)
};
let mut ids = context.sql.query_map(
let mut ids: Vec<_> = context.sql.fetch(sqlx::query(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
@@ -254,19 +259,21 @@ impl Chatlist {
AND c.blocked=0
AND NOT c.archived=?3
GROUP BY c.id
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
process_row,
process_rows,
).await?;
ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;"
)
.bind(MessageState::OutDraft)
.bind(skip_id)
.bind(ChatVisibility::Archived)
.bind(sort_id_up)
.bind(ChatVisibility::Pinned)
).await?.map(process_row).collect::<sqlx::Result<_>>().await?;
if !flag_no_specials {
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await
if let Some(last_deaddrop_fresh_msg_id) =
get_last_deaddrop_fresh_msg(context).await?
{
if !flag_for_forwarding {
ids.insert(
0,
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
);
ids.insert(0, (DC_CHAT_ID_DEADDROP, last_deaddrop_fresh_msg_id));
}
}
add_archived_link_item = true;
@@ -274,11 +281,11 @@ impl Chatlist {
ids
};
if add_archived_link_item && dc_get_archived_cnt(context).await > 0 {
if add_archived_link_item && dc_get_archived_cnt(context).await? > 0 {
if ids.is_empty() && flag_add_alldone_hint {
ids.push((ChatId::new(DC_CHAT_ID_ALLDONE_HINT), MsgId::new(0)));
ids.push((DC_CHAT_ID_ALLDONE_HINT, MsgId::new(0)));
}
ids.push((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0)));
ids.push((DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0)));
}
Ok(Chatlist { ids })
@@ -400,46 +407,44 @@ impl Chatlist {
}
/// Returns the number of archived chats
pub async fn dc_get_archived_cnt(context: &Context) -> u32 {
context
pub async fn dc_get_archived_cnt(context: &Context) -> Result<usize> {
let count = context
.sql
.query_get_value(
context,
.count(sqlx::query(
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
paramsv![],
)
.await
.unwrap_or_default()
))
.await?;
Ok(count)
}
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Result<Option<MsgId>> {
// We have an index over the state-column, this should be
// sufficient as there are typically only few fresh messages.
context
let id = context
.sql
.query_get_value(
context,
concat!(
"SELECT m.id",
" FROM msgs m",
" LEFT JOIN chats c",
" ON c.id=m.chat_id",
" WHERE m.state=10",
" AND m.hidden=0",
" AND c.blocked=2",
" ORDER BY m.timestamp DESC, m.id DESC;"
),
paramsv![],
)
.await
.query_get_value(sqlx::query(concat!(
"SELECT m.id",
" FROM msgs m",
" LEFT JOIN chats c",
" ON c.id=m.chat_id",
" WHERE m.state=10",
" AND m.hidden=0",
" AND c.blocked=2",
" ORDER BY m.timestamp DESC, m.id DESC;"
)))
.await?;
Ok(id)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::{create_group_chat, ProtectionStatus};
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
use crate::constants::Viewtype;
use crate::dc_receive_imf::dc_receive_imf;
use crate::message;
use crate::message::ContactRequestDecision;
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;
@@ -466,7 +471,7 @@ mod tests {
// drafts are sorted to the top
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("hello".to_string()));
chat_id2.set_draft(&t, Some(&mut msg)).await;
chat_id2.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.get_chat_id(0), chat_id2);
@@ -545,6 +550,136 @@ mod tests {
assert_eq!(chats.len(), 1);
}
#[async_std::test]
async fn test_search_single_chat() -> anyhow::Result<()> {
let t = TestContext::new_alice().await;
// receive a one-to-one-message, accept contact request
dc_receive_imf(
&t,
b"From: Bob Authname <bob@example.org>\n\
To: alice@example.com\n\
Subject: foo\n\
Message-ID: <msg1234@example.org>\n\
Chat-Version: 1.0\n\
Date: Sun, 22 Mar 2021 22:37:57 +0000\n\
\n\
hello foo\n",
"INBOX",
1,
false,
)
.await?;
let chats = Chatlist::try_load(&t, 0, Some("Bob Authname"), None).await?;
assert_eq!(chats.len(), 0);
let msg = t.get_last_msg().await;
assert_eq!(msg.get_chat_id(), DC_CHAT_ID_DEADDROP);
let chat_id =
message::decide_on_contact_request(&t, msg.get_id(), ContactRequestDecision::StartChat)
.await
.unwrap();
let contacts = get_chat_contacts(&t, chat_id).await?;
let contact_id = *contacts.first().unwrap();
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "Bob Authname");
// check, the one-to-one-chat can be found using chatlist search query
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.get_chat_id(0), chat_id);
// change the name of the contact; this also changes the name of the one-to-one-chat
let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
assert_eq!(contact_id, test_id);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "Bob Nickname");
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
assert_eq!(chats.len(), 0);
let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
assert_eq!(chats.len(), 1);
// revert contact to authname, this again changes the name of the one-to-one-chat
let test_id = Contact::create(&t, "", "bob@example.org").await?;
assert_eq!(contact_id, test_id);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "Bob Authname");
let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
assert_eq!(chats.len(), 1);
let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
assert_eq!(chats.len(), 0);
Ok(())
}
#[async_std::test]
async fn test_search_single_chat_without_authname() -> anyhow::Result<()> {
let t = TestContext::new_alice().await;
// receive a one-to-one-message without authname set, accept contact request
dc_receive_imf(
&t,
b"From: bob@example.org\n\
To: alice@example.com\n\
Subject: foo\n\
Message-ID: <msg5678@example.org>\n\
Chat-Version: 1.0\n\
Date: Sun, 22 Mar 2021 22:38:57 +0000\n\
\n\
hello foo\n",
"INBOX",
1,
false,
)
.await?;
let msg = t.get_last_msg().await;
let chat_id =
message::decide_on_contact_request(&t, msg.get_id(), ContactRequestDecision::StartChat)
.await
.unwrap();
let contacts = get_chat_contacts(&t, chat_id).await?;
let contact_id = *contacts.first().unwrap();
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "bob@example.org");
// check, the one-to-one-chat can be found using chatlist search query
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.get_chat_id(0), chat_id);
// change the name of the contact; this also changes the name of the one-to-one-chat
let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
assert_eq!(contact_id, test_id);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "Bob Nickname");
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
assert_eq!(chats.len(), 0); // email-addresses are searchable in contacts, not in chats
let chats = Chatlist::try_load(&t, 0, Some("Bob Nickname"), None).await?;
assert_eq!(chats.len(), 1);
assert_eq!(chats.get_chat_id(0), chat_id);
// revert name change, this again changes the name of the one-to-one-chat to the email-address
let test_id = Contact::create(&t, "", "bob@example.org").await?;
assert_eq!(contact_id, test_id);
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_name(), "bob@example.org");
let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
assert_eq!(chats.len(), 1);
let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
assert_eq!(chats.len(), 0);
// finally, also check that a simple substring-search is working with email-addresses
let chats = Chatlist::try_load(&t, 0, Some("b@exa"), None).await?;
assert_eq!(chats.len(), 1);
let chats = Chatlist::try_load(&t, 0, Some("b@exac"), None).await?;
assert_eq!(chats.len(), 0);
Ok(())
}
#[async_std::test]
async fn test_get_summary_unwrap() {
let t = TestContext::new().await;
@@ -554,7 +689,7 @@ mod tests {
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("foo:\nbar \r\n test".to_string()));
chat_id1.set_draft(&t, Some(&mut msg)).await;
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
let summary = chats.get_summary(&t, 0, None).await;

View File

@@ -16,23 +16,29 @@ fn str_to_angle(s: impl AsRef<str>) -> f64 {
f64::from(checksum) / 65536.0 * 360.0
}
/// Converts an identifier to RGB color.
/// Converts RGB tuple to a 24-bit number.
///
/// Returns a 24-bit number with 8 least significant bits corresponding to the blue color and 8
/// most significant bits corresponding to the red color.
fn rgb_to_u32((r, g, b): (f64, f64, f64)) -> u32 {
let r = ((r * 256.0) as u32).min(255);
let g = ((g * 256.0) as u32).min(255);
let b = ((b * 256.0) as u32).min(255);
65536 * r + 256 * g + b
}
/// Converts an identifier to RGB color.
///
/// Saturation is set to maximum (100.0) to make colors distinguishable, and lightness is set to
/// half (50.0) to make colors suitable both for light and dark theme.
pub(crate) fn str_to_color(s: impl AsRef<str>) -> u32 {
let (r, g, b) = hsluv_to_rgb((str_to_angle(s), 100.0, 50.0));
65536 * (r * 256.0) as u32 + 256 * (g * 256.0) as u32 + (b * 256.0) as u32
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(clippy::float_cmp)]
#[test]
fn test_str_to_angle() {
// Test against test vectors from
@@ -43,4 +49,14 @@ mod tests {
assert!((str_to_angle("council") - 359.994507).abs() < 1e-6);
assert!((str_to_angle("Board") - 171.430664).abs() < 1e-6);
}
#[test]
fn test_rgb_to_u32() {
assert_eq!(rgb_to_u32((0.0, 0.0, 0.0)), 0);
assert_eq!(rgb_to_u32((1.0, 1.0, 1.0)), 0xffffff);
assert_eq!(rgb_to_u32((0.0, 0.0, 1.0)), 0x0000ff);
assert_eq!(rgb_to_u32((0.0, 1.0, 0.0)), 0x00ff00);
assert_eq!(rgb_to_u32((1.0, 0.0, 0.0)), 0xff0000);
assert_eq!(rgb_to_u32((1.0, 0.5, 0.0)), 0xff8000);
}
}

View File

@@ -1,5 +1,6 @@
//! # Key-value configuration management
use anyhow::Result;
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
@@ -150,69 +151,66 @@ pub enum Config {
}
impl Context {
pub async fn config_exists(&self, key: Config) -> bool {
self.sql.get_raw_config(self, key).await.is_some()
pub async fn config_exists(&self, key: Config) -> Result<bool> {
Ok(self.sql.get_raw_config(key).await?.is_some())
}
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
pub async fn get_config(&self, key: Config) -> Option<String> {
pub async fn get_config(&self, key: Config) -> Result<Option<String>> {
let value = match key {
Config::Selfavatar => {
let rel_path = self.sql.get_raw_config(self, key).await;
let rel_path = self.sql.get_raw_config(key).await?;
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
}
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(self, key).await,
_ => self.sql.get_raw_config(key).await?,
};
if value.is_some() {
return value;
return Ok(value);
}
// Default values
match key {
Config::Selfstatus => Some(stock_str::status_line(self).await),
Config::ConfiguredInboxFolder => Some("INBOX".to_owned()),
_ => key.get_str("default").map(|s| s.to_string()),
Config::Selfstatus => Ok(Some(stock_str::status_line(self).await)),
Config::ConfiguredInboxFolder => Ok(Some("INBOX".to_owned())),
_ => Ok(key.get_str("default").map(|s| s.to_string())),
}
}
pub async fn get_config_int(&self, key: Config) -> i32 {
pub async fn get_config_int(&self, key: Config) -> Result<i32> {
self.get_config(key)
.await
.and_then(|s| s.parse().ok())
.unwrap_or_default()
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
}
pub async fn get_config_i64(&self, key: Config) -> i64 {
pub async fn get_config_i64(&self, key: Config) -> Result<i64> {
self.get_config(key)
.await
.and_then(|s| s.parse().ok())
.unwrap_or_default()
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
}
pub async fn get_config_u64(&self, key: Config) -> u64 {
pub async fn get_config_u64(&self, key: Config) -> Result<u64> {
self.get_config(key)
.await
.and_then(|s| s.parse().ok())
.unwrap_or_default()
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
}
pub async fn get_config_bool(&self, key: Config) -> bool {
self.get_config_int(key).await != 0
pub async fn get_config_bool(&self, key: Config) -> Result<bool> {
Ok(self.get_config_int(key).await? != 0)
}
/// Gets configured "delete_server_after" value.
///
/// `None` means never delete the message, `Some(0)` means delete
/// at once, `Some(x)` means delete after `x` seconds.
pub async fn get_config_delete_server_after(&self) -> Option<i64> {
match self.get_config_int(Config::DeleteServerAfter).await {
0 => None,
1 => Some(0),
x => Some(x as i64),
pub async fn get_config_delete_server_after(&self) -> Result<Option<i64>> {
match self.get_config_int(Config::DeleteServerAfter).await? {
0 => Ok(None),
1 => Ok(Some(0)),
x => Ok(Some(x as i64)),
}
}
@@ -220,41 +218,46 @@ impl Context {
///
/// The provider is determined by `get_provider_info()` during configuration and then saved
/// to the db in `param.save_to_database()`, together with all the other `configured_*` values.
pub async fn get_configured_provider(&self) -> Option<&'static Provider> {
get_provider_by_id(&self.get_config(Config::ConfiguredProvider).await?)
pub async fn get_configured_provider(&self) -> Result<Option<&'static Provider>> {
if let Some(cfg) = self.get_config(Config::ConfiguredProvider).await? {
return Ok(get_provider_by_id(&cfg));
}
Ok(None)
}
/// Gets configured "delete_device_after" value.
///
/// `None` means never delete the message, `Some(x)` means delete
/// after `x` seconds.
pub async fn get_config_delete_device_after(&self) -> Option<i64> {
match self.get_config_int(Config::DeleteDeviceAfter).await {
0 => None,
x => Some(x as i64),
pub async fn get_config_delete_device_after(&self) -> Result<Option<i64>> {
match self.get_config_int(Config::DeleteDeviceAfter).await? {
0 => Ok(None),
x => Ok(Some(x as i64)),
}
}
/// Set the given config key.
/// If `None` is passed as a value the value is cleared and set to the default if there is one.
pub async fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
match key {
Config::Selfavatar => {
self.sql
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![])
.execute(sqlx::query("UPDATE contacts SET selfavatar_sent=0;"))
.await?;
self.sql
.set_raw_config_bool(self, "attach_selfavatar", true)
.set_raw_config_bool("attach_selfavatar", true)
.await?;
match value {
Some(value) => {
let blob = BlobObject::new_from_path(&self, value).await?;
let blob = BlobObject::new_from_path(self, value).await?;
blob.recode_to_avatar_size(self).await?;
self.sql
.set_raw_config(self, key, Some(blob.as_name()))
.await
self.sql.set_raw_config(key, Some(blob.as_name())).await?;
Ok(())
}
None => {
self.sql.set_raw_config(key, None).await?;
Ok(())
}
None => self.sql.set_raw_config(self, key, None).await,
}
}
Config::Selfstatus => {
@@ -265,10 +268,15 @@ impl Context {
value
};
self.sql.set_raw_config(self, key, val).await
self.sql.set_raw_config(key, val).await?;
Ok(())
}
Config::DeleteDeviceAfter => {
let ret = self.sql.set_raw_config(self, key, value).await;
let ret = self
.sql
.set_raw_config(key, value)
.await
.map_err(Into::into);
// Force chatlist reload to delete old messages immediately.
self.emit_event(EventType::MsgsChanged {
msg_id: MsgId::new(0),
@@ -278,20 +286,29 @@ impl Context {
}
Config::Displayname => {
let value = value.map(improve_single_line_input);
self.sql.set_raw_config(self, key, value.as_deref()).await
self.sql.set_raw_config(key, value.as_deref()).await?;
Ok(())
}
Config::DeleteServerAfter => {
let ret = self.sql.set_raw_config(self, key, value).await;
let ret = self
.sql
.set_raw_config(key, value)
.await
.map_err(Into::into);
job::schedule_resync(self).await;
ret
}
_ => self.sql.set_raw_config(self, key, value).await,
_ => {
self.sql.set_raw_config(key, value).await?;
Ok(())
}
}
}
pub async fn set_config_bool(&self, key: Config, value: bool) -> crate::sql::Result<()> {
self.set_config(key, if value { Some("1") } else { None })
.await
.await?;
Ok(())
}
}
@@ -344,12 +361,12 @@ mod tests {
.unwrap();
let avatar_blob = t.get_blobdir().join("avatar.jpg");
assert!(!avatar_blob.exists().await);
t.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
.unwrap();
assert!(avatar_blob.exists().await);
assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64);
let avatar_cfg = t.get_config(Config::Selfavatar).await;
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
let img = image::open(avatar_src).unwrap();
@@ -375,10 +392,10 @@ mod tests {
assert_eq!(img.width(), 900);
assert_eq!(img.height(), 900);
t.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
.unwrap();
let avatar_cfg = t.get_config(Config::Selfavatar).await;
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
assert_eq!(avatar_cfg, avatar_src.to_str().map(|s| s.to_string()));
let img = image::open(avatar_src).unwrap();
@@ -397,7 +414,7 @@ mod tests {
.unwrap();
let avatar_blob = t.get_blobdir().join("avatar.png");
assert!(!avatar_blob.exists().await);
t.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
.unwrap();
assert!(avatar_blob.exists().await);
@@ -405,21 +422,21 @@ mod tests {
std::fs::metadata(&avatar_blob).unwrap().len(),
avatar_bytes.len() as u64
);
let avatar_cfg = t.get_config(Config::Selfavatar).await;
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
}
#[async_std::test]
async fn test_media_quality_config_option() {
let t = TestContext::new().await;
let media_quality = t.get_config_int(Config::MediaQuality).await;
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
assert_eq!(media_quality, 0);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
assert_eq!(media_quality, constants::MediaQuality::Balanced);
t.set_config(Config::MediaQuality, Some("1")).await.unwrap();
let media_quality = t.get_config_int(Config::MediaQuality).await;
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
assert_eq!(media_quality, 1);
assert_eq!(constants::MediaQuality::Worse as i32, 1);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();

View File

@@ -115,8 +115,8 @@ fn parse_server<B: BufRead>(
MozConfigTag::Username => username = Some(val),
MozConfigTag::Sockettype => {
sockettype = match val.to_lowercase().as_ref() {
"ssl" => Socket::SSL,
"starttls" => Socket::STARTTLS,
"ssl" => Socket::Ssl,
"starttls" => Socket::Starttls,
"plain" => Socket::Plain,
_ => Socket::Automatic,
}
@@ -233,8 +233,8 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerPar
.chain(moz_ac.outgoing_servers.into_iter())
.filter_map(|server| {
let protocol = match server.typ.as_ref() {
"imap" => Some(Protocol::IMAP),
"smtp" => Some(Protocol::SMTP),
"imap" => Some(Protocol::Imap),
"smtp" => Some(Protocol::Smtp),
_ => None,
};
Some(ServerParams {
@@ -276,10 +276,10 @@ mod tests {
fn test_parse_outlook_autoconfig() {
let xml_raw = include_str!("../../test-data/autoconfig/outlook.com.xml");
let res = parse_serverparams("example@outlook.com", xml_raw).expect("XML parsing failed");
assert_eq!(res[0].protocol, Protocol::IMAP);
assert_eq!(res[0].protocol, Protocol::Imap);
assert_eq!(res[0].hostname, "outlook.office365.com");
assert_eq!(res[0].port, 993);
assert_eq!(res[1].protocol, Protocol::SMTP);
assert_eq!(res[1].protocol, Protocol::Smtp);
assert_eq!(res[1].hostname, "smtp.office365.com");
assert_eq!(res[1].port, 587);
}
@@ -295,25 +295,25 @@ mod tests {
assert_eq!(res.incoming_servers[0].typ, "imap");
assert_eq!(res.incoming_servers[0].hostname, "mail.lakenet.ch");
assert_eq!(res.incoming_servers[0].port, 993);
assert_eq!(res.incoming_servers[0].sockettype, Socket::SSL);
assert_eq!(res.incoming_servers[0].sockettype, Socket::Ssl);
assert_eq!(res.incoming_servers[0].username, "example@lakenet.ch");
assert_eq!(res.incoming_servers[1].typ, "imap");
assert_eq!(res.incoming_servers[1].hostname, "mail.lakenet.ch");
assert_eq!(res.incoming_servers[1].port, 143);
assert_eq!(res.incoming_servers[1].sockettype, Socket::STARTTLS);
assert_eq!(res.incoming_servers[1].sockettype, Socket::Starttls);
assert_eq!(res.incoming_servers[1].username, "example@lakenet.ch");
assert_eq!(res.incoming_servers[2].typ, "pop3");
assert_eq!(res.incoming_servers[2].hostname, "mail.lakenet.ch");
assert_eq!(res.incoming_servers[2].port, 995);
assert_eq!(res.incoming_servers[2].sockettype, Socket::SSL);
assert_eq!(res.incoming_servers[2].sockettype, Socket::Ssl);
assert_eq!(res.incoming_servers[2].username, "example@lakenet.ch");
assert_eq!(res.incoming_servers[3].typ, "pop3");
assert_eq!(res.incoming_servers[3].hostname, "mail.lakenet.ch");
assert_eq!(res.incoming_servers[3].port, 110);
assert_eq!(res.incoming_servers[3].sockettype, Socket::STARTTLS);
assert_eq!(res.incoming_servers[3].sockettype, Socket::Starttls);
assert_eq!(res.incoming_servers[3].username, "example@lakenet.ch");
assert_eq!(res.outgoing_servers.len(), 1);
@@ -321,7 +321,7 @@ mod tests {
assert_eq!(res.outgoing_servers[0].typ, "smtp");
assert_eq!(res.outgoing_servers[0].hostname, "mail.lakenet.ch");
assert_eq!(res.outgoing_servers[0].port, 587);
assert_eq!(res.outgoing_servers[0].sockettype, Socket::STARTTLS);
assert_eq!(res.outgoing_servers[0].sockettype, Socket::Starttls);
assert_eq!(res.outgoing_servers[0].username, "example@lakenet.ch");
}
}

View File

@@ -73,7 +73,7 @@ fn parse_protocol<B: BufRead>(
}
}
Event::Text(ref e) => {
let val = e.unescape_and_decode(&reader).unwrap_or_default();
let val = e.unescape_and_decode(reader).unwrap_or_default();
if let Some(ref tag) = current_tag {
match tag.as_str() {
@@ -117,7 +117,7 @@ fn parse_redirecturl<B: BufRead>(
let mut buf = Vec::new();
match reader.read_event(&mut buf)? {
Event::Text(ref e) => {
let val = e.unescape_and_decode(&reader).unwrap_or_default();
let val = e.unescape_and_decode(reader).unwrap_or_default();
Ok(val.trim().to_string())
}
_ => Ok("".to_string()),
@@ -154,7 +154,7 @@ fn parse_xml_reader<B: BufRead>(
}
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
let mut reader = quick_xml::Reader::from_str(&xml_raw);
let mut reader = quick_xml::Reader::from_str(xml_raw);
reader.trim_text(true);
parse_xml_reader(&mut reader).map_err(|error| Error::InvalidXml {
@@ -169,8 +169,8 @@ fn protocols_to_serverparams(protocols: Vec<ProtocolTag>) -> Vec<ServerParams> {
.filter_map(|protocol| {
Some(ServerParams {
protocol: match protocol.typ.to_lowercase().as_ref() {
"imap" => Some(Protocol::IMAP),
"smtp" => Some(Protocol::SMTP),
"imap" => Some(Protocol::Imap),
"smtp" => Some(Protocol::Smtp),
_ => None,
}?,
socket: match protocol.ssl {

View File

@@ -50,8 +50,11 @@ macro_rules! progress {
impl Context {
/// Checks if the context is already configured.
pub async fn is_configured(&self) -> bool {
self.sql.get_raw_config_bool(self, "configured").await
pub async fn is_configured(&self) -> Result<bool> {
self.sql
.get_raw_config_bool("configured")
.await
.map_err(Into::into)
}
/// Configures this account with the currently set parameters.
@@ -84,14 +87,14 @@ impl Context {
async fn inner_configure(&self) -> Result<()> {
info!(self, "Configure ...");
let mut param = LoginParam::from_database(self, "").await;
let mut param = LoginParam::from_database(self, "").await?;
let success = configure(self, &mut param).await;
self.set_config(Config::NotifyAboutWrongPw, None).await?;
if let Some(provider) = param.provider {
if let Some(config_defaults) = &provider.config_defaults {
for def in config_defaults.iter() {
if !self.config_exists(def.key).await {
if !self.config_exists(def.key).await? {
info!(self, "apply config_defaults {}={}", def.key, def.value);
self.set_config(def.key, Some(def.value)).await?;
} else {
@@ -177,13 +180,13 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
progress!(ctx, 10);
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, &param.addr, &param.imap.password)
.await
.await?
.and_then(|e| e.parse().ok())
{
info!(ctx, "Authorized address is {}", oauth2_addr);
param.addr = oauth2_addr;
ctx.sql
.set_raw_config(ctx, "addr", Some(param.addr.as_str()))
.set_raw_config("addr", Some(param.addr.as_str()))
.await?;
}
progress!(ctx, 20);
@@ -217,7 +220,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
if let Some(provider) = provider::get_provider_info(&param_domain).await {
param.provider = Some(provider);
match provider.status {
provider::Status::OK | provider::Status::PREPARATION => {
provider::Status::Ok | provider::Status::Preparation => {
if provider.server.is_empty() {
info!(ctx, "offline autoconfig found, but no servers defined");
param_autoconfig = None;
@@ -232,8 +235,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
hostname: s.hostname.to_string(),
port: s.port,
username: match s.username_pattern {
UsernamePattern::EMAIL => param.addr.to_string(),
UsernamePattern::EMAILLOCALPART => {
UsernamePattern::Email => param.addr.to_string(),
UsernamePattern::Emaillocalpart => {
if let Some(at) = param.addr.find('@') {
param.addr.split_at(at).0.to_string()
} else {
@@ -247,7 +250,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
param_autoconfig = Some(servers)
}
}
provider::Status::BROKEN => {
provider::Status::Broken => {
info!(ctx, "offline autoconfig found, provider is broken");
param_autoconfig = None;
}
@@ -266,10 +269,10 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let mut servers = param_autoconfig.unwrap_or_default();
if !servers
.iter()
.any(|server| server.protocol == Protocol::IMAP)
.any(|server| server.protocol == Protocol::Imap)
{
servers.push(ServerParams {
protocol: Protocol::IMAP,
protocol: Protocol::Imap,
hostname: param.imap.server.clone(),
port: param.imap.port,
socket: param.imap.security,
@@ -278,10 +281,10 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
}
if !servers
.iter()
.any(|server| server.protocol == Protocol::SMTP)
.any(|server| server.protocol == Protocol::Smtp)
{
servers.push(ServerParams {
protocol: Protocol::SMTP,
protocol: Protocol::Smtp,
hostname: param.smtp.server.clone(),
port: param.smtp.port,
socket: param.smtp.security,
@@ -300,7 +303,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let smtp_addr = param.addr.clone();
let smtp_servers: Vec<ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::SMTP)
.filter(|params| params.protocol == Protocol::Smtp)
.cloned()
.collect();
let provider_strict_tls = param.provider.map_or(false, |provider| provider.strict_tls);
@@ -348,7 +351,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let mut imap_configured = false;
let imap_servers: Vec<&ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::IMAP)
.filter(|params| params.protocol == Protocol::Imap)
.collect();
let imap_servers_count = imap_servers.len();
let mut errors = Vec::new();
@@ -397,8 +400,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 900);
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await
|| ctx.get_config_bool(Config::MvboxMove).await;
let create_mvbox = ctx.get_config_bool(Config::MvboxWatch).await?
|| ctx.get_config_bool(Config::MvboxMove).await?;
imap.configure_folders(ctx, create_mvbox).await?;
@@ -413,7 +416,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// "configured_" prefix; also write the "configured"-flag */
// the trailing underscore is correct
param.save_to_database(ctx, "configured_").await?;
ctx.sql.set_raw_config_bool(ctx, "configured", true).await?;
ctx.sql.set_raw_config_bool("configured", true).await?;
ctx.set_config(Config::ConfiguredTimestamp, Some(&time().to_string()))
.await?;
@@ -450,7 +453,7 @@ async fn get_autoconfig(
"https://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}",
param_domain, param_addr_urlencoded
),
&param,
param,
)
.await
{
@@ -465,7 +468,7 @@ async fn get_autoconfig(
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
&param_domain, &param_addr_urlencoded
),
&param,
param,
)
.await
{
@@ -501,7 +504,7 @@ async fn get_autoconfig(
if let Ok(res) = moz_autoconfigure(
ctx,
format!("https://autoconfig.thunderbird.net/v1.1/{}", &param_domain),
&param,
param,
)
.await
{

View File

@@ -25,7 +25,7 @@ pub(crate) struct ServerParams {
}
impl ServerParams {
pub(crate) fn expand_usernames(mut self, addr: &str) -> Vec<ServerParams> {
fn expand_usernames(mut self, addr: &str) -> Vec<ServerParams> {
let mut res = Vec::new();
if self.username.is_empty() {
@@ -42,15 +42,15 @@ impl ServerParams {
res
}
pub(crate) fn expand_hostnames(mut self, param_domain: &str) -> Vec<ServerParams> {
fn expand_hostnames(mut self, param_domain: &str) -> Vec<ServerParams> {
let mut res = Vec::new();
if self.hostname.is_empty() {
self.hostname = param_domain.to_string();
res.push(self.clone());
self.hostname = match self.protocol {
Protocol::IMAP => "imap.".to_string() + param_domain,
Protocol::SMTP => "smtp.".to_string() + param_domain,
Protocol::Imap => "imap.".to_string() + param_domain,
Protocol::Smtp => "smtp.".to_string() + param_domain,
};
res.push(self.clone());
@@ -62,17 +62,17 @@ impl ServerParams {
res
}
pub(crate) fn expand_ports(mut self) -> Vec<ServerParams> {
fn expand_ports(mut self) -> Vec<ServerParams> {
// Try to infer port from socket security.
if self.port == 0 {
self.port = match self.socket {
Socket::SSL => match self.protocol {
Protocol::IMAP => 993,
Protocol::SMTP => 465,
Socket::Ssl => match self.protocol {
Protocol::Imap => 993,
Protocol::Smtp => 465,
},
Socket::STARTTLS | Socket::Plain => match self.protocol {
Protocol::IMAP => 143,
Protocol::SMTP => 587,
Socket::Starttls | Socket::Plain => match self.protocol {
Protocol::Imap => 143,
Protocol::Smtp => 587,
},
Socket::Automatic => 0,
}
@@ -85,27 +85,27 @@ impl ServerParams {
// Try common secure combinations.
// Try STARTTLS
self.socket = Socket::STARTTLS;
self.socket = Socket::Starttls;
self.port = match self.protocol {
Protocol::IMAP => 143,
Protocol::SMTP => 587,
Protocol::Imap => 143,
Protocol::Smtp => 587,
};
res.push(self.clone());
// Try TLS
self.socket = Socket::SSL;
self.socket = Socket::Ssl;
self.port = match self.protocol {
Protocol::IMAP => 993,
Protocol::SMTP => 465,
Protocol::Imap => 993,
Protocol::Smtp => 465,
};
res.push(self);
} else if self.socket == Socket::Automatic {
// Try TLS over user-provided port.
self.socket = Socket::SSL;
self.socket = Socket::Ssl;
res.push(self.clone());
// Try STARTTLS over user-provided port.
self.socket = Socket::STARTTLS;
self.socket = Socket::Starttls;
res.push(self);
} else {
res.push(self);
@@ -140,10 +140,10 @@ mod tests {
fn test_expand_param_vector() {
let v = expand_param_vector(
vec![ServerParams {
protocol: Protocol::IMAP,
protocol: Protocol::Imap,
hostname: "example.net".to_string(),
port: 0,
socket: Socket::SSL,
socket: Socket::Ssl,
username: "foobar".to_string(),
}],
"foobar@example.net",
@@ -153,12 +153,44 @@ mod tests {
assert_eq!(
v,
vec![ServerParams {
protocol: Protocol::IMAP,
protocol: Protocol::Imap,
hostname: "example.net".to_string(),
port: 993,
socket: Socket::SSL,
socket: Socket::Ssl,
username: "foobar".to_string(),
}],
);
let v = expand_param_vector(
vec![ServerParams {
protocol: Protocol::Smtp,
hostname: "example.net".to_string(),
port: 123,
socket: Socket::Automatic,
username: "foobar".to_string(),
}],
"foobar@example.net",
"example.net",
);
assert_eq!(
v,
vec![
ServerParams {
protocol: Protocol::Smtp,
hostname: "example.net".to_string(),
port: 123,
socket: Socket::Ssl,
username: "foobar".to_string()
},
ServerParams {
protocol: Protocol::Smtp,
hostname: "example.net".to_string(),
port: 123,
socket: Socket::Starttls,
username: "foobar".to_string()
}
],
);
}
}

View File

@@ -1,8 +1,9 @@
//! # Constants
use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use crate::chat::ChatId;
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
#[derive(
@@ -14,12 +15,11 @@ pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION")
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
sqlx::Type,
)]
#[repr(u8)]
#[repr(i8)]
pub enum Blocked {
Not = 0,
Manually = 1,
@@ -32,7 +32,7 @@ impl Default for Blocked {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum ShowEmails {
Off = 0,
@@ -46,7 +46,7 @@ impl Default for ShowEmails {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum MediaQuality {
Balanced = 0,
@@ -59,7 +59,7 @@ impl Default for MediaQuality {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum KeyGenType {
Default = 0,
@@ -73,7 +73,7 @@ impl Default for KeyGenType {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(i8)]
pub enum VideochatType {
Unknown = 0,
@@ -101,8 +101,8 @@ pub const DC_GCL_FOR_FORWARDING: usize = 0x08;
pub const DC_GCM_ADDDAYMARKER: u32 = 0x01;
pub const DC_GCM_INFO_ONLY: u32 = 0x02;
pub const DC_GCL_VERIFIED_ONLY: usize = 0x01;
pub const DC_GCL_ADD_SELF: usize = 0x02;
pub const DC_GCL_VERIFIED_ONLY: u32 = 0x01;
pub const DC_GCL_ADD_SELF: u32 = 0x02;
// unchanged user avatars are resent to the recipients every some days
pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
@@ -114,15 +114,15 @@ pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
pub const DC_OUTDATED_WARNING_DAYS: i64 = 365;
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
pub const DC_CHAT_ID_DEADDROP: u32 = 1;
pub const DC_CHAT_ID_DEADDROP: ChatId = ChatId::new(1);
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
pub const DC_CHAT_ID_TRASH: u32 = 3;
pub const DC_CHAT_ID_TRASH: ChatId = ChatId::new(3);
/// only an indicator in a chatlist
pub const DC_CHAT_ID_ARCHIVED_LINK: u32 = 6;
pub const DC_CHAT_ID_ARCHIVED_LINK: ChatId = ChatId::new(6);
/// only an indicator in a chatlist
pub const DC_CHAT_ID_ALLDONE_HINT: u32 = 7;
pub const DC_CHAT_ID_ALLDONE_HINT: ChatId = ChatId::new(7);
/// larger chat IDs are "real" chats, their messages are "real" messages.
pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
#[derive(
Debug,
@@ -133,11 +133,10 @@ pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
IntoStaticStr,
Serialize,
Deserialize,
sqlx::Type,
)]
#[repr(u32)]
pub enum Chattype {
@@ -157,8 +156,34 @@ pub const DC_MSG_ID_MARKER1: u32 = 1;
pub const DC_MSG_ID_DAYMARKER: u32 = 9;
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
/// approx. max. length returned by dc_msg_get_text()
pub const DC_MAX_GET_TEXT_LEN: usize = 30000;
/// string that indicates sth. is left out or truncated
pub const DC_ELLIPSE: &str = "[...]";
/// to keep bubbles and chat flow usable,
/// and to avoid problems with controls using very long texts,
/// we limit the text length to DC_DESIRED_TEXT_LEN.
/// if the text is longer, the full text can be retrieved usind has_html()/get_html().
///
/// we are using a bit less than DC_MAX_GET_TEXT_LEN to avoid cutting twice
/// (a bit less as truncation may not be exact and ellipses may be added).
///
/// note, that DC_DESIRED_TEXT_LEN and DC_MAX_GET_TEXT_LEN
/// define max. number of bytes, _not_ unicode graphemes.
/// in general, that seems to be okay for such an upper limit,
/// esp. as calculating the number of graphemes is not simple
/// (one graphemes may be a sequence of code points which is a sequence of bytes).
/// also even if we have the exact number of graphemes,
/// that would not always help on getting an idea about the screen space used
/// (to keep bubbles and chat flow usable).
///
/// therefore, the number of bytes is only a very rough estimation,
/// however, the ~30K seems to work okayish for a while,
/// if it turns out, it is too few for some alphabet, we can still increase.
pub const DC_DESIRED_TEXT_LEN: usize = 29_000;
/// approx. max. length (number of bytes) returned by dc_msg_get_text()
pub const DC_MAX_GET_TEXT_LEN: usize = 30_000;
/// approx. max. length returned by dc_get_msg_info()
pub const DC_MAX_GET_INFO_LEN: usize = 100_000;
@@ -222,12 +247,11 @@ pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
sqlx::Type,
)]
#[repr(i32)]
#[repr(u32)]
pub enum Viewtype {
Unknown = 0,
@@ -286,16 +310,6 @@ impl Default for Viewtype {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derive_display_works_as_expected() {
assert_eq!(format!("{}", Viewtype::Audio), "Audio");
}
}
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
@@ -304,3 +318,100 @@ pub enum KeyType {
Public = 0,
Private = 1,
}
#[cfg(test)]
mod tests {
use super::*;
use num_traits::FromPrimitive;
#[test]
fn test_derive_display_works_as_expected() {
assert_eq!(format!("{}", Viewtype::Audio), "Audio");
}
#[test]
fn test_viewtype_values() {
// values may be written to disk and must not change
assert_eq!(Viewtype::Unknown, Viewtype::default());
assert_eq!(Viewtype::Unknown, Viewtype::from_i32(0).unwrap());
assert_eq!(Viewtype::Text, Viewtype::from_i32(10).unwrap());
assert_eq!(Viewtype::Image, Viewtype::from_i32(20).unwrap());
assert_eq!(Viewtype::Gif, Viewtype::from_i32(21).unwrap());
assert_eq!(Viewtype::Sticker, Viewtype::from_i32(23).unwrap());
assert_eq!(Viewtype::Audio, Viewtype::from_i32(40).unwrap());
assert_eq!(Viewtype::Voice, Viewtype::from_i32(41).unwrap());
assert_eq!(Viewtype::Video, Viewtype::from_i32(50).unwrap());
assert_eq!(Viewtype::File, Viewtype::from_i32(60).unwrap());
assert_eq!(
Viewtype::VideochatInvitation,
Viewtype::from_i32(70).unwrap()
);
}
#[test]
fn test_chattype_values() {
// values may be written to disk and must not change
assert_eq!(Chattype::Undefined, Chattype::default());
assert_eq!(Chattype::Undefined, Chattype::from_i32(0).unwrap());
assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap());
assert_eq!(Chattype::Group, Chattype::from_i32(120).unwrap());
assert_eq!(Chattype::Mailinglist, Chattype::from_i32(140).unwrap());
}
#[test]
fn test_keygentype_values() {
// values may be written to disk and must not change
assert_eq!(KeyGenType::Default, KeyGenType::default());
assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap());
assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap());
assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap());
}
#[test]
fn test_keytype_values() {
// values may be written to disk and must not change
assert_eq!(KeyType::Public, KeyType::from_i32(0).unwrap());
assert_eq!(KeyType::Private, KeyType::from_i32(1).unwrap());
}
#[test]
fn test_showemails_values() {
// values may be written to disk and must not change
assert_eq!(ShowEmails::Off, ShowEmails::default());
assert_eq!(ShowEmails::Off, ShowEmails::from_i32(0).unwrap());
assert_eq!(
ShowEmails::AcceptedContacts,
ShowEmails::from_i32(1).unwrap()
);
assert_eq!(ShowEmails::All, ShowEmails::from_i32(2).unwrap());
}
#[test]
fn test_blocked_values() {
// values may be written to disk and must not change
assert_eq!(Blocked::Not, Blocked::default());
assert_eq!(Blocked::Not, Blocked::from_i32(0).unwrap());
assert_eq!(Blocked::Manually, Blocked::from_i32(1).unwrap());
assert_eq!(Blocked::Deaddrop, Blocked::from_i32(2).unwrap());
}
#[test]
fn test_mediaquality_values() {
// values may be written to disk and must not change
assert_eq!(MediaQuality::Balanced, MediaQuality::default());
assert_eq!(MediaQuality::Balanced, MediaQuality::from_i32(0).unwrap());
assert_eq!(MediaQuality::Worse, MediaQuality::from_i32(1).unwrap());
}
#[test]
fn test_videochattype_values() {
// values may be written to disk and must not change
assert_eq!(VideochatType::Unknown, VideochatType::default());
assert_eq!(VideochatType::Unknown, VideochatType::from_i32(0).unwrap());
assert_eq!(
VideochatType::BasicWebrtc,
VideochatType::from_i32(1).unwrap()
);
assert_eq!(VideochatType::Jitsi, VideochatType::from_i32(2).unwrap());
}
}

View File

@@ -1,22 +1,24 @@
//! Contacts module
use std::convert::TryFrom;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use anyhow::{bail, ensure, format_err, Result};
use async_std::path::PathBuf;
use deltachat_derive::{FromSql, ToSql};
use async_std::prelude::*;
use itertools::Itertools;
use once_cell::sync::Lazy;
use regex::Regex;
use sqlx::Row;
use crate::aheader::EncryptPreference;
use crate::chat::ChatId;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
Chattype, DC_CHAT_ID_DEADDROP, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_DEVICE_ADDR,
Blocked, Chattype, DC_CHAT_ID_DEADDROP, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_DEVICE_ADDR,
DC_CONTACT_ID_LAST_SPECIAL, DC_CONTACT_ID_SELF, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY,
};
use crate::context::Context;
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input, listflags_has, EmailAddress};
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input, EmailAddress};
use crate::events::EventType;
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
@@ -24,7 +26,7 @@ use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::stock_str;
use crate::{chat, stock_str};
/// An object representing a single contact in memory.
///
@@ -77,12 +79,15 @@ pub struct Contact {
/// Possible origins of a contact.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, FromSql, ToSql,
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, sqlx::Type,
)]
#[repr(i32)]
#[repr(u32)]
pub enum Origin {
Unknown = 0,
/// The contact is a mailing list address, needed to unblock mailing lists
MailinglistAddress = 0x2,
/// Hidden on purpose, e.g. addresses with the word "noreply" in it
Hidden = 0x8,
@@ -171,43 +176,45 @@ pub enum VerifiedStatus {
impl Contact {
pub async fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result<Self> {
let mut res = context
let row = context
.sql
.query_row(
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status
.fetch_one(
sqlx::query(
"SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status
FROM contacts c
WHERE c.id=?;",
paramsv![contact_id as i32],
|row| {
let contact = Self {
id: contact_id,
name: row.get::<_, String>(0)?,
authname: row.get::<_, String>(4)?,
addr: row.get::<_, String>(1)?,
blocked: row.get::<_, Option<i32>>(3)?.unwrap_or_default() != 0,
origin: row.get(2)?,
param: row.get::<_, String>(5)?.parse().unwrap_or_default(),
status: row.get(6).unwrap_or_default(),
};
Ok(contact)
},
)
.bind(contact_id),
)
.await?;
let mut contact = Contact {
id: contact_id,
name: row.try_get(0)?,
authname: row.try_get(4)?,
addr: row.try_get(1)?,
blocked: row.try_get::<Option<i32>, _>(3)?.unwrap_or_default() != 0,
origin: row.try_get(2)?,
param: row.try_get::<String, _>(5)?.parse().unwrap_or_default(),
status: row.try_get::<Option<String>, _>(6)?.unwrap_or_default(),
};
if contact_id == DC_CONTACT_ID_SELF {
res.name = stock_str::self_msg(context).await;
res.addr = context
contact.name = stock_str::self_msg(context).await;
contact.addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.unwrap_or_default();
res.status = context
contact.status = context
.get_config(Config::Selfstatus)
.await
.await?
.unwrap_or_default();
} else if contact_id == DC_CONTACT_ID_DEVICE {
res.name = stock_str::device_messages(context).await;
res.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
contact.name = stock_str::device_messages(context).await;
contact.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string();
}
Ok(res)
Ok(contact)
}
/// Returns `true` if this contact is blocked.
@@ -278,13 +285,15 @@ impl Contact {
if context
.sql
.execute(
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh],
sqlx::query("UPDATE msgs SET state=? WHERE from_id=? AND state=?;")
.bind(MessageState::InNoticed)
.bind(id as i32)
.bind(MessageState::InFresh),
)
.await
.is_ok()
{
context.emit_event(EventType::MsgsNoticed(ChatId::new(DC_CHAT_ID_DEADDROP)));
context.emit_event(EventType::MsgsNoticed(DC_CHAT_ID_DEADDROP));
}
}
@@ -305,21 +314,27 @@ impl Contact {
let addr_normalized = addr_normalize(addr.as_ref());
if let Some(addr_self) = context.get_config(Config::ConfiguredAddr).await {
if let Some(addr_self) = context.get_config(Config::ConfiguredAddr).await? {
if addr_cmp(addr_normalized, addr_self) {
return Ok(Some(DC_CONTACT_ID_SELF));
}
}
context.sql.query_get_value_result(
"SELECT id FROM contacts WHERE addr=?1 COLLATE NOCASE AND id>?2 AND origin>=?3 AND blocked=0;",
paramsv![
addr_normalized,
DC_CONTACT_ID_LAST_SPECIAL as i32,
min_origin as u32,
],
)
.await
.context("lookup_id_by_addr: SQL query failed")
let id = context
.sql
.query_get_value(
sqlx::query(
"SELECT id FROM contacts \
WHERE addr=?1 COLLATE NOCASE \
AND id>?2 AND origin>=?3 AND blocked=0;",
)
.bind(addr_normalized)
.bind(DC_CONTACT_ID_LAST_SPECIAL)
.bind(min_origin),
)
.await?
.unwrap_or_default();
Ok(id)
}
/// Lookup a contact and create it if it does not exist yet.
@@ -364,7 +379,7 @@ impl Contact {
let addr = addr_normalize(addr.as_ref()).to_string();
let addr_self = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.unwrap_or_default();
if addr_cmp(&addr, addr_self) {
@@ -416,25 +431,33 @@ impl Contact {
let mut update_addr = false;
let mut row_id = 0;
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context.sql.query_row(
"SELECT id, name, addr, origin, authname FROM contacts WHERE addr=? COLLATE NOCASE;",
paramsv![addr.to_string()],
|row| {
let row_id = row.get(0)?;
let row_name: String = row.get(1)?;
let row_addr: String = row.get(2)?;
let row_origin: Origin = row.get(3)?;
let row_authname: String = row.get(4)?;
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context
.sql
.fetch_one(
sqlx::query(
"SELECT id, name, addr, origin, authname \
FROM contacts WHERE addr=? COLLATE NOCASE;",
)
.bind(addr.to_string()),
)
.await
.and_then(|row| {
let row_id = row.try_get(0)?;
let row_name: String = row.try_get(1)?;
let row_addr: String = row.try_get(2)?;
let row_origin: Origin = row.try_get(3)?;
let row_authname: String = row.try_get(4)?;
Ok((row_id, row_name, row_addr, row_origin, row_authname))
},
)
.await {
})
{
let update_name = manual && name != row_name;
let update_authname =
!manual && name != row_authname && !name.is_empty() &&
(origin >= row_origin || origin == Origin::IncomingUnknownFrom || row_authname.is_empty());
let update_authname = !manual
&& name != row_authname
&& !name.is_empty()
&& (origin >= row_origin
|| origin == Origin::IncomingUnknownFrom
|| row_authname.is_empty());
row_id = id;
if origin as i32 >= row_origin as i32 && addr != row_addr {
update_addr = true;
@@ -446,43 +469,57 @@ impl Contact {
row_name
};
context
.sql
.execute(
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
paramsv![
new_name,
if update_addr { addr.to_string() } else { row_addr },
if origin > row_origin {
origin
} else {
row_origin
},
if update_authname {
name.to_string()
} else {
row_authname
},
row_id
],
)
.await
.ok();
let query = sqlx::query(
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
)
.bind(&new_name)
.bind(if update_addr {
addr.to_string()
} else {
row_addr
})
.bind(if origin > row_origin {
origin
} else {
row_origin
})
.bind(if update_authname {
name.to_string()
} else {
row_authname
})
.bind(row_id);
context.sql.execute(query).await.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.
let chat_id = context.sql.query_get_value::<i32>(
context,
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
paramsv![Chattype::Single, row_id]
).await;
let chat_id = context.sql.query_get_value::<_, u32>(
sqlx::query(
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)"
).bind(Chattype::Single).bind(row_id)
).await?;
if let Some(chat_id) = chat_id {
match context.sql.execute("UPDATE chats SET name=? WHERE id=? AND name!=?1", paramsv![new_name, chat_id]).await {
let contact = Contact::get_by_id(context, row_id as u32).await?;
let chat_name = contact.get_display_name();
match context
.sql
.execute(
sqlx::query("UPDATE chats SET name=?1 WHERE id=?2 AND name!=?3")
.bind(&chat_name)
.bind(chat_id)
.bind(&chat_name),
)
.await
{
Err(err) => warn!(context, "Can't update chat name: {}", err),
Ok(count) => if count > 0 {
// Chat name updated
context.emit_event(EventType::ChatModified(ChatId::new(chat_id as u32)));
Ok(count) => {
if count > 0 {
// Chat name updated
context
.emit_event(EventType::ChatModified(ChatId::new(chat_id)));
}
}
}
}
@@ -493,24 +530,28 @@ impl Contact {
let update_name = manual;
let update_authname = !manual;
if context
if let Ok(new_row_id) = context
.sql
.execute(
"INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);",
paramsv![
if update_name { name.to_string() } else { "".to_string() },
addr,
origin,
if update_authname { name.to_string() } else { "".to_string() }
],
.insert(
sqlx::query(
"INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);",
)
.bind(if update_name {
name.to_string()
} else {
"".to_string()
})
.bind(&addr)
.bind(origin)
.bind(if update_authname {
name.to_string()
} else {
"".to_string()
}),
)
.await
.is_ok()
{
row_id = context
.sql
.get_rowid(context, "contacts", "addr", &addr)
.await?;
row_id = new_row_id;
sth_modified = Modifier::Created;
info!(context, "added contact id={} addr={}", row_id, &addr);
} else {
@@ -518,7 +559,7 @@ impl Contact {
}
}
Ok((row_id, sth_modified))
Ok((u32::try_from(row_id)?, sth_modified))
}
/// Add a number of contacts.
@@ -581,13 +622,13 @@ impl Contact {
) -> Result<Vec<u32>> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.unwrap_or_default();
let mut add_self = false;
let mut ret = Vec::new();
let flag_verified_only = listflags_has(listflags, DC_GCL_VERIFIED_ONLY);
let flag_add_self = listflags_has(listflags, DC_GCL_ADD_SELF);
let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0;
let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0;
if flag_verified_only || query.is_some() {
let s3str_like_cmd = format!(
@@ -597,10 +638,12 @@ impl Contact {
.map(|s| s.as_ref().to_string())
.unwrap_or_default()
);
context
let mut rows = context
.sql
.query_map(
"SELECT c.id FROM contacts c \
.fetch(
sqlx::query(
"SELECT c.id FROM contacts c \
LEFT JOIN acpeerstates ps ON c.addr=ps.addr \
WHERE c.addr!=?1 \
AND c.id>?2 \
@@ -609,27 +652,23 @@ impl Contact {
AND (iif(c.name='',c.authname,c.name) LIKE ?4 OR c.addr LIKE ?5) \
AND (1=?6 OR LENGTH(ps.verified_key_fingerprint)!=0) \
ORDER BY LOWER(iif(c.name='',c.authname,c.name)||c.addr),c.id;",
paramsv![
self_addr,
DC_CONTACT_ID_LAST_SPECIAL as i32,
Origin::IncomingReplyTo,
s3str_like_cmd,
s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 },
],
|row| row.get::<_, i32>(0),
|ids| {
for id in ids {
ret.push(id? as u32);
}
Ok(())
},
)
.bind(&self_addr)
.bind(DC_CONTACT_ID_LAST_SPECIAL)
.bind(Origin::IncomingReplyTo)
.bind(&s3str_like_cmd)
.bind(&s3str_like_cmd)
.bind(if flag_verified_only { 0i32 } else { 1i32 }),
)
.await?;
.await?
.map(|row| row?.try_get(0));
while let Some(id) = rows.next().await {
ret.push(id?);
}
let self_name = context
.get_config(Config::Displayname)
.await
.await?
.unwrap_or_default();
let self_name2 = stock_str::self_msg(context);
@@ -646,25 +685,27 @@ impl Contact {
} else {
add_self = true;
context
let mut rows = context
.sql
.query_map(
"SELECT id FROM contacts
.fetch(
sqlx::query(
"SELECT id FROM contacts
WHERE addr!=?1
AND id>?2
AND origin>=?3
AND blocked=0
ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
paramsv![self_addr, DC_CONTACT_ID_LAST_SPECIAL as i32, 0x100],
|row| row.get::<_, i32>(0),
|ids| {
for id in ids {
ret.push(id? as u32);
}
Ok(())
},
)
.bind(self_addr)
.bind(DC_CONTACT_ID_LAST_SPECIAL)
.bind(Origin::IncomingReplyTo),
)
.await?;
.await?
.map(|row| row?.try_get(0));
while let Some(id) = rows.next().await {
ret.push(id?);
}
}
if flag_add_self && add_self {
@@ -674,33 +715,83 @@ impl Contact {
Ok(ret)
}
pub async fn get_blocked_cnt(context: &Context) -> usize {
context
// add blocked mailinglists as contacts
// to allow unblocking them as if they are contacts
// (this way, only one unblock-ffi is needed and only one set of ui-functions,
// from the users perspective,
// there is not much difference in an email- and a mailinglist-address)
async fn update_blocked_mailinglist_contacts(context: &Context) -> Result<()> {
let mut rows = context
.sql
.query_get_value::<isize>(
context,
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
.fetch(
sqlx::query("SELECT name, grpid FROM chats WHERE type=? AND blocked=?;")
.bind(Chattype::Mailinglist)
.bind(Blocked::Manually),
)
.await
.unwrap_or_default() as usize
.await?;
while let Some(row) = rows.next().await {
let row = row?;
let name = row.try_get::<String, _>(0)?;
let grpid = row.try_get::<String, _>(1)?;
if !context
.sql
.exists(sqlx::query("SELECT COUNT(id) FROM contacts WHERE addr=?;").bind(&grpid))
.await?
{
context
.sql
.execute(sqlx::query("INSERT INTO contacts (addr) VALUES (?);").bind(&grpid))
.await?;
}
// always do an update in case the blocking is reset or name is changed
context
.sql
.execute(
sqlx::query("UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?;")
.bind(name)
.bind(Origin::MailinglistAddress)
.bind(&grpid),
)
.await?;
}
Ok(())
}
pub async fn get_blocked_cnt(context: &Context) -> Result<usize> {
let count = context
.sql
.count(
sqlx::query("SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0")
.bind(DC_CONTACT_ID_LAST_SPECIAL),
)
.await?;
Ok(count as usize)
}
/// Get blocked contacts.
pub async fn get_all_blocked(context: &Context) -> Vec<u32> {
context
pub async fn get_all_blocked(context: &Context) -> Result<Vec<u32>> {
if let Err(e) = Contact::update_blocked_mailinglist_contacts(context).await {
warn!(
context,
"Cannot update blocked mailinglist contacts: {:?}", e
);
}
let list = context
.sql
.query_map(
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(name||addr),id;",
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
|row| row.get::<_, u32>(0),
|ids| {
ids.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
.fetch(
sqlx::query(
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
).bind(DC_CONTACT_ID_LAST_SPECIAL)
)
.await
.unwrap_or_default()
.await?
.map(|row| row?.try_get::<u32, _>(0))
.collect::<sqlx::Result<Vec<_>>>()
.await?;
Ok(list)
}
/// Returns a textual summary of the encryption state for the contact.
@@ -716,7 +807,7 @@ impl Contact {
let mut ret = String::new();
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
let loginparam = LoginParam::from_database(context, "configured_").await;
let loginparam = LoginParam::from_database(context, "configured_").await?;
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
if let Some(peerstate) = peerstate.filter(|peerstate| {
@@ -783,26 +874,23 @@ impl Contact {
"Can not delete special contact"
);
let count_contacts: i32 = context
let count_contacts = context
.sql
.query_get_value(
context,
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
paramsv![contact_id as i32],
.count(
sqlx::query("SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;")
.bind(contact_id),
)
.await
.unwrap_or_default();
.await?;
let count_msgs: i32 = if count_contacts > 0 {
let count_msgs = if count_contacts > 0 {
context
.sql
.query_get_value(
context,
"SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;",
paramsv![contact_id as i32, contact_id as i32],
.count(
sqlx::query("SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;")
.bind(contact_id)
.bind(contact_id),
)
.await
.unwrap_or_default()
.await?
} else {
0
};
@@ -810,10 +898,7 @@ impl Contact {
if count_msgs == 0 {
match context
.sql
.execute(
"DELETE FROM contacts WHERE id=?;",
paramsv![contact_id as i32],
)
.execute(sqlx::query("DELETE FROM contacts WHERE id=?;").bind(contact_id as i32))
.await
{
Ok(_) => {
@@ -850,8 +935,9 @@ impl Contact {
context
.sql
.execute(
"UPDATE contacts SET param=? WHERE id=?",
paramsv![self.param.to_string(), self.id as i32],
sqlx::query("UPDATE contacts SET param=? WHERE id=?")
.bind(self.param.to_string())
.bind(self.id as i32),
)
.await?;
Ok(())
@@ -862,8 +948,9 @@ impl Contact {
context
.sql
.execute(
"UPDATE contacts SET status=? WHERE id=?",
paramsv![self.status, self.id as i32],
sqlx::query("UPDATE contacts SET status=? WHERE id=?")
.bind(&self.status)
.bind(self.id as i32),
)
.await?;
Ok(())
@@ -928,17 +1015,17 @@ impl Contact {
/// Get the contact's profile image.
/// This is the image set by each remote user on their own
/// using dc_set_config(context, "selfavatar", image).
pub async fn get_profile_image(&self, context: &Context) -> Option<PathBuf> {
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
if self.id == DC_CONTACT_ID_SELF {
if let Some(p) = context.get_config(Config::Selfavatar).await {
return Some(PathBuf::from(p));
if let Some(p) = context.get_config(Config::Selfavatar).await? {
return Ok(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));
return Ok(Some(dc_get_abs_path(context, image_rel)));
}
}
None
Ok(None)
}
/// Get a color for the contact.
@@ -971,7 +1058,7 @@ impl Contact {
pub async fn is_verified_ex(
&self,
context: &Context,
peerstate: Option<&Peerstate<'_>>,
peerstate: Option<&Peerstate>,
) -> VerifiedStatus {
// We're always sort of secured-verified as we could verify the key on this device any time with the key
// on this device
@@ -1026,20 +1113,19 @@ impl Contact {
false
}
pub async fn get_real_cnt(context: &Context) -> usize {
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
if !context.sql.is_open().await {
return 0;
return Ok(0);
}
context
let count = context
.sql
.query_get_value::<isize>(
context,
"SELECT COUNT(*) FROM contacts WHERE id>?;",
paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32],
.count(
sqlx::query("SELECT COUNT(*) FROM contacts WHERE id>?;")
.bind(DC_CONTACT_ID_LAST_SPECIAL),
)
.await
.unwrap_or_default() as usize
.await?;
Ok(count)
}
pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> bool {
@@ -1049,10 +1135,7 @@ impl Contact {
context
.sql
.exists(
"SELECT id FROM contacts WHERE id=?;",
paramsv![contact_id as i32],
)
.exists(sqlx::query("SELECT COUNT(*) FROM contacts WHERE id=?;").bind(contact_id))
.await
.unwrap_or_default()
}
@@ -1061,8 +1144,10 @@ impl Contact {
context
.sql
.execute(
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
paramsv![origin, contact_id as i32, origin],
sqlx::query("UPDATE contacts SET origin=? WHERE id=? AND origin<?;")
.bind(origin)
.bind(contact_id)
.bind(origin),
)
.await
.is_ok()
@@ -1116,8 +1201,9 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
&& context
.sql
.execute(
"UPDATE contacts SET blocked=? WHERE id=?;",
paramsv![new_blocking as i32, contact_id as i32],
sqlx::query("UPDATE contacts SET blocked=? WHERE id=?;")
.bind(new_blocking as i32)
.bind(contact_id),
)
.await
.is_ok()
@@ -1127,13 +1213,37 @@ async fn set_block_contact(context: &Context, contact_id: u32, new_blocking: boo
// (Maybe, beside normal chats (type=100) we should also block group chats with only this user.
// However, I'm not sure about this point; it may be confusing if the user wants to add other people;
// this would result in recreating the same group...)
if context.sql.execute(
"UPDATE chats SET blocked=? WHERE type=? AND id IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);",
paramsv![new_blocking, 100, contact_id as i32]).await.is_ok()
if context
.sql
.execute(
sqlx::query(
r#"
UPDATE chats
SET blocked=?
WHERE type=? AND id IN (
SELECT chat_id FROM chats_contacts WHERE contact_id=?
);
"#,
)
.bind(new_blocking)
.bind(Chattype::Single)
.bind(contact_id),
)
.await
.is_ok()
{
Contact::mark_noticed(context, contact_id).await;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
}
// also unblock mailinglist
// if the contact is a mailinglist address explicitly created to allow unblocking
if !new_blocking && contact.origin == Origin::MailinglistAddress {
if let Ok((chat_id, _, _)) = chat::get_chat_id_by_grpid(context, contact.addr).await
{
chat_id.set_blocked(context, Blocked::Not).await;
}
}
}
}
}
@@ -1251,7 +1361,7 @@ impl Context {
pub async fn is_self_addr(&self, addr: &str) -> Result<bool> {
let self_addr = self
.get_config(Config::ConfiguredAddr)
.await
.await?
.ok_or_else(|| format_err!("Not configured"))?;
Ok(addr_cmp(self_addr, addr))
@@ -1328,7 +1438,7 @@ mod tests {
#[test]
fn test_split_address_book() {
let book = "Name one\nAddress one\nName two\nAddress two\nrest name";
let list = split_address_book(&book);
let list = split_address_book(book);
assert_eq!(
list,
vec![("Name one", "Address one"), ("Name two", "Address two")]

View File

@@ -6,22 +6,24 @@ use std::ops::Deref;
use std::time::{Instant, SystemTime};
use anyhow::{bail, ensure, Result};
use async_std::prelude::*;
use async_std::{
channel::{self, Receiver, Sender},
path::{Path, PathBuf},
sync::{Arc, Mutex, RwLock},
task,
};
use sqlx::Row;
use crate::chat::{get_chat_cnt, ChatId};
use crate::config::Config;
use crate::constants::DC_VERSION_STR;
use crate::contact::Contact;
use crate::dc_tools::duration_to_str;
use crate::dc_tools::{duration_to_str, time};
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::message::{self, MsgId};
use crate::message::{self, MessageState, MsgId};
use crate::scheduler::Scheduler;
use crate::securejoin::Bob;
use crate::sql::Sql;
@@ -89,8 +91,9 @@ pub struct RunningState {
pub fn get_info() -> BTreeMap<&'static str, String> {
let mut res = BTreeMap::new();
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("sqlite_version", rusqlite::version().to_string());
res.insert("sqlite_version", crate::sql::version().to_string());
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
res.insert("num_cpus", num_cpus::get().to_string());
res.insert("level", "awesome".into());
res
}
@@ -270,68 +273,68 @@ impl Context {
* UI chat/message related API
******************************************************************************/
pub async fn get_info(&self) -> BTreeMap<&'static str, String> {
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
let unset = "0";
let l = LoginParam::from_database(self, "").await;
let l2 = LoginParam::from_database(self, "configured_").await;
let displayname = self.get_config(Config::Displayname).await;
let chats = get_chat_cnt(self).await as usize;
let l = LoginParam::from_database(self, "").await?;
let l2 = LoginParam::from_database(self, "configured_").await?;
let displayname = self.get_config(Config::Displayname).await?;
let chats = get_chat_cnt(self).await? as usize;
let real_msgs = message::get_real_msg_cnt(self).await as usize;
let deaddrop_msgs = message::get_deaddrop_msg_cnt(self).await as usize;
let contacts = Contact::get_real_cnt(self).await as usize;
let is_configured = self.get_config_int(Config::Configured).await;
let contacts = Contact::get_real_cnt(self).await? as usize;
let is_configured = self.get_config_int(Config::Configured).await?;
let dbversion = self
.sql
.get_raw_config_int(self, "dbversion")
.await
.get_raw_config_int("dbversion")
.await?
.unwrap_or_default();
let journal_mode = self
.sql
.query_get_value(self, "PRAGMA journal_mode;", paramsv![])
.await
.query_get_value(sqlx::query("PRAGMA journal_mode;"))
.await?
.unwrap_or_else(|| "unknown".to_string());
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await;
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await;
let bcc_self = self.get_config_int(Config::BccSelf).await;
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
let bcc_self = self.get_config_int(Config::BccSelf).await?;
let prv_key_cnt: Option<isize> = self
let prv_key_cnt = self
.sql
.query_get_value(self, "SELECT COUNT(*) FROM keypairs;", paramsv![])
.await;
.count(sqlx::query("SELECT COUNT(*) FROM keypairs;"))
.await?;
let pub_key_cnt: Option<isize> = self
let pub_key_cnt = self
.sql
.query_get_value(self, "SELECT COUNT(*) FROM acpeerstates;", paramsv![])
.await;
.count(sqlx::query("SELECT COUNT(*) FROM acpeerstates;"))
.await?;
let fingerprint_str = match SignedPublicKey::load_self(self).await {
Ok(key) => key.fingerprint().hex(),
Err(err) => format!("<key failure: {}>", err),
};
let inbox_watch = self.get_config_int(Config::InboxWatch).await;
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await;
let mvbox_watch = self.get_config_int(Config::MvboxWatch).await;
let mvbox_move = self.get_config_int(Config::MvboxMove).await;
let sentbox_move = self.get_config_int(Config::SentboxMove).await;
let inbox_watch = self.get_config_int(Config::InboxWatch).await?;
let sentbox_watch = self.get_config_int(Config::SentboxWatch).await?;
let mvbox_watch = self.get_config_int(Config::MvboxWatch).await?;
let mvbox_move = self.get_config_int(Config::MvboxMove).await?;
let sentbox_move = self.get_config_int(Config::SentboxMove).await?;
let folders_configured = self
.sql
.get_raw_config_int(self, "folders_configured")
.await
.get_raw_config_int("folders_configured")
.await?
.unwrap_or_default();
let configured_sentbox_folder = self
.get_config(Config::ConfiguredSentboxFolder)
.await
.await?
.unwrap_or_else(|| "<unset>".to_string());
let configured_mvbox_folder = self
.get_config(Config::ConfiguredMvboxFolder)
.await
.await?
.unwrap_or_else(|| "<unset>".to_string());
let mut res = get_info();
// insert values
res.insert("bot", self.get_config_int(Config::Bot).await.to_string());
res.insert("bot", self.get_config_int(Config::Bot).await?.to_string());
res.insert("number_of_chats", chats.to_string());
res.insert("number_of_chat_messages", real_msgs.to_string());
res.insert("messages_in_contact_requests", deaddrop_msgs.to_string());
@@ -344,7 +347,7 @@ impl Context {
res.insert(
"selfavatar",
self.get_config(Config::Selfavatar)
.await
.await?
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert("is_configured", is_configured.to_string());
@@ -353,12 +356,12 @@ impl Context {
res.insert(
"fetch_existing_msgs",
self.get_config_int(Config::FetchExistingMsgs)
.await
.await?
.to_string(),
);
res.insert(
"show_emails",
self.get_config_int(Config::ShowEmails).await.to_string(),
self.get_config_int(Config::ShowEmails).await?.to_string(),
);
res.insert("inbox_watch", inbox_watch.to_string());
res.insert("sentbox_watch", sentbox_watch.to_string());
@@ -372,64 +375,64 @@ impl Context {
res.insert("e2ee_enabled", e2ee_enabled.to_string());
res.insert(
"key_gen_type",
self.get_config_int(Config::KeyGenType).await.to_string(),
self.get_config_int(Config::KeyGenType).await?.to_string(),
);
res.insert("bcc_self", bcc_self.to_string());
res.insert(
"private_key_count",
prv_key_cnt.unwrap_or_default().to_string(),
);
res.insert(
"public_key_count",
pub_key_cnt.unwrap_or_default().to_string(),
);
res.insert("private_key_count", prv_key_cnt.to_string());
res.insert("public_key_count", pub_key_cnt.to_string());
res.insert("fingerprint", fingerprint_str);
res.insert(
"webrtc_instance",
self.get_config(Config::WebrtcInstance)
.await
.await?
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert(
"media_quality",
self.get_config_int(Config::MediaQuality).await.to_string(),
self.get_config_int(Config::MediaQuality).await?.to_string(),
);
res.insert(
"delete_device_after",
self.get_config_int(Config::DeleteDeviceAfter)
.await
.await?
.to_string(),
);
res.insert(
"delete_server_after",
self.get_config_int(Config::DeleteServerAfter)
.await
.await?
.to_string(),
);
res.insert(
"last_housekeeping",
self.get_config_int(Config::LastHousekeeping)
.await
.await?
.to_string(),
);
res.insert(
"scan_all_folders_debounce_secs",
self.get_config_int(Config::ScanAllFoldersDebounceSecs)
.await
.await?
.to_string(),
);
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
res
Ok(res)
}
pub async fn get_fresh_msgs(&self) -> Vec<MsgId> {
let show_deaddrop: i32 = 0;
self.sql
.query_map(
concat!(
/// Get a list of fresh, unmuted messages in any chat but deaddrop.
///
/// The list starts with the most recent message
/// and is typically used to show notifications.
/// Moreover, the number of returned messages
/// can be used for a badge counter on the app icon.
pub async fn get_fresh_msgs(&self) -> Result<Vec<MsgId>> {
let list = self
.sql
.fetch(
sqlx::query(concat!(
"SELECT m.id",
" FROM msgs m",
" LEFT JOIN contacts ct",
@@ -438,70 +441,77 @@ impl Context {
" ON m.chat_id=c.id",
" WHERE m.state=?",
" AND m.hidden=0",
" AND m.chat_id>?",
" AND m.chat_id>9",
" AND ct.blocked=0",
" AND (c.blocked=0 OR c.blocked=?)",
" AND c.blocked=0",
" AND NOT(c.muted_until=-1 OR c.muted_until>?)",
" ORDER BY m.timestamp DESC,m.id DESC;"
),
paramsv![10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
|row| row.get::<_, MsgId>(0),
|rows| {
let mut ret = Vec::new();
for row in rows {
ret.push(row?);
}
Ok(ret)
},
))
.bind(MessageState::InFresh)
.bind(time()),
)
.await
.unwrap_or_default()
.await?
.map(|row| row?.try_get("id"))
.collect::<sqlx::Result<_>>()
.await?;
Ok(list)
}
/// Searches for messages containing the query string.
///
/// If `chat_id` is provided this searches only for messages in this chat, if `chat_id`
/// is `None` this searches messages from all chats.
pub async fn search_msgs(&self, chat_id: Option<ChatId>, query: impl AsRef<str>) -> Vec<MsgId> {
pub async fn search_msgs(
&self,
chat_id: Option<ChatId>,
query: impl AsRef<str>,
) -> Result<Vec<MsgId>> {
let real_query = query.as_ref().trim();
if real_query.is_empty() {
return Vec::new();
return Ok(Vec::new());
}
let str_like_in_text = format!("%{}%", real_query);
let str_like_beg = format!("{}%", real_query);
let do_query = |query, params| {
self.sql.query_map(
query,
params,
|row| row.get::<_, MsgId>("id"),
|rows| {
let mut ret = Vec::new();
for id in rows {
ret.push(id?);
}
Ok(ret)
},
)
};
if let Some(chat_id) = chat_id {
do_query(
"SELECT m.id AS id, m.timestamp AS timestamp
let list = if let Some(chat_id) = chat_id {
self.sql
.fetch(
sqlx::query(
"SELECT m.id AS id, m.timestamp AS timestamp
FROM msgs m
LEFT JOIN contacts ct
ON m.from_id=ct.id
WHERE m.chat_id=?
AND m.hidden=0
AND ct.blocked=0
AND (txt LIKE ? OR ct.name LIKE ?)
AND txt LIKE ?
ORDER BY m.timestamp,m.id;",
paramsv![chat_id, str_like_in_text, str_like_beg],
)
.await
.unwrap_or_default()
)
.bind(chat_id)
.bind(str_like_in_text),
)
.await?
.map(|row| {
let row = row?;
let id = row.try_get::<MsgId, _>("id")?;
Ok(id)
})
.collect::<sqlx::Result<Vec<MsgId>>>()
.await?
} else {
do_query(
"SELECT m.id AS id, m.timestamp AS timestamp
// For performance reasons results are sorted only by `id`, that is in the order of
// message reception.
//
// Unlike chat view, sorting by `timestamp` is not necessary but slows down the query by
// ~25% according to benchmarks.
//
// To speed up incremental search, where queries for few characters usually return lots
// of unwanted results that are discarded moments later, we added `LIMIT 1000`.
// According to some tests, this limit speeds up eg. 2 character searches by factor 10.
// The limit is documented and UI may add a hint when getting 1000 results.
self.sql
.fetch(
sqlx::query(
"SELECT m.id AS id, m.timestamp AS timestamp
FROM msgs m
LEFT JOIN contacts ct
ON m.from_id=ct.id
@@ -511,33 +521,46 @@ impl Context {
AND m.hidden=0
AND c.blocked=0
AND ct.blocked=0
AND (m.txt LIKE ? OR ct.name LIKE ?)
ORDER BY m.timestamp DESC,m.id DESC;",
paramsv![str_like_in_text, str_like_beg],
)
.await
.unwrap_or_default()
}
AND m.txt LIKE ?
ORDER BY m.id DESC LIMIT 1000",
)
.bind(str_like_in_text),
)
.await?
.map(|row| {
let row = row?;
let id = row.try_get::<MsgId, _>("id")?;
Ok(id)
})
.collect::<sqlx::Result<Vec<MsgId>>>()
.await?
};
Ok(list)
}
pub async fn is_inbox(&self, folder_name: impl AsRef<str>) -> bool {
self.get_config(Config::ConfiguredInboxFolder).await
== Some(folder_name.as_ref().to_string())
pub async fn is_inbox(&self, folder_name: impl AsRef<str>) -> Result<bool> {
let inbox = self.get_config(Config::ConfiguredInboxFolder).await?;
Ok(inbox == Some(folder_name.as_ref().to_string()))
}
pub async fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
self.get_config(Config::ConfiguredSentboxFolder).await
== Some(folder_name.as_ref().to_string())
pub async fn is_sentbox(&self, folder_name: impl AsRef<str>) -> Result<bool> {
let sentbox = self.get_config(Config::ConfiguredSentboxFolder).await?;
Ok(sentbox == Some(folder_name.as_ref().to_string()))
}
pub async fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
self.get_config(Config::ConfiguredMvboxFolder).await
== Some(folder_name.as_ref().to_string())
pub async fn is_mvbox(&self, folder_name: impl AsRef<str>) -> Result<bool> {
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
Ok(mvbox == Some(folder_name.as_ref().to_string()))
}
pub async fn is_spam_folder(&self, folder_name: impl AsRef<str>) -> bool {
self.get_config(Config::ConfiguredSpamFolder).await
== Some(folder_name.as_ref().to_string())
pub async fn is_spam_folder(&self, folder_name: impl AsRef<str>) -> Result<bool> {
let is_spam = self.get_config(Config::ConfiguredSpamFolder).await?
== Some(folder_name.as_ref().to_string());
Ok(is_spam)
}
pub fn derive_blobdir(dbfile: &PathBuf) -> PathBuf {
@@ -589,7 +612,16 @@ pub fn get_version_str() -> &'static str {
mod tests {
use super::*;
use crate::chat::{
create_by_contact_id, get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat,
MuteDuration,
};
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_tools::dc_create_outgoing_rfc724_mid;
use crate::message::Message;
use crate::test_utils::TestContext;
use std::time::Duration;
use strum::IntoEnumIterator;
#[async_std::test]
@@ -604,10 +636,144 @@ mod tests {
#[async_std::test]
async fn test_get_fresh_msgs() {
let t = TestContext::new().await;
let fresh = t.get_fresh_msgs().await;
let fresh = t.get_fresh_msgs().await.unwrap();
assert!(fresh.is_empty())
}
async fn receive_msg(t: &TestContext, chat: &Chat) {
let members = get_chat_contacts(t, chat.id).await.unwrap();
let contact = Contact::load_from_db(t, *members.first().unwrap())
.await
.unwrap();
let msg = format!(
"From: {}\n\
To: alice@example.com\n\
Message-ID: <{}>\n\
Chat-Version: 1.0\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
contact.get_addr(),
dc_create_outgoing_rfc724_mid(None, contact.get_addr())
);
println!("{}", msg);
dc_receive_imf(t, msg.as_bytes(), "INBOX", 1, false)
.await
.unwrap();
}
#[async_std::test]
async fn test_get_fresh_msgs_and_muted_chats() {
// receive various mails in 3 chats
let t = TestContext::new_alice().await;
let bob = t.create_chat_with_contact("", "bob@g.it").await;
let claire = t.create_chat_with_contact("", "claire@g.it").await;
let dave = t.create_chat_with_contact("", "dave@g.it").await;
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
receive_msg(&t, &bob).await;
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.unwrap().len(), 1);
assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
receive_msg(&t, &claire).await;
receive_msg(&t, &claire).await;
assert_eq!(
get_chat_msgs(&t, claire.id, 0, None).await.unwrap().len(),
2
);
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3);
receive_msg(&t, &dave).await;
receive_msg(&t, &dave).await;
receive_msg(&t, &dave).await;
assert_eq!(get_chat_msgs(&t, dave.id, 0, None).await.unwrap().len(), 3);
assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6);
// mute one of the chats
set_muted(&t, claire.id, MuteDuration::Forever)
.await
.unwrap();
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 4); // muted claires messages are no longer counted
// receive more messages
receive_msg(&t, &bob).await;
receive_msg(&t, &claire).await;
receive_msg(&t, &dave).await;
assert_eq!(
get_chat_msgs(&t, claire.id, 0, None).await.unwrap().len(),
3
);
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted
// unmute claire again
set_muted(&t, claire.id, MuteDuration::NotMuted)
.await
.unwrap();
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again
}
#[async_std::test]
async fn test_get_fresh_msgs_and_muted_until() {
let t = TestContext::new_alice().await;
let bob = t.create_chat_with_contact("", "bob@g.it").await;
receive_msg(&t, &bob).await;
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.unwrap().len(), 1);
// chat is unmuted by default, here and in the following assert(),
// we check mainly that the SQL-statements in is_muted() and get_fresh_msgs()
// have the same view to the database.
assert!(!bob.is_muted());
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
// test get_fresh_msgs() with mute_until in the future
set_muted(
&t,
bob.id,
MuteDuration::Until(SystemTime::now() + Duration::from_secs(3600)),
)
.await
.unwrap();
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
assert!(bob.is_muted());
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
// to test get_fresh_msgs() with mute_until in the past,
// we need to modify the database directly
t.sql
.execute(
sqlx::query("UPDATE chats SET muted_until=? WHERE id=?;")
.bind(time() - 3600)
.bind(bob.id),
)
.await
.unwrap();
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
assert!(!bob.is_muted());
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
// test get_fresh_msgs() with "forever" mute_until
set_muted(&t, bob.id, MuteDuration::Forever).await.unwrap();
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
assert!(bob.is_muted());
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
// that results in "muted forever" by definition.
t.sql
.execute(sqlx::query("UPDATE chats SET muted_until=-2 WHERE id=?;").bind(bob.id))
.await
.unwrap();
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
assert!(!bob.is_muted());
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
}
#[async_std::test]
async fn test_blobdir_exists() {
let tmp = tempfile::tempdir().unwrap();
@@ -670,7 +836,7 @@ mod tests {
async fn test_get_info() {
let t = TestContext::new().await;
let info = t.get_info().await;
let info = t.get_info().await.unwrap();
assert!(info.get("database_dir").is_some());
}
@@ -710,7 +876,7 @@ mod tests {
"smtp_certificate_checks",
];
let t = TestContext::new().await;
let info = t.get_info().await;
let info = t.get_info().await.unwrap();
for key in Config::iter() {
let key: String = key.to_string();
if !skip_from_get_info.contains(&&*key)
@@ -719,9 +885,99 @@ mod tests {
{
assert!(
info.contains_key(&*key),
format!("'{}' missing in get_info() output", key)
"'{}' missing in get_info() output",
key
);
}
}
}
#[async_std::test]
async fn test_search_msgs() -> Result<()> {
let alice = TestContext::new_alice().await;
let self_talk = create_by_contact_id(&alice, DC_CONTACT_ID_SELF).await?;
let chat = alice
.create_chat_with_contact("Bob", "bob@example.org")
.await;
// Global search finds nothing.
let res = alice.search_msgs(None, "foo").await?;
assert!(res.is_empty());
// Search in chat with Bob finds nothing.
let res = alice.search_msgs(Some(chat.id), "foo").await?;
assert!(res.is_empty());
// Add messages to chat with Bob.
let mut msg1 = Message::new(Viewtype::Text);
msg1.set_text(Some("foobar".to_string()));
send_msg(&alice, chat.id, &mut msg1).await?;
let mut msg2 = Message::new(Viewtype::Text);
msg2.set_text(Some("barbaz".to_string()));
send_msg(&alice, chat.id, &mut msg2).await?;
// Global search with a part of text finds the message.
let res = alice.search_msgs(None, "ob").await?;
assert_eq!(res.len(), 1);
// Global search for "bar" matches both "foobar" and "barbaz".
let res = alice.search_msgs(None, "bar").await?;
assert_eq!(res.len(), 2);
// Message added later is returned first.
assert_eq!(res.get(0), Some(&msg2.id));
assert_eq!(res.get(1), Some(&msg1.id));
// Global search with longer text does not find any message.
let res = alice.search_msgs(None, "foobarbaz").await?;
assert!(res.is_empty());
// Search for random string finds nothing.
let res = alice.search_msgs(None, "abc").await?;
assert!(res.is_empty());
// Search in chat with Bob finds the message.
let res = alice.search_msgs(Some(chat.id), "foo").await?;
assert_eq!(res.len(), 1);
// Search in Saved Messages does not find the message.
let res = alice.search_msgs(Some(self_talk), "foo").await?;
assert!(res.is_empty());
Ok(())
}
#[async_std::test]
async fn test_limit_search_msgs() -> Result<()> {
let alice = TestContext::new_alice().await;
let chat = alice
.create_chat_with_contact("Bob", "bob@example.org")
.await;
// Add 999 messages
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("foobar".to_string()));
for _ in 0..999 {
send_msg(&alice, chat.id, &mut msg).await?;
}
let res = alice.search_msgs(None, "foo").await?;
assert_eq!(res.len(), 999);
// Add one more message, no limit yet
send_msg(&alice, chat.id, &mut msg).await?;
let res = alice.search_msgs(None, "foo").await?;
assert_eq!(res.len(), 1000);
// Add one more message, that one is truncated then
send_msg(&alice, chat.id, &mut msg).await?;
let res = alice.search_msgs(None, "foo").await?;
assert_eq!(res.len(), 1000);
// In-chat should not be not limited
let res = alice.search_msgs(Some(chat.id), "foo").await?;
assert_eq!(res.len(), 1001);
Ok(())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@ use chrono::{Local, TimeZone};
use rand::{thread_rng, Rng};
use crate::chat::{add_device_msg, add_device_msg_with_importance};
use crate::constants::{Viewtype, DC_OUTDATED_WARNING_DAYS};
use crate::constants::{Viewtype, DC_ELLIPSE, DC_OUTDATED_WARNING_DAYS};
use crate::context::Context;
use crate::events::EventType;
use crate::message::Message;
@@ -28,10 +28,8 @@ use crate::stock_str;
/// end of the shortened string.
#[allow(clippy::indexing_slicing)]
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
let ellipse = "[...]";
let count = buf.chars().count();
if approx_chars > 0 && count > approx_chars + ellipse.len() {
if approx_chars > 0 && count > approx_chars + DC_ELLIPSE.len() {
let end_pos = buf
.char_indices()
.nth(approx_chars)
@@ -39,9 +37,9 @@ pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
.unwrap_or_default();
if let Some(index) = buf[..end_pos].rfind(|c| c == ' ' || c == '\n') {
Cow::Owned(format!("{}{}", &buf[..=index], ellipse))
Cow::Owned(format!("{}{}", &buf[..=index], DC_ELLIPSE))
} else {
Cow::Owned(format!("{}{}", &buf[..end_pos], ellipse))
Cow::Owned(format!("{}{}", &buf[..end_pos], DC_ELLIPSE))
}
} else {
Cow::Borrowed(buf)
@@ -514,6 +512,7 @@ pub(crate) async fn get_next_backup_path(
) -> Result<(PathBuf, PathBuf), Error> {
let folder = PathBuf::from(folder.as_ref());
let stem = chrono::NaiveDateTime::from_timestamp(backup_time, 0)
// Don't change this file name format, in has_backup() we use string comparison to determine which backup is newer:
.format("delta-chat-backup-%Y-%m-%d")
.to_string();
@@ -633,21 +632,6 @@ impl FromStr for EmailAddress {
}
}
impl rusqlite::types::ToSql for EmailAddress {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Text(self.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
/// Utility to check if a in the binary represantion of listflags
/// the bit at position bitindex is 1.
pub(crate) fn listflags_has(listflags: u32, bitindex: usize) -> bool {
let listflags = listflags as usize;
(listflags & bitindex) == bitindex
}
/// Makes sure that a user input that is not supposed to contain newlines does not contain newlines.
pub(crate) fn improve_single_line_input(input: impl AsRef<str>) -> String {
input
@@ -670,14 +654,32 @@ where
}
}
pub fn remove_subject_prefix(last_subject: &str) -> String {
let subject_start = if last_subject.starts_with("Chat:") {
0
} else {
// "Antw:" is the longest abbreviation in
// https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations#Abbreviations_in_other_languages,
// so look at the first _5_ characters:
match last_subject.chars().take(5).position(|c| c == ':') {
Some(prefix_end) => prefix_end + 1,
None => 0,
}
};
last_subject
.chars()
.skip(subject_start)
.collect::<String>()
.trim()
.to_string()
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use std::convert::TryInto;
use crate::constants::{DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
use crate::test_utils::TestContext;
#[test]
@@ -877,16 +879,6 @@ mod tests {
}
assert!(!dc_delete_file(context, "$BLOBDIR/lkqwjelqkwlje").await);
if dc_file_exist!(context, "$BLOBDIR/foobar").await
|| dc_file_exist!(context, "$BLOBDIR/dada").await
|| dc_file_exist!(context, "$BLOBDIR/foobar.dadada").await
|| dc_file_exist!(context, "$BLOBDIR/foobar-folder").await
{
dc_delete_file(context, "$BLOBDIR/foobar").await;
dc_delete_file(context, "$BLOBDIR/dada").await;
dc_delete_file(context, "$BLOBDIR/foobar.dadada").await;
dc_delete_file(context, "$BLOBDIR/foobar-folder").await;
}
assert!(dc_write_file(context, "$BLOBDIR/foobar", b"content")
.await
.is_ok());
@@ -929,20 +921,6 @@ mod tests {
assert!(!dc_file_exist!(context, &fn0).await);
}
#[test]
fn test_listflags_has() {
let listflags: u32 = 0x1101;
assert!(listflags_has(listflags, 0x1));
assert!(!listflags_has(listflags, 0x10));
assert!(listflags_has(listflags, 0x100));
assert!(listflags_has(listflags, 0x1000));
let listflags: u32 = (DC_GCL_ADD_SELF | DC_GCL_VERIFIED_ONLY).try_into().unwrap();
assert!(listflags_has(listflags, DC_GCL_VERIFIED_ONLY));
assert!(listflags_has(listflags, DC_GCL_ADD_SELF));
let listflags: u32 = DC_GCL_VERIFIED_ONLY.try_into().unwrap();
assert!(!listflags_has(listflags, DC_GCL_ADD_SELF));
}
#[async_std::test]
async fn test_create_smeared_timestamp() {
let t = TestContext::new().await;
@@ -1057,7 +1035,9 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 1);
// the message should be added only once a day - test that an hour later and nearly a day later
@@ -1067,7 +1047,9 @@ mod tests {
get_provider_update_timestamp(),
)
.await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 1);
maybe_warn_on_bad_time(
@@ -1076,7 +1058,9 @@ mod tests {
get_provider_update_timestamp(),
)
.await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 1);
// next day, there should be another device message
@@ -1089,7 +1073,9 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
assert_eq!(device_chat_id, chats.get_chat_id(0));
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 2);
}
@@ -1119,7 +1105,9 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), 1);
// do not repeat the warning every day ...
@@ -1139,7 +1127,9 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
let test_len = msgs.len();
assert!(test_len == 1 || test_len == 2);
@@ -1154,7 +1144,9 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let device_chat_id = chats.get_chat_id(0);
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None).await;
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
.await
.unwrap();
assert_eq!(msgs.len(), test_len + 1);
}
}

View File

@@ -23,11 +23,14 @@ struct Dehtml {
/// Everything between <div name="quote"> and <div name="quoted-content"> is usually metadata
/// If this is > `0`, then we are inside a `<div name="quoted-content">`.
divs_since_quoted_content_div: u32,
/// All-Inkl just puts the quote into `<blockquote> </blockquote>`. This count is
/// increased at each `<blockquote>` and decreased at each `</blockquote>`.
blockquotes_since_blockquote: u32,
}
impl Dehtml {
fn line_prefix(&self) -> &str {
if self.divs_since_quoted_content_div > 0 {
if self.divs_since_quoted_content_div > 0 || self.blockquotes_since_blockquote > 0 {
"> "
} else {
""
@@ -67,7 +70,7 @@ pub fn dehtml(buf: &str) -> Option<String> {
None
}
pub fn dehtml_quick_xml(buf: &str) -> String {
fn dehtml_quick_xml(buf: &str) -> String {
let buf = buf.trim().trim_start_matches("<!doctype html>");
let mut dehtml = Dehtml {
@@ -76,6 +79,7 @@ pub fn dehtml_quick_xml(buf: &str) -> String {
last_href: None,
divs_since_quote_div: 0,
divs_since_quoted_content_div: 0,
blockquotes_since_blockquote: 0,
};
let mut reader = quick_xml::Reader::from_str(buf);
@@ -179,6 +183,7 @@ fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
dehtml.strbuilder += "_";
}
}
"blockquote" => pop_tag(&mut dehtml.blockquotes_since_blockquote),
_ => {}
}
}
@@ -241,6 +246,7 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
dehtml.strbuilder += "_";
}
}
"blockquote" => dehtml.blockquotes_since_blockquote += 1,
_ => {}
}
}
@@ -308,6 +314,7 @@ mod tests {
"[ Foo ](https://example.com)",
),
("<b> bar </b>", "* bar *"),
("<i>foo</i>", "_foo_"),
("<b> bar <i> foo", "* bar _ foo"),
("&amp; bar", "& bar"),
// Despite missing ', this should be shown:
@@ -385,6 +392,13 @@ mod tests {
assert_eq!(txt.trim(), "lots of text");
}
#[test]
fn test_pre_tag() {
let input = "<html><pre>\ntwo\nlines\n</pre></html>";
let txt = dehtml(input).unwrap();
assert_eq!(txt.trim(), "two\nlines");
}
#[async_std::test]
async fn test_quote_div() {
let input = include_str!("../test-data/message/gmx-quote-body.eml");

View File

@@ -26,9 +26,9 @@ pub struct EncryptHelper {
impl EncryptHelper {
pub async fn new(context: &Context) -> Result<EncryptHelper> {
let prefer_encrypt =
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await)
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
.unwrap_or_default();
let addr = match context.get_config(Config::ConfiguredAddr).await {
let addr = match context.get_config(Config::ConfiguredAddr).await? {
None => {
bail!("addr not configured!");
}
@@ -113,7 +113,7 @@ impl EncryptHelper {
context: &Context,
min_verified: PeerstateVerifiedStatus,
mail_to_encrypt: lettre_email::PartBuilder,
peerstates: Vec<(Option<Peerstate<'_>>, &str)>,
peerstates: Vec<(Option<Peerstate>, &str)>,
) -> Result<String> {
let mut keyring: Keyring<SignedPublicKey> = Keyring::new();
@@ -153,7 +153,7 @@ pub async fn try_decrypt(
let from = mail
.headers
.get_header(HeaderDef::From_)
.and_then(|from_addr| mailparse::addrparse_header(&from_addr).ok())
.and_then(|from_addr| mailparse::addrparse_header(from_addr).ok())
.and_then(|from| from.extract_single_info())
.map(|from| from.addr)
.unwrap_or_default();
@@ -163,10 +163,10 @@ pub async fn try_decrypt(
// Apply Autocrypt header
if let Some(ref header) = Aheader::from_headers(context, &from, &mail.headers) {
if let Some(ref mut peerstate) = peerstate {
peerstate.apply_header(&header, message_time);
peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else {
let p = Peerstate::from_header(context, header, message_time);
let p = Peerstate::from_header(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
@@ -209,29 +209,58 @@ pub async fn try_decrypt(
Ok((out_mail, signatures))
}
/// 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
);
/// Returns a reference to the encrypted payload of a valid PGP/MIME message.
///
/// Returns `None` if the message is not a valid PGP/MIME message.
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
if mail.ctype.mimetype != "multipart/encrypted" {
return None;
}
if let [first_part, second_part] = &mail.subparts[..] {
ensure!(
first_part.ctype.mimetype == "application/pgp-encrypted",
"Invalid Autocrypt Level 1 version part: {:?}",
first_part.ctype,
);
ensure!(
second_part.ctype.mimetype == "application/octet-stream",
"Invalid Autocrypt Level 1 encrypted part: {:?}",
second_part.ctype
);
Ok(second_part)
if first_part.ctype.mimetype == "application/pgp-encrypted"
&& second_part.ctype.mimetype == "application/octet-stream"
{
Some(second_part)
} else {
None
}
} else {
bail!("Invalid Autocrypt Level 1 Mime Parts")
None
}
}
/// Returns a reference to the encrypted payload of a ["Mixed
/// Up"][pgpmime-message-mangling] message.
///
/// According to [RFC 3156] encrypted messages should have
/// `multipart/encrypted` MIME type and two parts, but Microsoft
/// Exchange and ProtonMail IMAP/SMTP Bridge are known to mangle this
/// structure by changing the type to `multipart/mixed` and prepending
/// an empty part at the start.
///
/// ProtonMail IMAP/SMTP Bridge prepends a part literally saying
/// "Empty Message", so we don't check its contents at all, checking
/// only for `text/plain` type.
///
/// Returns `None` if the message is not a "Mixed Up" message.
///
/// [RFC 3156]: https://www.rfc-editor.org/info/rfc3156
/// [pgpmime-message-mangling]: https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html
fn get_mixed_up_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
if mail.ctype.mimetype != "multipart/mixed" {
return None;
}
if let [first_part, second_part, third_part] = &mail.subparts[..] {
if first_part.ctype.mimetype == "text/plain"
&& second_part.ctype.mimetype == "application/pgp-encrypted"
&& third_part.ctype.mimetype == "application/octet-stream"
{
Some(third_part)
} else {
None
}
} else {
None
}
}
@@ -242,12 +271,12 @@ async fn decrypt_if_autocrypt_message(
public_keyring_for_validate: Keyring<SignedPublicKey>,
ret_valid_signatures: &mut HashSet<Fingerprint>,
) -> Result<Option<Vec<u8>>> {
let encrypted_data_part = match get_autocrypt_mime(mail) {
Err(_) => {
let encrypted_data_part = match get_autocrypt_mime(mail).or_else(|| get_mixed_up_mime(mail)) {
None => {
// not an autocrypt mime message, abort and ignore
return Ok(None);
}
Ok(res) => res,
Some(res) => res,
};
info!(context, "Detected Autocrypt-mime message");
@@ -329,7 +358,7 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool {
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.ok_or_else(|| {
format_err!(concat!(
"Failed to get self address, ",
@@ -497,14 +526,10 @@ Sent with my Delta Chat Messenger: https://delta.chat";
Ok(())
}
fn new_peerstates(
ctx: &Context,
prefer_encrypt: EncryptPreference,
) -> Vec<(Option<Peerstate<'_>>, &str)> {
fn new_peerstates(prefer_encrypt: EncryptPreference) -> Vec<(Option<Peerstate>, &'static str)> {
let addr = "bob@foo.bar";
let pub_key = bob_keypair().public;
let peerstate = Peerstate {
context: &ctx,
addr: addr.into(),
last_seen: 13,
last_seen_autocrypt: 14,
@@ -519,9 +544,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
let mut peerstates = Vec::new();
peerstates.push((Some(peerstate), addr));
peerstates
vec![(Some(peerstate), addr)]
}
#[async_std::test]
@@ -531,24 +554,46 @@ Sent with my Delta Chat Messenger: https://delta.chat";
// test with EncryptPreference::NoPreference:
// if e2ee_eguaranteed is unset, there is no encryption as not more than half of peers want encryption
let ps = new_peerstates(&t, EncryptPreference::NoPreference);
let ps = new_peerstates(EncryptPreference::NoPreference);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
// test with EncryptPreference::Reset
let ps = new_peerstates(&t, EncryptPreference::Reset);
let ps = new_peerstates(EncryptPreference::Reset);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
// test with EncryptPreference::Mutual (self is also Mutual)
let ps = new_peerstates(&t, EncryptPreference::Mutual);
let ps = new_peerstates(EncryptPreference::Mutual);
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
assert!(encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
// test with missing peerstate
let mut ps = Vec::new();
ps.push((None, "bob@foo.bar"));
let ps = vec![(None, "bob@foo.bar")];
assert!(encrypt_helper.should_encrypt(&t, true, &ps).is_err());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
}
#[test]
fn test_mixed_up_mime() -> Result<()> {
// "Mixed Up" mail as received when sending an encrypted
// message using Delta Chat Desktop via ProtonMail IMAP/SMTP
// Bridge.
let mixed_up_mime = include_bytes!("../test-data/message/protonmail-mixed-up.eml");
let mail = mailparse::parse_mail(mixed_up_mime)?;
assert!(get_autocrypt_mime(&mail).is_none());
assert!(get_mixed_up_mime(&mail).is_some());
// Same "Mixed Up" mail repaired by Thunderbird 78.9.0.
//
// It added `X-Enigmail-Info: Fixed broken PGP/MIME message`
// header although the repairing is done by the built-in
// OpenPGP support, not Enigmail.
let repaired_mime = include_bytes!("../test-data/message/protonmail-repaired.eml");
let mail = mailparse::parse_mail(repaired_mime)?;
assert!(get_autocrypt_mime(&mail).is_some());
assert!(get_mixed_up_mime(&mail).is_none());
Ok(())
}
}

View File

@@ -61,11 +61,11 @@ use std::num::ParseIntError;
use std::str::FromStr;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use anyhow::{ensure, Error};
use anyhow::{ensure, Context as _, Error};
use async_std::task;
use serde::{Deserialize, Serialize};
use sqlx::Row;
use crate::chat::{lookup_by_contact_id, send_msg, ChatId};
use crate::constants::{
Viewtype, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF,
};
@@ -76,6 +76,10 @@ use crate::message::{Message, MessageState, MsgId};
use crate::mimeparser::SystemMessage;
use crate::sql;
use crate::stock_str;
use crate::{
chat::{lookup_by_contact_id, send_msg, ChatId},
job,
};
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
pub enum Timer {
@@ -120,28 +124,41 @@ impl FromStr for Timer {
}
}
impl rusqlite::types::ToSql for Timer {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Integer(match self {
Self::Disabled => 0,
Self::Enabled { duration } => i64::from(*duration),
});
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
impl sqlx::Type<sqlx::Sqlite> for Timer {
fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
<i64 as sqlx::Type<_>>::type_info()
}
fn compatible(ty: &sqlx::sqlite::SqliteTypeInfo) -> bool {
<i64 as sqlx::Type<_>>::compatible(ty)
}
}
impl rusqlite::types::FromSql for Timer {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
i64::column_result(value).and_then(|value| {
if value == 0 {
Ok(Self::Disabled)
} else if let Ok(duration) = u32::try_from(value) {
Ok(Self::Enabled { duration })
} else {
Err(rusqlite::types::FromSqlError::OutOfRange(value))
}
})
impl<'q> sqlx::Encode<'q, sqlx::Sqlite> for Timer {
fn encode_by_ref(
&self,
args: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>,
) -> sqlx::encode::IsNull {
args.push(sqlx::sqlite::SqliteArgumentValue::Int64(
self.to_u32() as i64
));
sqlx::encode::IsNull::No
}
}
impl<'r> sqlx::Decode<'r, sqlx::Sqlite> for Timer {
fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
let value: i64 = sqlx::Decode::decode(value)?;
if value == 0 {
Ok(Self::Disabled)
} else if let Ok(duration) = u32::try_from(value) {
Ok(Self::Enabled { duration })
} else {
Err(Box::new(sqlx::Error::Decode(Box::new(
crate::error::OutOfRangeError,
))))
}
}
}
@@ -150,9 +167,8 @@ impl ChatId {
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer, Error> {
let timer = context
.sql
.query_get_value_result(
"SELECT ephemeral_timer FROM chats WHERE id=?;",
paramsv![self],
.query_get_value(
sqlx::query("SELECT ephemeral_timer FROM chats WHERE id=?;").bind(self),
)
.await?;
Ok(timer.unwrap_or_default())
@@ -172,10 +188,13 @@ impl ChatId {
context
.sql
.execute(
"UPDATE chats
sqlx::query(
"UPDATE chats
SET ephemeral_timer=?
WHERE id=?;",
paramsv![timer, self],
)
.bind(timer)
.bind(self),
)
.await?;
@@ -214,44 +233,45 @@ pub(crate) async fn stock_ephemeral_timer_changed(
from_id: u32,
) -> String {
match timer {
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await,
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id as u32).await,
Timer::Enabled { duration } => match duration {
0..=59 => {
stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id).await
stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id as u32)
.await
}
60 => stock_str::msg_ephemeral_timer_minute(context, from_id).await,
60 => stock_str::msg_ephemeral_timer_minute(context, from_id as u32).await,
61..=3599 => {
stock_str::msg_ephemeral_timer_minutes(
context,
format!("{}", (f64::from(duration) / 6.0).round() / 10.0),
from_id,
from_id as u32,
)
.await
}
3600 => stock_str::msg_ephemeral_timer_hour(context, from_id).await,
3600 => stock_str::msg_ephemeral_timer_hour(context, from_id as u32).await,
3601..=86399 => {
stock_str::msg_ephemeral_timer_hours(
context,
format!("{}", (f64::from(duration) / 360.0).round() / 10.0),
from_id,
from_id as u32,
)
.await
}
86400 => stock_str::msg_ephemeral_timer_day(context, from_id).await,
86400 => stock_str::msg_ephemeral_timer_day(context, from_id as u32).await,
86401..=604_799 => {
stock_str::msg_ephemeral_timer_days(
context,
format!("{}", (f64::from(duration) / 8640.0).round() / 10.0),
from_id,
from_id as u32,
)
.await
}
604_800 => stock_str::msg_ephemeral_timer_week(context, from_id).await,
604_800 => stock_str::msg_ephemeral_timer_week(context, from_id as u32).await,
_ => {
stock_str::msg_ephemeral_timer_weeks(
context,
format!("{}", (f64::from(duration) / 60480.0).round() / 10.0),
from_id,
from_id as u32,
)
.await
}
@@ -261,33 +281,38 @@ pub(crate) async fn stock_ephemeral_timer_changed(
impl MsgId {
/// Returns ephemeral message timer value for the message.
pub(crate) async fn ephemeral_timer(self, context: &Context) -> crate::sql::Result<Timer> {
pub(crate) async fn ephemeral_timer(self, context: &Context) -> anyhow::Result<Timer> {
let res = match context
.sql
.query_get_value_result(
"SELECT ephemeral_timer FROM msgs WHERE id=?",
paramsv![self],
.query_get_value::<_, i64>(
sqlx::query("SELECT ephemeral_timer FROM msgs WHERE id=?").bind(self),
)
.await?
{
None | Some(0) => Timer::Disabled,
Some(duration) => Timer::Enabled { duration },
Some(duration) => Timer::Enabled {
duration: u32::try_from(duration)?,
},
};
Ok(res)
}
/// Starts ephemeral message timer for the message if it is not started yet.
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> crate::sql::Result<()> {
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> anyhow::Result<()> {
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
let ephemeral_timestamp = time() + i64::from(duration);
context
.sql
.execute(
"UPDATE msgs SET ephemeral_timestamp = ? \
sqlx::query(
"UPDATE msgs SET ephemeral_timestamp = ? \
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
AND id = ?",
paramsv![ephemeral_timestamp, ephemeral_timestamp, self],
)
.bind(ephemeral_timestamp)
.bind(ephemeral_timestamp)
.bind(self),
)
.await?;
schedule_ephemeral_task(context).await;
@@ -308,18 +333,29 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, E
let mut updated = context
.sql
.execute(
"UPDATE msgs \
SET chat_id=?, txt='', txt_raw='', from_id=0, to_id=0, param='' \
WHERE \
ephemeral_timestamp != 0 \
AND ephemeral_timestamp <= ? \
AND chat_id != ?",
paramsv![DC_CHAT_ID_TRASH, time(), DC_CHAT_ID_TRASH],
sqlx::query(
// If you change which information is removed here, also change MsgId::trash() and
// which information dc_receive_imf::add_parts() still adds to the db if the chat_id is TRASH
r#"
UPDATE msgs
SET
chat_id=?, txt='', subject='', txt_raw='',
mime_headers='', from_id=0, to_id=0, param=''
WHERE
ephemeral_timestamp != 0
AND ephemeral_timestamp <= ?
AND chat_id != ?
"#,
)
.bind(DC_CHAT_ID_TRASH)
.bind(time())
.bind(DC_CHAT_ID_TRASH),
)
.await?
.await
.context("update failed")?
> 0;
if let Some(delete_device_after) = context.get_config_delete_device_after().await {
if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
let self_chat_id = lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
.await
.unwrap_or_default()
@@ -338,21 +374,22 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, E
let rows_modified = context
.sql
.execute(
"UPDATE msgs \
sqlx::query(
"UPDATE msgs \
SET txt = 'DELETED', chat_id = ? \
WHERE timestamp < ? \
AND chat_id > ? \
AND chat_id != ? \
AND chat_id != ?",
paramsv![
DC_CHAT_ID_TRASH,
threshold_timestamp,
DC_CHAT_ID_LAST_SPECIAL,
self_chat_id,
device_chat_id
],
)
.bind(DC_CHAT_ID_TRASH)
.bind(threshold_timestamp)
.bind(DC_CHAT_ID_LAST_SPECIAL)
.bind(self_chat_id)
.bind(device_chat_id),
)
.await?;
.await
.context("deleted update failed")?;
updated |= rows_modified > 0;
}
@@ -374,14 +411,18 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, E
pub async fn schedule_ephemeral_task(context: &Context) {
let ephemeral_timestamp: Option<i64> = match context
.sql
.query_get_value_result(
"SELECT ephemeral_timestamp \
FROM msgs \
WHERE ephemeral_timestamp != 0 \
AND chat_id != ? \
ORDER BY ephemeral_timestamp ASC \
LIMIT 1",
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
.query_get_value(
sqlx::query(
r#"
SELECT ephemeral_timestamp
FROM msgs
WHERE ephemeral_timestamp != 0
AND chat_id != ?
ORDER BY ephemeral_timestamp ASC
LIMIT 1;
"#,
)
.bind(DC_CHAT_ID_TRASH), // Trash contains already deleted messages, skip them
)
.await
{
@@ -437,25 +478,36 @@ pub async fn schedule_ephemeral_task(context: &Context) {
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result<Option<MsgId>> {
let now = time();
let threshold_timestamp = match context.get_config_delete_server_after().await {
let threshold_timestamp = match context.get_config_delete_server_after().await? {
None => 0,
Some(delete_server_after) => now - delete_server_after,
};
context
let row = context
.sql
.query_row_optional(
"SELECT id FROM msgs \
.fetch_optional(
sqlx::query(
"SELECT id FROM msgs \
WHERE ( \
timestamp < ? \
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \
) \
AND server_uid != 0 \
AND NOT id IN (SELECT foreign_id FROM jobs WHERE action = ?)
LIMIT 1",
paramsv![threshold_timestamp, now],
|row| row.get::<_, MsgId>(0),
)
.bind(threshold_timestamp)
.bind(now)
.bind(job::Action::DeleteMsgOnImap),
)
.await
.await?;
if let Some(row) = row {
let msg_id = row.try_get(0)?;
Ok(Some(msg_id))
} else {
Ok(None)
}
}
/// Start ephemeral timers for seen messages if they are not started
@@ -471,17 +523,17 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> sql::Result<()>
context
.sql
.execute(
"UPDATE msgs \
sqlx::query(
"UPDATE msgs \
SET ephemeral_timestamp = ? + ephemeral_timer \
WHERE ephemeral_timer > 0 \
AND ephemeral_timestamp = 0 \
AND state NOT IN (?, ?, ?)",
paramsv![
time(),
MessageState::InFresh,
MessageState::InNoticed,
MessageState::OutDraft
],
)
.bind(time())
.bind(MessageState::InFresh)
.bind(MessageState::InNoticed)
.bind(MessageState::OutDraft),
)
.await?;
@@ -490,6 +542,7 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> sql::Result<()>
#[cfg(test)]
mod tests {
use crate::param::Params;
use async_std::task::sleep;
use super::*;
@@ -501,7 +554,7 @@ mod tests {
#[async_std::test]
async fn test_stock_ephemeral_messages() {
let context = TestContext::new().await.ctx;
let context = TestContext::new().await;
assert_eq!(
stock_ephemeral_timer_changed(&context, Timer::Disabled, DC_CONTACT_ID_SELF).await,
@@ -711,11 +764,35 @@ mod tests {
sleep(Duration::from_millis(1100)).await;
// Check checks that the msg was deleted locally
check_msg_was_deleted(&t, &chat, msg.sender_msg_id).await;
// Check that the msg will be deleted on the server
// First of all, set a server_uid so that DC thinks that it's actually possible to delete
t.sql
.execute(sqlx::query("UPDATE msgs SET server_uid=1 WHERE id=?").bind(msg.sender_msg_id))
.await
.unwrap();
let job = job::load_imap_deletion_job(&t).await.unwrap();
assert_eq!(
job,
Some(job::Job::new(
job::Action::DeleteMsgOnImap,
msg.sender_msg_id.to_u32(),
Params::new(),
0,
))
);
// Let's assume that executing the job fails on first try and the job is saved to the db
job.unwrap().save(&t).await.unwrap();
// Make sure that we don't get yet another job when loading from db
let job2 = job::load_imap_deletion_job(&t).await.unwrap();
assert_eq!(job2, None);
}
async fn check_msg_was_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
let chat_items = chat::get_chat_msgs(&t, chat.id, 0, None).await;
let chat_items = chat::get_chat_msgs(t, chat.id, 0, None).await.unwrap();
// Check that the chat is empty except for possibly info messages:
for item in &chat_items {
if let ChatItem::Message { msg_id } = item {
@@ -725,15 +802,16 @@ mod tests {
}
// Check that if there is a message left, the text and metadata are gone
if let Ok(msg) = Message::load_from_db(&t, msg_id).await {
if let Ok(msg) = Message::load_from_db(t, msg_id).await {
assert_eq!(msg.from_id, 0);
assert_eq!(msg.to_id, 0);
assert!(msg.text.is_none_or_empty(), msg.text);
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
let rawtxt: Option<String> = t
.sql
.query_get_value(&t, "SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id])
.await;
assert!(rawtxt.is_none_or_empty(), rawtxt);
.query_get_value(sqlx::query("SELECT txt_raw FROM msgs WHERE id=?;").bind(msg_id))
.await
.unwrap();
assert!(rawtxt.is_none_or_empty(), "{:?}", rawtxt);
}
}
}

View File

@@ -1,5 +1,9 @@
//! # Error handling
#[derive(Debug, thiserror::Error)]
#[error("Out of Range")]
pub struct OutOfRangeError;
#[macro_export]
macro_rules! ensure_eq {
($left:expr, $right:expr) => ({

View File

@@ -146,6 +146,17 @@ mod tests {
let text = "> Not a quote";
assert_eq!(format_flowed(text), " > Not a quote");
// Test space stuffing of wrapped lines
let text = "> This is the Autocrypt Setup Message used to transfer your key between clients.\n\
> \n\
> To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
let expected = "\x20> This is the Autocrypt Setup Message used to transfer your key between \r\n\
clients.\r\n\
\x20>\r\n\
\x20> To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
client and enter the setup code presented on the generating device.";
assert_eq!(format_flowed(text), expected);
}
#[test]

View File

@@ -11,11 +11,20 @@ pub enum HeaderDef {
To,
Cc,
Disposition,
/// Used in the "Body Part Header" of MDNs as of RFC 8098.
/// Indicates the Message-ID of the message for which the MDN is being issued.
OriginalMessageId,
/// Delta Chat extension for message IDs in combined MDNs
AdditionalMessageIds,
/// Outlook-SMTP-server replace the `Message-ID:`-header
/// and write the original ID to `X-Microsoft-Original-Message-ID`.
/// To sort things correctly and to not show outgoing messages twice,
/// we need to check that header as well.
XMicrosoftOriginalMessageId,
ListId,
References,
InReplyTo,
@@ -43,6 +52,7 @@ pub enum HeaderDef {
SecureJoinFingerprint,
SecureJoinInvitenumber,
SecureJoinAuth,
Sender,
EphemeralTimer,
Received,
_TestHeader,

View File

@@ -13,12 +13,12 @@ use std::pin::Pin;
use anyhow::Result;
use lettre_email::mime::{self, Mime};
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::message::{Message, MsgId};
use crate::mimeparser::parse_message_id;
use crate::param::Param::SendHtml;
use crate::plaintext::PlainText;
use crate::{context::Context, message};
use lettre_email::PartBuilder;
use mailparse::ParsedContentType;
@@ -108,9 +108,12 @@ impl HtmlMsgParser {
/// Function iterates over all mime-parts
/// and searches for text/plain and text/html parts and saves the
/// last one found
/// first one found.
/// in the corresponding structure fields.
/// Usually, there is at most one plain-text and one HTML-text part.
///
/// Usually, there is at most one plain-text and one HTML-text part,
/// multiple plain-text parts might be used for mailinglist-footers,
/// therefore we use the first one.
fn collect_texts_recursive<'a>(
&'a mut self,
context: &'a Context,
@@ -136,11 +139,12 @@ impl HtmlMsgParser {
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype == mime::TEXT_HTML {
if let Ok(decoded_data) = mail.get_body() {
self.html = decoded_data;
return Ok(());
if self.html.is_empty() {
if let Ok(decoded_data) = mail.get_body() {
self.html = decoded_data;
}
}
} else if mimetype == mime::TEXT_PLAIN {
} else if mimetype == mime::TEXT_PLAIN && self.plain.is_none() {
if let Ok(decoded_data) = mail.get_body() {
self.plain = Some(PlainText {
text: decoded_data,
@@ -155,7 +159,6 @@ impl HtmlMsgParser {
false
},
});
return Ok(());
}
}
Ok(())
@@ -194,7 +197,7 @@ impl HtmlMsgParser {
if mimetype.type_() == mime::IMAGE {
if let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId) {
if let Ok(cid) = parse_message_id(&cid) {
if let Ok(replacement) = mimepart_to_data_url(&mail).await {
if let Ok(replacement) = mimepart_to_data_url(mail).await {
let re_string = format!(
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
regex::escape(&cid)
@@ -241,32 +244,20 @@ impl MsgId {
/// this is the case at least when `Message.has_html()` returns true
/// (we do not save raw mime unconditionally in the database to save space).
/// The corresponding ffi-function is `dc_get_msg_html()`.
pub async fn get_html(self, context: &Context) -> Option<String> {
let rawmime: Option<String> = context
.sql
.query_get_value(
context,
"SELECT mime_headers FROM msgs WHERE id=?;",
paramsv![self],
)
.await;
pub async fn get_html(self, context: &Context) -> Result<Option<String>> {
let rawmime = message::get_mime_headers(context, self).await?;
if let Some(rawmime) = rawmime {
if !rawmime.is_empty() {
match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await {
Err(err) => {
warn!(context, "get_html: parser error: {}", err);
None
}
Ok(parser) => Some(parser.html),
if !rawmime.is_empty() {
match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await {
Err(err) => {
warn!(context, "get_html: parser error: {}", err);
Ok(None)
}
} else {
warn!(context, "get_html: empty mime for {}", self);
None
Ok(parser) => Ok(Some(parser.html)),
}
} else {
warn!(context, "get_html: no mime for {}", self);
None
Ok(None)
}
}
}
@@ -420,30 +411,23 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/apple_cid_jpg.eml");
let test = String::from_utf8_lossy(raw);
assert!(test
.find("Content-Id: <8AE052EF-BC90-486F-BB78-58D3590308EC@fritz.box>")
.is_some());
assert!(test
.find("cid:8AE052EF-BC90-486F-BB78-58D3590308EC@fritz.box")
.is_some());
assert!(test.contains("Content-Id: <8AE052EF-BC90-486F-BB78-58D3590308EC@fritz.box>"));
assert!(test.contains("cid:8AE052EF-BC90-486F-BB78-58D3590308EC@fritz.box"));
assert!(test.find("data:").is_none());
// parsing converts cid: to data:
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert!(parser.html.find("<html>").is_some());
assert!(parser.html.find("Content-Id:").is_none());
assert!(parser
.html
.find("data:image/jpeg;base64,/9j/4AAQ")
.is_some());
assert!(parser.html.find("cid:").is_none());
assert!(parser.html.contains("<html>"));
assert!(!parser.html.contains("Content-Id:"));
assert!(parser.html.contains("data:image/jpeg;base64,/9j/4AAQ"));
assert!(!parser.html.contains("cid:"));
}
#[async_std::test]
async fn test_get_html_empty() {
let t = TestContext::new().await;
let msg_id = MsgId::new_unset();
assert!(msg_id.get_html(&t).await.is_none())
assert!(msg_id.get_html(&t).await.unwrap().is_none())
}
#[async_std::test]
@@ -462,10 +446,10 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_ne!(msg.get_from_id(), DC_CONTACT_ID_SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::No);
assert!(!msg.is_forwarded());
assert!(msg.get_text().unwrap().find("this is plain").is_some());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap();
assert!(html.find("this is <b>html</b>").is_some());
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
// alice: create chat with bob and forward received html-message there
let chat = alice.create_chat_with_contact("", "bob@example.net").await;
@@ -476,10 +460,10 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_eq!(msg.get_from_id(), DC_CONTACT_ID_SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().find("this is plain").is_some());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap();
assert!(html.find("this is <b>html</b>").is_some());
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
// bob: check that bob also got the html-part of the forwarded message
let bob = TestContext::new_bob().await;
@@ -489,10 +473,10 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_ne!(msg.get_from_id(), DC_CONTACT_ID_SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().find("this is plain").is_some());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&bob).await.unwrap();
assert!(html.find("this is <b>html</b>").is_some());
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
}
#[async_std::test]
@@ -521,7 +505,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
// receive the message on another device
let alice = TestContext::new_alice().await;
assert_eq!(alice.get_config_int(Config::ShowEmails).await, 0); // set to "1" above, make sure it is another db
assert_eq!(alice.get_config_int(Config::ShowEmails).await.unwrap(), 0); // set to "1" above, make sure it is another db
alice.recv_msg(&msg).await;
let chat = alice.get_self_chat().await;
let msg = alice.get_last_msg_in(chat.get_id()).await;
@@ -529,10 +513,10 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.get_showpadlock());
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().find("this is plain").is_some());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap();
assert!(html.find("this is <b>html</b>").is_some());
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
}
#[async_std::test]
@@ -553,8 +537,8 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_eq!(msg.get_text(), Some("plain text".to_string()));
assert!(!msg.is_forwarded());
assert!(msg.mime_modified);
let html = msg.get_id().get_html(&alice).await.unwrap();
assert!(html.find("<b>html</b> text").is_some());
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("<b>html</b> text"));
// let bob receive the message
let chat_id = bob.create_chat(&alice).await.id;
@@ -563,7 +547,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_eq!(msg.get_text(), Some("plain text".to_string()));
assert!(!msg.is_forwarded());
assert!(msg.mime_modified);
let html = msg.get_id().get_html(&bob).await.unwrap();
assert!(html.find("<b>html</b> text").is_some());
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
assert!(html.contains("<b>html</b> text"));
}
}

View File

@@ -17,8 +17,8 @@ use num_traits::FromPrimitive;
use crate::chat;
use crate::config::Config;
use crate::constants::{
Chattype, ShowEmails, Viewtype, DC_CONTACT_ID_SELF, DC_FETCH_EXISTING_MSGS_COUNT,
DC_FOLDERS_CONFIGURED_VERSION, DC_LP_AUTH_OAUTH2,
Chattype, ShowEmails, Viewtype, DC_FETCH_EXISTING_MSGS_COUNT, DC_FOLDERS_CONFIGURED_VERSION,
DC_LP_AUTH_OAUTH2,
};
use crate::context::Context;
use crate::dc_receive_imf::{
@@ -72,8 +72,14 @@ const PREFETCH_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
CHAT-VERSION \
AUTOCRYPT-SETUP-MESSAGE\
)])";
const DELETE_CHECK_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])";
const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])";
const DELETE_CHECK_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \
X-MICROSOFT-ORIGINAL-MESSAGE-ID\
)])";
const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \
X-MICROSOFT-ORIGINAL-MESSAGE-ID\
)])";
const JUST_UID: &str = "(UID)";
const BODY_FLAGS: &str = "(FLAGS BODY.PEEK[])";
@@ -188,7 +194,7 @@ impl Imap {
let oauth2 = self.config.oauth2;
let connection_res: ImapResult<Client> = if self.config.lp.security == Socket::STARTTLS
let connection_res: ImapResult<Client> = if self.config.lp.security == Socket::Starttls
|| self.config.lp.security == Socket::Plain
{
let config = &mut self.config;
@@ -197,7 +203,7 @@ impl Imap {
match Client::connect_insecure((imap_server, imap_port)).await {
Ok(client) => {
if config.lp.security == Socket::STARTTLS {
if config.lp.security == Socket::Starttls {
client.secure(imap_server, config.strict_tls).await
} else {
Ok(client)
@@ -223,7 +229,7 @@ impl Imap {
let addr: &str = config.addr.as_ref();
if let Some(token) =
dc_get_oauth2_access_token(context, addr, imap_pw, true).await
dc_get_oauth2_access_token(context, addr, imap_pw, true).await?
{
let auth = OAuth2 {
user: imap_user.into(),
@@ -261,7 +267,7 @@ impl Imap {
let lock = context.wrong_pw_warning_mutex.lock().await;
if self.login_failed_once
&& context.get_config_bool(Config::NotifyAboutWrongPw).await
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
if let Err(e) = context.set_config(Config::NotifyAboutWrongPw, None).await {
warn!(context, "{}", e);
@@ -333,11 +339,11 @@ impl Imap {
if self.is_connected() && !self.should_reconnect() {
return Ok(());
}
if !context.is_configured().await {
if !context.is_configured().await? {
bail!("IMAP Connect without configured params");
}
let param = LoginParam::from_database(context, "configured_").await;
let param = LoginParam::from_database(context, "configured_").await?;
// the trailing underscore is correct
if let Err(err) = self
@@ -515,24 +521,29 @@ impl Imap {
// Write collected UIDs to SQLite database.
context
.sql
.with_conn(move |mut conn| {
let conn2 = &mut conn;
let tx = conn2.transaction()?;
tx.execute(
"UPDATE msgs SET server_uid=0 WHERE server_folder=?",
params![folder],
)?;
for (uid, rfc724_mid) in &msg_ids {
// This may detect previously undetected moved
// messages, so we update server_folder too.
tx.execute(
"UPDATE msgs \
SET server_folder=?,server_uid=? WHERE rfc724_mid=?",
params![folder, uid, rfc724_mid],
)?;
}
tx.commit()?;
Ok(())
.transaction(|conn| {
Box::pin(async move {
sqlx::query("UPDATE msgs SET server_uid=0 WHERE server_folder=?")
.bind(&folder)
.execute(&mut *conn)
.await?;
for (uid, rfc724_mid) in &msg_ids {
// This may detect previously undetected moved
// messages, so we update server_folder too.
sqlx::query(
"UPDATE msgs \
SET server_folder=?,server_uid=? WHERE rfc724_mid=?",
)
.bind(&folder)
.bind(uid)
.bind(rfc724_mid)
.execute(&mut *conn)
.await?;
}
Ok(())
})
})
.await?;
Ok(())
@@ -558,8 +569,8 @@ impl Imap {
.uid_validity
.with_context(|| format!("No UIDVALIDITY for folder {}", folder))?;
let old_uid_validity = get_uidvalidity(context, folder).await;
let old_uid_next = get_uid_next(context, folder).await;
let old_uid_validity = get_uidvalidity(context, folder).await?;
let old_uid_next = get_uid_next(context, folder).await?;
if new_uid_validity == old_uid_validity {
let new_emails = if newly_selected == NewlySelected::No {
@@ -569,6 +580,15 @@ impl Imap {
// new messages is only one command, just as a SELECT command)
true
} else if let Some(uid_next) = mailbox.uid_next {
if uid_next < old_uid_next {
warn!(
context,
"The server illegally decreased the uid_next of folder {} from {} to {} without changing validity ({}), resyncing UIDs...",
folder, old_uid_next, uid_next, new_uid_validity,
);
set_uid_next(context, folder, uid_next).await?;
job::schedule_resync(context).await;
}
uid_next != old_uid_next // If uid_next changed, there are new emails
} else {
true // We have no uid_next and if in doubt, return true
@@ -640,7 +660,7 @@ impl Imap {
folder: S,
fetch_existing_msgs: bool,
) -> Result<bool> {
let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await)
let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
.unwrap_or_default();
let new_emails = self
@@ -652,7 +672,7 @@ impl Imap {
return Ok(false);
}
let old_uid_next = get_uid_next(context, folder.as_ref()).await;
let old_uid_next = get_uid_next(context, folder.as_ref()).await?;
let msgs = if fetch_existing_msgs {
self.prefetch_existing_msgs().await?
@@ -715,7 +735,7 @@ impl Imap {
let new_uid_next = largest_uid_without_errors + 1;
if new_uid_next > old_uid_next {
set_uid_next(context, &folder, new_uid_next).await?;
set_uid_next(context, folder, new_uid_next).await?;
}
if read_errors == 0 {
@@ -739,7 +759,7 @@ impl Imap {
let session = self.session.as_mut().unwrap();
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.ok_or_else(|| format_err!("Not configured"))?;
let search_command = format!("FROM \"{}\"", self_addr);
@@ -762,17 +782,12 @@ impl Imap {
let msg = fetch?;
match get_fetch_headers(&msg) {
Ok(headers) => {
let (from_id, _, _) = from_field_to_contact_id(
context,
&mimeparser::get_from(&headers),
false,
)
.await?;
if from_id == DC_CONTACT_ID_SELF {
result.extend(mimeparser::get_recipients(&headers));
if let Some(from) = mimeparser::get_from(&headers).first() {
if context.is_self_addr(&from.addr).await? {
result.extend(mimeparser::get_recipients(&headers));
}
}
}
Err(err) => {
warn!(context, "{}", err);
continue;
@@ -932,7 +947,7 @@ impl Imap {
match dc_receive_imf_inner(
&context,
&body,
body,
&folder,
server_uid,
is_seen,
@@ -1266,10 +1281,7 @@ impl Imap {
context: &Context,
create_mvbox: bool,
) -> Result<()> {
let folders_configured = context
.sql
.get_raw_config_int(context, "folders_configured")
.await;
let folders_configured = context.sql.get_raw_config_int("folders_configured").await?;
if folders_configured.unwrap_or_default() >= DC_FOLDERS_CONFIGURED_VERSION {
return Ok(());
}
@@ -1311,7 +1323,7 @@ impl Imap {
}
let folder_meaning = get_folder_meaning(&folder);
let folder_name_meaning = get_folder_meaning_by_name(&folder.name());
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
if folder.name() == "DeltaChat" {
// Always takes precendent
mvbox_folder = Some(folder.name().to_string());
@@ -1397,7 +1409,7 @@ impl Imap {
}
context
.sql
.set_raw_config_int(context, "folders_configured", DC_FOLDERS_CONFIGURED_VERSION)
.set_raw_config_int("folders_configured", DC_FOLDERS_CONFIGURED_VERSION)
.await?;
}
info!(context, "FINISHED configuring IMAP-folders.");
@@ -1501,7 +1513,7 @@ async fn precheck_imf(
server_uid: u32,
) -> Result<bool> {
if let Some((old_server_folder, old_server_uid, msg_id)) =
message::rfc724_mid_exists(context, &rfc724_mid).await?
message::rfc724_mid_exists(context, rfc724_mid).await?
{
if old_server_folder.is_empty() && old_server_uid == 0 {
info!(
@@ -1509,7 +1521,7 @@ async fn precheck_imf(
"[move] detected bcc-self {} as {}/{}", rfc724_mid, server_folder, server_uid
);
let delete_server_after = context.get_config_delete_server_after().await;
let delete_server_after = context.get_config_delete_server_after().await?;
if delete_server_after != Some(0) {
if msg_id
@@ -1594,7 +1606,9 @@ fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>>
}
fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Result<String> {
if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId) {
if let Some(message_id) = headers.get_header_value(HeaderDef::XMicrosoftOriginalMessageId) {
Ok(crate::mimeparser::parse_message_id(&message_id)?)
} else if let Some(message_id) = headers.get_header_value(HeaderDef::MessageId) {
Ok(crate::mimeparser::parse_message_id(&message_id)?)
} else {
bail!("prefetch: No message ID found");
@@ -1609,7 +1623,7 @@ pub(crate) async fn prefetch_should_download(
let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
let parent = get_prefetch_parent_message(context, headers).await?;
let is_reply_to_chat_message = parent.is_some();
if let Some(parent) = parent {
if let Some(parent) = &parent {
let chat = chat::Chat::load_from_db(context, parent.get_chat_id()).await?;
if chat.typ == Chattype::Group {
// This might be a group command, like removing a group member.
@@ -1655,6 +1669,7 @@ pub(crate) async fn prefetch_should_download(
}
ShowEmails::All => true,
};
let should_download = (show && !blocked_contact) || maybe_ndn;
Ok(should_download)
}
@@ -1667,7 +1682,7 @@ async fn message_needs_processing(
folder: &str,
show_emails: ShowEmails,
) -> bool {
let skip = match precheck_imf(context, &msg_id, folder, current_uid).await {
let skip = match precheck_imf(context, msg_id, folder, current_uid).await {
Ok(skip) => skip,
Err(err) => {
warn!(context, "precheck_imf error: {}", err);
@@ -1687,7 +1702,7 @@ async fn message_needs_processing(
// we do not know the message-id
// or the message-id is missing (in this case, we create one in the further process)
// or some other error happened
let show = match prefetch_should_download(context, &headers, show_emails).await {
let show = match prefetch_should_download(context, headers, show_emails).await {
Ok(show) => show,
Err(err) => {
warn!(context, "prefetch_should_download error: {}", err);
@@ -1717,9 +1732,15 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
context
.sql
.execute(
"INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?)
sqlx::query(
"INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?)
ON CONFLICT(folder) DO UPDATE SET uid_next=? WHERE folder=?;",
paramsv![folder, 0u32, uid_next, uid_next, folder],
)
.bind(folder)
.bind(0i32)
.bind(uid_next as i64)
.bind(uid_next as i64)
.bind(folder),
)
.await?;
Ok(())
@@ -1730,16 +1751,12 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
/// This method returns the uid_next from the last time we fetched messages.
/// We can compare this to the current uid_next to find out whether there are new messages
/// and fetch from this value on to get all new messages.
async fn get_uid_next(context: &Context, folder: &str) -> u32 {
context
async fn get_uid_next(context: &Context, folder: &str) -> Result<u32> {
Ok(context
.sql
.query_get_value(
context,
"SELECT uid_next FROM imap_sync WHERE folder=?;",
paramsv![folder],
)
.await
.unwrap_or(0)
.query_get_value(sqlx::query("SELECT uid_next FROM imap_sync WHERE folder=?;").bind(folder))
.await?
.unwrap_or(0))
}
pub(crate) async fn set_uidvalidity(
@@ -1750,38 +1767,45 @@ pub(crate) async fn set_uidvalidity(
context
.sql
.execute(
"INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?)
sqlx::query(
"INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?)
ON CONFLICT(folder) DO UPDATE SET uidvalidity=? WHERE folder=?;",
paramsv![folder, uidvalidity, 0u32, uidvalidity, folder],
)
.bind(folder)
.bind(uidvalidity as i32)
.bind(0i32)
.bind(uidvalidity as i32)
.bind(folder),
)
.await?;
Ok(())
}
async fn get_uidvalidity(context: &Context, folder: &str) -> u32 {
context
async fn get_uidvalidity(context: &Context, folder: &str) -> Result<u32> {
Ok(context
.sql
.query_get_value(
context,
"SELECT uidvalidity FROM imap_sync WHERE folder=?;",
paramsv![folder],
sqlx::query("SELECT uidvalidity FROM imap_sync WHERE folder=?;").bind(folder),
)
.await
.unwrap_or(0)
.await?
.unwrap_or(0))
}
/// Deprecated, use get_uid_next() and get_uidvalidity()
pub async fn get_config_last_seen_uid<S: AsRef<str>>(context: &Context, folder: S) -> (u32, u32) {
pub async fn get_config_last_seen_uid<S: AsRef<str>>(
context: &Context,
folder: S,
) -> Result<(u32, u32)> {
let key = format!("imap.mailbox.{}", folder.as_ref());
if let Some(entry) = context.sql.get_raw_config(context, &key).await {
if let Some(entry) = context.sql.get_raw_config(&key).await? {
// the entry has the format `imap.mailbox.<folder>=<uidvalidity>:<lastseenuid>`
let mut parts = entry.split(':');
(
Ok((
parts.next().unwrap_or_default().parse().unwrap_or(0),
parts.next().unwrap_or_default().parse().unwrap_or(0),
)
))
} else {
(0, 0)
Ok((0, 0))
}
}
@@ -1876,17 +1900,17 @@ mod tests {
#[async_std::test]
async fn test_set_uid_next_validity() {
let t = TestContext::new_alice().await;
assert_eq!(get_uid_next(&t.ctx, "Inbox").await, 0);
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await, 0);
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 0);
set_uidvalidity(&t.ctx, "Inbox", 7).await.unwrap();
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await, 7);
assert_eq!(get_uid_next(&t.ctx, "Inbox").await, 0);
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 7);
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
set_uid_next(&t.ctx, "Inbox", 5).await.unwrap();
set_uidvalidity(&t.ctx, "Inbox", 6).await.unwrap();
assert_eq!(get_uid_next(&t.ctx, "Inbox").await, 5);
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await, 6);
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 5);
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 6);
}
#[test]

View File

@@ -17,10 +17,9 @@ impl Imap {
let elapsed_secs = last_scan.elapsed().as_secs();
let debounce_secs = context
.get_config_u64(Config::ScanAllFoldersDebounceSecs)
.await;
.await?;
if elapsed_secs < debounce_secs {
info!(context, "Not scanning, we scanned {}s ago", elapsed_secs);
return Ok(());
}
}
@@ -45,17 +44,8 @@ impl Imap {
};
let foldername = folder.name();
if watched_folders.contains(&foldername.to_string()) {
info!(
context,
"Not scanning folder {} as it is watched anyway", foldername
);
continue;
}
info!(context, "Scanning folder: {}", foldername);
let folder_meaning = get_folder_meaning(&folder);
let folder_name_meaning = get_folder_meaning_by_name(&foldername);
let folder_name_meaning = get_folder_meaning_by_name(foldername);
if folder_meaning == FolderMeaning::SentObjects {
// Always takes precedent
@@ -71,8 +61,11 @@ impl Imap {
spam_folder = Some(folder.name().to_string());
}
if let Err(e) = self.fetch_new_messages(context, foldername, false).await {
warn!(context, "Can't fetch new msgs in scanned folder: {:#}", e);
// Don't scan folders that are watched anyway
if !watched_folders.contains(&foldername.to_string()) {
if let Err(e) = self.fetch_new_messages(context, foldername, false).await {
warn!(context, "Can't fetch new msgs in scanned folder: {:#}", e);
}
}
}
@@ -96,8 +89,8 @@ async fn get_watched_folders(context: &Context) -> Vec<String> {
(Config::InboxWatch, Config::ConfiguredInboxFolder),
];
for (watched, configured) in folder_watched_configured {
if context.get_config_bool(*watched).await {
if let Some(folder) = context.get_config(*configured).await {
if context.get_config_bool(*watched).await.unwrap_or_default() {
if let Ok(Some(folder)) = context.get_config(*configured).await {
res.push(folder);
}
}

View File

@@ -10,8 +10,8 @@ use async_std::{
prelude::*,
};
use rand::{thread_rng, Rng};
use sqlx::Row;
use crate::blob::BlobObject;
use crate::chat;
use crate::chat::delete_and_reset_all_device_msgs;
use crate::config::Config;
@@ -30,6 +30,7 @@ use crate::param::Param;
use crate::pgp;
use crate::sql::{self, Sql};
use crate::stock_str;
use crate::{blob::BlobObject, log::LogExt};
use ::pgp::types::KeyTrait;
use async_tar::Archive;
@@ -38,7 +39,7 @@ const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite";
const BLOBS_BACKUP_NAME: &str = "blobs_backup";
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(i32)]
#[repr(u32)]
pub enum ImexMode {
/// Export all private keys and all public keys of the user to the
/// directory given as `param1`. The default key is written to the files `public-key-default.asc`
@@ -158,6 +159,7 @@ pub async fn has_backup_old(context: &Context, dir_name: impl AsRef<Path>) -> Re
let dir_name = dir_name.as_ref();
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
let mut newest_backup_time = 0;
let mut newest_backup_name = "".to_string();
let mut newest_backup_path: Option<PathBuf> = None;
while let Some(dirent) = dir_iter.next().await {
if let Ok(dirent) = dirent {
@@ -169,8 +171,8 @@ pub async fn has_backup_old(context: &Context, dir_name: impl AsRef<Path>) -> Re
match sql.open(context, &path, true).await {
Ok(_) => {
let curr_backup_time = sql
.get_raw_config_int(context, "backup_time")
.await
.get_raw_config_int("backup_time")
.await?
.unwrap_or_default();
if curr_backup_time > newest_backup_time {
newest_backup_path = Some(path);
@@ -179,10 +181,22 @@ pub async fn has_backup_old(context: &Context, dir_name: impl AsRef<Path>) -> Re
info!(context, "backup_time of {} is {}", name, curr_backup_time);
sql.close().await;
}
Err(e) => warn!(
context,
"Found backup file {} which could not be opened: {}", name, e
),
Err(e) => {
warn!(
context,
"Found backup file {} which could not be opened: {}", name, e
);
// On some Android devices we can't open sql files that are not in our private directory
// (see https://github.com/deltachat/deltachat-android/issues/1768). So, compare names
// to still find the newest backup.
let name: String = name.into();
if newest_backup_time == 0
&& (newest_backup_name.is_empty() || name > newest_backup_name)
{
newest_backup_path = Some(path);
newest_backup_name = name;
}
}
}
}
}
@@ -258,12 +272,12 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
bail!("Passphrase must be at least 2 chars long.");
};
let private_key = SignedSecretKey::load_self(context).await?;
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await {
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await? {
false => None,
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
};
let private_key_asc = private_key.to_asc(ac_headers);
let encr = pgp::symm_encrypt(&passphrase, private_key_asc.as_bytes()).await?;
let encr = pgp::symm_encrypt(passphrase, private_key_asc.as_bytes()).await?;
let replacement = format!(
concat!(
@@ -320,7 +334,7 @@ pub fn create_setup_code(_context: &Context) -> String {
}
async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
if !context.sql.get_raw_config_bool(context, "bcc_self").await {
if !context.sql.get_raw_config_bool("bcc_self").await? {
let mut msg = Message::new(Viewtype::Text);
// TODO: define this as a stockstring once the wording is settled.
msg.text = Some(
@@ -381,7 +395,7 @@ async fn set_self_key(
};
context
.sql
.set_raw_config_int(context, "e2ee_enabled", e2ee_enabled)
.set_raw_config_int("e2ee_enabled", e2ee_enabled)
.await?;
}
None => {
@@ -391,7 +405,7 @@ async fn set_self_key(
}
};
let self_addr = context.get_config(Config::ConfiguredAddr).await;
let self_addr = context.get_config(Config::ConfiguredAddr).await?;
ensure!(self_addr.is_some(), "Missing self addr");
let addr = EmailAddress::new(&self_addr.unwrap_or_default())?;
let keypair = pgp::KeyPair {
@@ -480,9 +494,13 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
);
ensure!(
!context.is_configured().await,
!context.is_configured().await?,
"Cannot import backups to accounts in use."
);
ensure!(
!context.scheduler.read().await.is_running(),
"cannot import backup, IO already running"
);
context.sql.close().await;
dc_delete_file(context, context.get_dbfile()).await;
ensure!(
@@ -529,11 +547,11 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
context
.sql
.open(&context, &context.get_dbfile(), false)
.open(context, &context.get_dbfile(), false)
.await
.context("Could not re-open db")?;
delete_and_reset_all_device_msgs(&context).await?;
delete_and_reset_all_device_msgs(context).await?;
Ok(())
}
@@ -547,9 +565,13 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
);
ensure!(
!context.is_configured().await,
!context.is_configured().await?,
"Cannot import backups to accounts in use."
);
ensure!(
!context.scheduler.read().await.is_running(),
"cannot import backup, IO already running"
);
context.sql.close().await;
dc_delete_file(context, context.get_dbfile()).await;
ensure!(
@@ -565,17 +587,17 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
/* re-open copied database file */
context
.sql
.open(&context, &context.get_dbfile(), false)
.open(context, &context.get_dbfile(), false)
.await
.context("Could not re-open db")?;
delete_and_reset_all_device_msgs(&context).await?;
delete_and_reset_all_device_msgs(context).await?;
let total_files_cnt = context
.sql
.query_get_value::<isize>(context, "SELECT COUNT(*) FROM backup_blobs;", paramsv![])
.await
.unwrap_or_default() as usize;
.count(sqlx::query("SELECT COUNT(*) FROM backup_blobs;"))
.await?;
info!(
context,
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
@@ -585,29 +607,25 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
// consuming too much memory.
let file_ids = context
.sql
.query_map(
"SELECT id FROM backup_blobs ORDER BY id",
paramsv![],
|row| row.get(0),
|ids| {
ids.collect::<std::result::Result<Vec<i64>, _>>()
.map_err(Into::into)
},
)
.fetch(sqlx::query("SELECT id FROM backup_blobs ORDER BY id"))
.await?
.map(|row| row?.try_get(0))
.collect::<sqlx::Result<Vec<i64>>>()
.await?;
let mut all_files_extracted = true;
for (processed_files_cnt, file_id) in file_ids.into_iter().enumerate() {
// Load a single blob into memory
let (file_name, file_blob) = context
let row = context
.sql
.query_row(
"SELECT file_name, file_content FROM backup_blobs WHERE id = ?",
paramsv![file_id],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?)),
.fetch_one(
sqlx::query("SELECT file_name, file_content FROM backup_blobs WHERE id = ?")
.bind(file_id),
)
.await?;
let file_name: String = row.try_get(0)?;
let file_blob: &[u8] = row.try_get(1)?;
if context.shall_stop_ongoing().await {
all_files_extracted = false;
break;
@@ -625,16 +643,16 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
}
let path_filename = context.get_blobdir().join(file_name);
dc_write_file(context, &path_filename, &file_blob).await?;
dc_write_file(context, &path_filename, file_blob).await?;
}
if all_files_extracted {
// only delete backup_blobs if all files were successfully extracted
context
.sql
.execute("DROP TABLE backup_blobs;", paramsv![])
.execute(sqlx::query("DROP TABLE backup_blobs;"))
.await?;
context.sql.execute("VACUUM;", paramsv![]).await.ok();
context.sql.execute(sqlx::query("VACUUM;")).await.ok();
Ok(())
} else {
bail!("received stop signal");
@@ -653,16 +671,21 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
context
.sql
.set_raw_config_int(context, "backup_time", now as i32)
.set_raw_config_int("backup_time", now as i32)
.await?;
sql::housekeeping(context).await;
sql::housekeeping(context).await.ok_or_log(context);
context
.sql
.execute("VACUUM;", paramsv![])
.execute(sqlx::query("VACUUM;"))
.await
.map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e));
ensure!(
!context.scheduler.read().await.is_running(),
"cannot export backup, IO already running"
);
// we close the database during the export
context.sql.close().await;
@@ -678,7 +701,7 @@ async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
// we re-open the database after export is finished
context
.sql
.open(&context, &context.get_dbfile(), false)
.open(context, &context.get_dbfile(), false)
.await;
match &res {
@@ -807,29 +830,26 @@ async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
async fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
let mut export_errors = 0;
let keys = context
let mut keys = context
.sql
.query_map(
.fetch(sqlx::query(
"SELECT id, public_key, private_key, is_default FROM keypairs;",
paramsv![],
|row| {
let id = row.get(0)?;
let public_key_blob: Vec<u8> = row.get(1)?;
let public_key = SignedPublicKey::from_slice(&public_key_blob);
let private_key_blob: Vec<u8> = row.get(2)?;
let private_key = SignedSecretKey::from_slice(&private_key_blob);
let is_default: i32 = row.get(3)?;
))
.await?
.map(|row| -> sqlx::Result<_> {
let row = row?;
let id = row.try_get(0)?;
let public_key_blob: &[u8] = row.try_get(1)?;
let public_key = SignedPublicKey::from_slice(public_key_blob);
let private_key_blob: &[u8] = row.try_get(2)?;
let private_key = SignedSecretKey::from_slice(private_key_blob);
let is_default: i32 = row.try_get(3)?;
Ok((id, public_key, private_key, is_default))
},
|keys| {
keys.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
Ok((id, public_key, private_key, is_default))
});
for (id, public_key, private_key, is_default) in keys {
while let Some(parts) = keys.next().await {
let (id, public_key, private_key, is_default) = parts?;
let id = Some(id).filter(|_| is_default != 0);
if let Ok(key) = public_key {
if export_key_to_asc_file(context, &dir, id, &key)

View File

@@ -2,16 +2,17 @@
//!
//! This module implements a job queue maintained in the SQLite database
//! and job types.
use std::fmt;
use std::future::Future;
use std::{fmt, time::Duration};
use anyhow::{bail, ensure, format_err, Context as _, Error, Result};
use async_smtp::smtp::response::{Category, Code, Detail};
use deltachat_derive::{FromSql, ToSql};
use async_std::prelude::*;
use async_std::task::sleep;
use itertools::Itertools;
use rand::{thread_rng, Rng};
use sqlx::Row;
use crate::context::Context;
use crate::dc_tools::{dc_delete_file, dc_read_file, time};
use crate::ephemeral::load_imap_deletion_msgid;
use crate::events::EventType;
@@ -29,14 +30,15 @@ use crate::{
};
use crate::{config::Config, constants::Blocked};
use crate::{constants::Chattype, contact::Contact};
use crate::{context::Context, log::LogExt};
use crate::{scheduler::InterruptInfo, sql};
// results in ~3 weeks for the last backoff timespan
const JOB_RETRIES: u32 = 17;
/// Thread IDs
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(i32)]
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)]
#[repr(u32)]
pub(crate) enum Thread {
Unknown = 0,
Imap = 100,
@@ -73,19 +75,9 @@ impl Default for Thread {
}
#[derive(
Debug,
Display,
Copy,
Clone,
PartialEq,
Eq,
PartialOrd,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Debug, Display, Copy, Clone, PartialEq, Eq, PartialOrd, FromPrimitive, ToPrimitive, sqlx::Type,
)]
#[repr(i32)]
#[repr(u32)]
pub enum Action {
Unknown = 0,
@@ -138,7 +130,7 @@ impl From<Action> for Thread {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct Job {
pub job_id: u32,
pub action: Action,
@@ -181,7 +173,7 @@ impl Job {
if self.job_id != 0 {
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32])
.execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(self.job_id as i32))
.await?;
}
@@ -200,26 +192,24 @@ impl Job {
context
.sql
.execute(
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
paramsv![
self.desired_timestamp,
self.tries as i64,
self.param.to_string(),
self.job_id as i32,
],
sqlx::query(
"UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;",
)
.bind(self.desired_timestamp)
.bind(self.tries as i64)
.bind(self.param.to_string())
.bind(self.job_id as i32),
)
.await?;
} else {
context.sql.execute(
"INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);",
paramsv![
self.added_timestamp,
thread,
self.action,
self.foreign_id,
self.param.to_string(),
self.desired_timestamp
]
sqlx::query("INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);")
.bind(self.added_timestamp)
.bind(thread)
.bind(self.action)
.bind(self.foreign_id)
.bind(self.param.to_string())
.bind(self.desired_timestamp)
).await?;
}
@@ -250,7 +240,7 @@ impl Job {
let status = match smtp.send(context, recipients, message, job_id).await {
Err(crate::smtp::send::Error::SendError(err)) => {
// Remote error, retry later.
warn!(context, "SMTP failed to send: {}", err);
warn!(context, "SMTP failed to send: {:?}", err);
self.pending_error = Some(err.to_string());
let res = match err {
@@ -336,6 +326,12 @@ impl Job {
error!(context, "SMTP job failed because SMTP has no transport");
Status::Finished(Err(format_err!("SMTP has not transport")))
}
Err(crate::smtp::send::Error::Other(err)) => {
// Local error, job is invalid, do not retry.
smtp.disconnect().await;
warn!(context, "unable to load job: {}", err);
Status::Finished(Err(err))
}
Ok(()) => {
job_try!(success_cb().await);
Status::Finished(Ok(()))
@@ -384,11 +380,21 @@ impl Job {
/* if there is a msg-id and it does not exist in the db, cancel sending.
this happends if dc_delete_msgs() was called
before the generated mime was sent out */
if 0 != self.foreign_id && !message::exists(context, MsgId::new(self.foreign_id)).await {
return Status::Finished(Err(format_err!(
"Not sending Message {} as it was deleted",
self.foreign_id
)));
if 0 != self.foreign_id {
match message::exists(context, MsgId::new(self.foreign_id)).await {
Ok(exists) => {
if !exists {
return Status::Finished(Err(format_err!(
"Not sending Message {} as it was deleted",
self.foreign_id
)));
}
}
Err(err) => {
warn!(context, "failed to check message existence: {:?}", err);
return Status::RetryLater;
}
}
};
let foreign_id = self.foreign_id;
@@ -396,7 +402,7 @@ impl Job {
async move {
// smtp success, update db ASAP, then delete smtp file
if 0 != foreign_id {
set_delivered(context, MsgId::new(foreign_id)).await;
set_delivered(context, MsgId::new(foreign_id)).await?;
}
// now also delete the generated file
dc_delete_file(context, filename).await;
@@ -413,44 +419,38 @@ impl Job {
contact_id: u32,
) -> sql::Result<(Vec<u32>, Vec<String>)> {
// Extract message IDs from job parameters
let res: Vec<(u32, MsgId)> = context
let mut rows = context
.sql
.query_map(
"SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?",
paramsv![contact_id, self.job_id],
|row| {
let job_id: u32 = row.get(0)?;
let params_str: String = row.get(1)?;
let params: Params = params_str.parse().unwrap_or_default();
Ok((job_id, params))
},
|jobs| {
let res = jobs
.filter_map(|row| {
let (job_id, params) = row.ok()?;
let msg_id = params.get_msg_id()?;
Some((job_id, msg_id))
})
.collect();
Ok(res)
},
.fetch(
sqlx::query("SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?")
.bind(contact_id)
.bind(self.job_id),
)
.await?;
// Load corresponding RFC724 message IDs
let mut job_ids = Vec::new();
let mut rfc724_mids = Vec::new();
for (job_id, msg_id) in res {
if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id).await {
job_ids.push(job_id);
rfc724_mids.push(rfc724_mid);
while let Some(row) = rows.next().await {
let row = row?;
let job_id: u32 = row.try_get(0)?;
let params_str: String = row.try_get(1)?;
let params: Params = params_str.parse().unwrap_or_default();
if let Some(msg_id) = params.get_msg_id() {
if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id).await
{
job_ids.push(job_id);
rfc724_mids.push(rfc724_mid);
}
}
}
Ok((job_ids, rfc724_mids))
}
async fn send_mdn(&mut self, context: &Context, smtp: &mut Smtp) -> Status {
if !context.get_config_bool(Config::MdnsEnabled).await {
let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await);
if !mdns_enabled {
// User has disabled MDNs after job scheduling but before
// execution.
return Status::Finished(Err(format_err!("MDNs are disabled")));
@@ -488,7 +488,7 @@ impl Job {
let msg = job_try!(Message::load_from_db(context, msg_id).await);
let mimefactory =
job_try!(MimeFactory::from_mdn(context, &msg, additional_rfc724_mids).await);
let rendered_msg = job_try!(mimefactory.render().await);
let rendered_msg = job_try!(mimefactory.render(context).await);
let body = rendered_msg.message;
let addr = contact.get_addr();
@@ -536,7 +536,13 @@ impl Job {
);
return Status::Finished(Ok(()));
}
Ok(Some(config)) => context.get_config(config).await,
Ok(Some(config)) => match context.get_config(config).await {
Ok(folder) => folder,
Err(err) => {
warn!(context, "failed to load config: {}", err);
return Status::RetryLater;
}
},
};
if let Some(dest_folder) = dest_folder {
@@ -629,6 +635,7 @@ impl Job {
// Hidden messages are similar to trashed, but are
// related to some chat. We also delete their
// database records.
info!(context, "verbose (issue 2335): will delete from db");
job_try!(msg.id.delete_from_db(context).await)
} else {
// Remove server UID from the database record.
@@ -639,6 +646,7 @@ impl Job {
// we remove UID to reduce the number of messages
// pointing to the corresponding UID. Once the counter
// reaches zero, we will remove the message.
info!(context, "verbose (issue 2335): will unlink");
job_try!(msg.id.unlink(context).await);
}
Status::Finished(Ok(()))
@@ -654,7 +662,7 @@ impl Job {
/// Then, Fetch the last messages DC_FETCH_EXISTING_MSGS_COUNT emails from the server
/// and show them in the chat list.
async fn fetch_existing_msgs(&mut self, context: &Context, imap: &mut Imap) -> Status {
if context.get_config_bool(Config::Bot).await {
if job_try!(context.get_config_bool(Config::Bot).await) {
return Status::Finished(Ok(())); // Bots don't want those messages
}
if let Err(err) = imap.connect_configured(context).await {
@@ -666,13 +674,13 @@ impl Job {
add_all_recipients_as_contacts(context, imap, Config::ConfiguredMvboxFolder).await;
add_all_recipients_as_contacts(context, imap, Config::ConfiguredInboxFolder).await;
if context.get_config_bool(Config::FetchExistingMsgs).await {
if job_try!(context.get_config_bool(Config::FetchExistingMsgs).await) {
for config in &[
Config::ConfiguredMvboxFolder,
Config::ConfiguredInboxFolder,
Config::ConfiguredSentboxFolder,
] {
if let Some(folder) = context.get_config(*config).await {
if let Some(folder) = job_try!(context.get_config(*config).await) {
if let Err(e) = imap.fetch_new_messages(context, folder, true).await {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
warn!(context, "Could not fetch messages, retrying: {:#}", e);
@@ -682,10 +690,10 @@ impl Job {
}
}
// Make sure that if there now is a chat with a contact (created by an outgoing message), then group contact requests
// from this contact should also be unblocked.
// Make sure that if there now is a chat with a contact (created by an outgoing
// message), then group contact requests from this contact should also be unblocked.
// See https://github.com/deltachat/deltachat-core-rust/issues/2097.
for item in chat::get_chat_msgs(context, ChatId::new(DC_CHAT_ID_DEADDROP), 0, None).await {
for item in job_try!(chat::get_chat_msgs(context, DC_CHAT_ID_DEADDROP, 0, None).await) {
if let ChatItem::Message { msg_id } = item {
let msg = match Message::load_from_db(context, msg_id).await {
Err(e) => {
@@ -703,14 +711,8 @@ impl Job {
};
match chat.typ {
Chattype::Group | Chattype::Mailinglist => {
// The next lines are actually what we do in
let (test_normal_chat_id, test_normal_chat_id_blocked) =
chat::lookup_by_contact_id(context, msg.from_id)
.await
.unwrap_or_default();
if !test_normal_chat_id.is_unset()
&& test_normal_chat_id_blocked == Blocked::Not
if let Ok((_1to1_chat, Blocked::Not)) =
chat::lookup_by_contact_id(context, msg.from_id).await
{
chat.id.unblock(context).await;
}
@@ -739,26 +741,21 @@ impl Job {
return Status::RetryLater;
}
if let Some(sentbox_folder) = &context.get_config(Config::ConfiguredSentboxFolder).await {
job_try!(
imap.resync_folder_uids(context, sentbox_folder.to_string())
.await
);
let sentbox_folder = job_try!(context.get_config(Config::ConfiguredSentboxFolder).await);
if let Some(sentbox_folder) = sentbox_folder {
job_try!(imap.resync_folder_uids(context, sentbox_folder).await);
}
if let Some(inbox_folder) = &context.get_config(Config::ConfiguredInboxFolder).await {
job_try!(
imap.resync_folder_uids(context, inbox_folder.to_string())
.await
);
let inbox_folder = job_try!(context.get_config(Config::ConfiguredInboxFolder).await);
if let Some(inbox_folder) = inbox_folder {
job_try!(imap.resync_folder_uids(context, inbox_folder).await);
}
if let Some(mvbox_folder) = &context.get_config(Config::ConfiguredMvboxFolder).await {
job_try!(
imap.resync_folder_uids(context, mvbox_folder.to_string())
.await
);
let mvbox_folder = job_try!(context.get_config(Config::ConfiguredMvboxFolder).await);
if let Some(mvbox_folder) = mvbox_folder {
job_try!(imap.resync_folder_uids(context, mvbox_folder).await);
}
Status::Finished(Ok(()))
}
@@ -806,11 +803,13 @@ impl Job {
// the name sent in the From field by the user.
if msg.param.get_bool(Param::WantsMdn).unwrap_or_default()
&& !msg.is_system_message()
&& context.get_config_bool(Config::MdnsEnabled).await
{
if let Err(err) = send_mdn(context, &msg).await {
warn!(context, "could not send out mdn for {}: {}", msg.id, err);
return Status::Finished(Err(err));
let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await);
if mdns_enabled {
if let Err(err) = send_mdn(context, &msg).await {
warn!(context, "could not send out mdn for {}: {}", msg.id, err);
return Status::Finished(Err(err));
}
}
}
Status::Finished(Ok(()))
@@ -823,50 +822,46 @@ impl Job {
pub async fn kill_action(context: &Context, action: Action) -> bool {
context
.sql
.execute("DELETE FROM jobs WHERE action=?;", paramsv![action])
.execute(sqlx::query("DELETE FROM jobs WHERE action=?;").bind(action))
.await
.is_ok()
}
/// Remove jobs with specified IDs.
async fn kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> {
context
.sql
.execute(
format!(
"DELETE FROM jobs WHERE id IN({})",
job_ids.iter().map(|_| "?").join(",")
),
job_ids.iter().map(|i| i as &dyn crate::ToSql).collect(),
)
.await?;
let q = format!(
"DELETE FROM jobs WHERE id IN({})",
job_ids.iter().map(|_| "?").join(",")
);
let mut query = sqlx::query(&q);
for id in job_ids {
query = query.bind(*id);
}
context.sql.execute(query).await?;
Ok(())
}
pub async fn action_exists(context: &Context, action: Action) -> bool {
context
.sql
.exists("SELECT id FROM jobs WHERE action=?;", paramsv![action])
.exists(sqlx::query("SELECT COUNT(*) FROM jobs WHERE action=?;").bind(action))
.await
.unwrap_or_default()
}
async fn set_delivered(context: &Context, msg_id: MsgId) {
async fn set_delivered(context: &Context, msg_id: MsgId) -> Result<()> {
message::update_msg_state(context, msg_id, MessageState::OutDelivered).await;
let chat_id: ChatId = context
.sql
.query_get_value(
context,
"SELECT chat_id FROM msgs WHERE id=?",
paramsv![msg_id],
)
.await
.query_get_value(sqlx::query("SELECT chat_id FROM msgs WHERE id=?").bind(msg_id))
.await?
.unwrap_or_default();
context.emit_event(EventType::MsgDelivered { chat_id, msg_id });
Ok(())
}
async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, folder: Config) {
let mailbox = if let Some(m) = context.get_config(folder).await {
let mailbox = if let Ok(Some(m)) = context.get_config(folder).await {
m
} else {
return;
@@ -936,14 +931,14 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
let from = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.unwrap_or_default();
let lowercase_from = from.to_lowercase();
// Send BCC to self if it is enabled and we are not going to
// delete it immediately.
if context.get_config_bool(Config::BccSelf).await
&& context.get_config_delete_server_after().await != Some(0)
if context.get_config_bool(Config::BccSelf).await?
&& context.get_config_delete_server_after().await? != Some(0)
&& !recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
@@ -957,11 +952,11 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
context,
"message {} has no recipient, skipping smtp-send", msg_id
);
set_delivered(context, msg_id).await;
set_delivered(context, msg_id).await?;
return Ok(None);
}
let rendered_msg = match mimefactory.render().await {
let rendered_msg = match mimefactory.render(context).await {
Ok(res) => Ok(res),
Err(err) => {
message::set_msg_failed(context, msg_id, Some(err.to_string())).await;
@@ -1022,7 +1017,10 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
param.set(Param::File, blob.as_name());
param.set(Param::Recipients, &recipients);
let job = create(Action::SendMsgToSmtp, msg_id.to_u32() as i32, param, 0)?;
msg.subject = rendered_msg.subject.clone();
msg.update_subject(context).await;
let job = create(Action::SendMsgToSmtp, msg_id.to_u32(), param, 0)?;
Ok(Some(job))
}
@@ -1032,8 +1030,9 @@ pub(crate) enum Connection<'a> {
Smtp(&'a mut Smtp),
}
async fn load_imap_deletion_job(context: &Context) -> sql::Result<Option<Job>> {
pub(crate) async fn load_imap_deletion_job(context: &Context) -> sql::Result<Option<Job>> {
let res = if let Some(msg_id) = load_imap_deletion_msgid(context).await? {
info!(context, "verbose (issue 2335): loading imap deletion job");
Some(Job::new(
Action::DeleteMsgOnImap,
msg_id.to_u32(),
@@ -1119,7 +1118,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
if let Err(err) = res {
warn!(
context,
"{} removes job {} as it failed with error {:?}", &connection, job, err
"{} removes job {} as it failed with error {:#}", &connection, job, err
);
} else {
info!(
@@ -1143,7 +1142,7 @@ async fn perform_job_action(
) -> Status {
info!(
context,
"{} begin immediate try {} of job {}", &connection, tries, job
"{} begin immediate try {} of job {:?} - verbose (issue 2335)", &connection, tries, job
);
let try_res = match job.action {
@@ -1160,7 +1159,7 @@ async fn perform_job_action(
Action::MoveMsg => job.move_msg(context, connection.inbox()).await,
Action::FetchExistingMsgs => job.fetch_existing_msgs(context, connection.inbox()).await,
Action::Housekeeping => {
sql::housekeeping(context).await;
sql::housekeeping(context).await.ok_or_log(context);
Status::Finished(Ok(()))
}
};
@@ -1200,13 +1199,13 @@ pub(crate) async fn schedule_resync(context: &Context) {
}
/// Creates a job.
pub fn create(action: Action, foreign_id: i32, param: Params, delay_seconds: i64) -> Result<Job> {
pub fn create(action: Action, foreign_id: u32, param: Params, delay_seconds: i64) -> Result<Job> {
ensure!(
action != Action::Unknown,
"Invalid action passed to job_add"
);
Ok(Job::new(action, foreign_id as u32, param, delay_seconds))
Ok(Job::new(action, foreign_id, param, delay_seconds))
}
/// Adds a job to the database, scheduling it.
@@ -1245,7 +1244,13 @@ pub async fn add(context: &Context, job: Job) {
}
async fn load_housekeeping_job(context: &Context) -> Option<Job> {
let last_time = context.get_config_i64(Config::LastHousekeeping).await;
let last_time = match context.get_config_i64(Config::LastHousekeeping).await {
Ok(last_time) => last_time,
Err(err) => {
warn!(context, "failed to load housekeeping config: {:?}", err);
return None;
}
};
let next_time = last_time + (60 * 60 * 24);
if next_time <= time() {
@@ -1269,65 +1274,88 @@ pub(crate) async fn load_next(
) -> Option<Job> {
info!(context, "loading job for {}-thread", thread);
let query;
let params;
while !context.sql.is_open().await {
// The db is closed, which means that this thread should not be running.
// Wait until the db is re-opened (if we returned None, this thread might do further damage)
warn!(
context,
"{}: load_next() was called but the db was not opened, THIS SHOULD NOT HAPPEN. Waiting...",
thread
);
sleep(Duration::from_millis(500)).await;
}
let t = time();
let m;
let thread_i = thread as i64;
if let Some(msg_id) = info.msg_id {
query = r#"
let get_query = || {
if let Some(msg_id) = info.msg_id {
sqlx::query(
r#"
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
FROM jobs
WHERE thread=? AND foreign_id=?
ORDER BY action DESC, added_timestamp
LIMIT 1;
"#;
m = msg_id;
params = paramsv![thread_i, m];
} else if !info.probe_network {
// processing for first-try and after backoff-timeouts:
// process jobs in the order they were added.
query = r#"
"#,
)
.bind(thread_i)
.bind(msg_id)
} else if !info.probe_network {
// processing for first-try and after backoff-timeouts:
// process jobs in the order they were added.
sqlx::query(
r#"
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
FROM jobs
WHERE thread=? AND desired_timestamp<=?
ORDER BY action DESC, added_timestamp
LIMIT 1;
"#;
params = paramsv![thread_i, t];
} else {
// processing after call to dc_maybe_network():
// process _all_ pending jobs that failed before
// in the order of their backoff-times.
query = r#"
"#,
)
.bind(thread_i)
.bind(t)
} else {
// processing after call to dc_maybe_network():
// process _all_ pending jobs that failed before
// in the order of their backoff-times.
sqlx::query(
r#"
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
FROM jobs
WHERE thread=? AND tries>0
ORDER BY desired_timestamp, action DESC
LIMIT 1;
"#;
params = paramsv![thread_i];
"#,
)
.bind(thread_i)
}
};
let job = loop {
let job_res = context
.sql
.query_row_optional(query, params.clone(), |row| {
let job = Job {
job_id: row.get("id")?,
action: row.get("action")?,
foreign_id: row.get("foreign_id")?,
desired_timestamp: row.get("desired_timestamp")?,
added_timestamp: row.get("added_timestamp")?,
tries: row.get("tries")?,
param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
pending_error: None,
};
Ok(job)
})
.await;
.fetch_optional(get_query())
.await
.and_then(|row| {
if let Some(row) = row {
Ok(Some(Job {
job_id: row.try_get("id")?,
action: row.try_get("action")?,
foreign_id: row.try_get("foreign_id")?,
desired_timestamp: row.try_get("desired_timestamp")?,
added_timestamp: row.try_get("added_timestamp")?,
tries: row.try_get::<i64, _>("tries")? as u32,
param: row
.try_get::<String, _>("param")?
.parse()
.unwrap_or_default(),
pending_error: None,
}))
} else {
Ok(None)
}
});
match job_res {
Ok(job) => break job,
@@ -1338,15 +1366,18 @@ LIMIT 1;
// TODO: improve by only doing a single query
match context
.sql
.query_row(query, params.clone(), |row| row.get::<_, i32>(0))
.fetch_one(get_query())
.await
.and_then(|row| row.try_get::<i32, _>(0).map_err(Into::into))
{
Ok(id) => {
context
if let Err(err) = context
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
.execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(id))
.await
.ok();
{
warn!(context, "failed to delete job {}: {:?}", id, err);
}
}
Err(err) => {
error!(context, "failed to retrieve invalid job from DB: {}", err);
@@ -1370,9 +1401,14 @@ LIMIT 1;
.unwrap_or_default()
.or(Some(job))
} else {
info!(context, "verbose (issue 2335): executing job normally");
Some(job)
}
} else if let Some(job) = load_imap_deletion_job(context).await.unwrap_or_default() {
info!(
context,
"verbose (issue 2335): loaded imap deletion job (no others queued)"
);
Some(job)
} else {
load_housekeeping_job(context).await
@@ -1388,22 +1424,22 @@ mod tests {
use crate::test_utils::TestContext;
async fn insert_job(context: &Context, foreign_id: i64) {
async fn insert_job(context: &Context, foreign_id: i64, valid: bool) {
let now = time();
context
.sql
.execute(
"INSERT INTO jobs
sqlx::query(
"INSERT INTO jobs
(added_timestamp, thread, action, foreign_id, param, desired_timestamp)
VALUES (?, ?, ?, ?, ?, ?);",
paramsv![
now,
Thread::from(Action::MoveMsg),
Action::MoveMsg,
foreign_id,
Params::new().to_string(),
now
],
)
.bind(now)
.bind(Thread::from(Action::MoveMsg))
.bind(if valid { Action::MoveMsg as i32 } else { -1 })
.bind(foreign_id)
.bind(Params::new().to_string())
.bind(now),
)
.await
.unwrap();
@@ -1415,7 +1451,7 @@ mod tests {
// fails to load from the database instead of failing to load
// all jobs.
let t = TestContext::new().await;
insert_job(&t, -1).await; // This can not be loaded into Job struct.
insert_job(&t, 1, false).await; // This can not be loaded into Job struct.
let jobs = load_next(
&t,
Thread::from(Action::MoveMsg),
@@ -1425,7 +1461,7 @@ mod tests {
// The housekeeping job should be loaded as we didn't run housekeeping in the last day:
assert!(jobs.unwrap().action == Action::Housekeeping);
insert_job(&t, 1).await;
insert_job(&t, 1, true).await;
let jobs = load_next(
&t,
Thread::from(Action::MoveMsg),
@@ -1439,7 +1475,7 @@ mod tests {
async fn test_load_next_job_one() {
let t = TestContext::new().await;
insert_job(&t, 1).await;
insert_job(&t, 1, true).await;
let jobs = load_next(
&t,

View File

@@ -9,6 +9,7 @@ use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
use pgp::ser::Serialize;
use pgp::types::{KeyTrait, SecretKeyTrait};
use sqlx::Row;
use thiserror::Error;
use crate::config::Config;
@@ -41,6 +42,10 @@ pub enum Error {
InvalidConfiguredAddr(#[from] InvalidEmailError),
#[error("no data provided")]
Empty,
#[error("db: {}", _0)]
Sql(#[from] sqlx::Error),
#[error("{0}")]
Other(#[from] anyhow::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -118,24 +123,21 @@ impl DcKey for SignedPublicKey {
async fn load_self(context: &Context) -> Result<Self::KeyType> {
match context
.sql
.query_row(
.fetch_optional(sqlx::query(
r#"
SELECT public_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1;
"#,
paramsv![],
|row| row.get::<_, Vec<u8>>(0),
)
.await
))
.await?
{
Ok(bytes) => Self::from_slice(&bytes),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
Some(row) => Self::from_slice(row.try_get(0)?),
None => {
let keypair = generate_keypair(context).await?;
Ok(keypair.public)
}
Err(err) => Err(err.into()),
}
}
@@ -163,24 +165,21 @@ impl DcKey for SignedSecretKey {
async fn load_self(context: &Context) -> Result<Self::KeyType> {
match context
.sql
.query_row(
.fetch_optional(sqlx::query(
r#"
SELECT private_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1;
"#,
paramsv![],
|row| row.get::<_, Vec<u8>>(0),
)
.await
))
.await?
{
Ok(bytes) => Self::from_slice(&bytes),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
Some(row) => Self::from_slice(row.try_get(0)?),
None => {
let keypair = generate_keypair(context).await?;
Ok(keypair.secret)
}
Err(err) => Err(err.into()),
}
}
@@ -221,7 +220,7 @@ impl DcSecretKey for SignedSecretKey {
async fn generate_keypair(context: &Context) -> Result<KeyPair> {
let addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.ok_or(Error::NoConfiguredAddr)?;
let addr = EmailAddress::new(&addr)?;
let _guard = context.generating_key_mutex.lock().await;
@@ -229,26 +228,27 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
// Check if the key appeared while we were waiting on the lock.
match context
.sql
.query_row(
r#"
.fetch_optional(
sqlx::query(
r#"
SELECT public_key, private_key
FROM keypairs
WHERE addr=?1
AND is_default=1;
"#,
paramsv![addr],
|row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, Vec<u8>>(1)?)),
)
.bind(addr.to_string()),
)
.await
.await?
{
Ok((pub_bytes, sec_bytes)) => Ok(KeyPair {
Some(row) => Ok(KeyPair {
addr,
public: SignedPublicKey::from_slice(&pub_bytes)?,
secret: SignedSecretKey::from_slice(&sec_bytes)?,
public: SignedPublicKey::from_slice(row.try_get(0)?)?,
secret: SignedSecretKey::from_slice(row.try_get(1)?)?,
}),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
None => {
let start = std::time::SystemTime::now();
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await)
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await?)
.unwrap_or_default();
info!(context, "Generating keypair with type {}", keytype);
let keypair =
@@ -262,7 +262,6 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
);
Ok(keypair)
}
Err(err) => Err(err.into()),
}
}
@@ -320,15 +319,16 @@ pub async fn store_self_keypair(
context
.sql
.execute(
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
paramsv![public_key, secret_key],
sqlx::query("DELETE FROM keypairs WHERE public_key=? OR private_key=?;")
.bind(&public_key)
.bind(&secret_key),
)
.await
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
if default == KeyPairUse::Default {
context
.sql
.execute("UPDATE keypairs SET is_default=0;", paramsv![])
.execute(sqlx::query("UPDATE keypairs SET is_default=0;"))
.await
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
}
@@ -340,13 +340,18 @@ pub async fn store_self_keypair(
let addr = keypair.addr.to_string();
let t = time();
let params = paramsv![addr, is_default, public_key, secret_key, t];
context
.sql
.execute(
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
sqlx::query(
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
VALUES (?,?,?,?,?);",
params,
)
.bind(addr)
.bind(is_default)
.bind(&public_key)
.bind(&secret_key)
.bind(t),
)
.await
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))?;
@@ -620,7 +625,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
let nrows = || async {
ctx.sql
.query_get_value::<u32>(&ctx, "SELECT COUNT(*) FROM keypairs;", paramsv![])
.count(sqlx::query("SELECT COUNT(*) FROM keypairs;"))
.await
.unwrap()
};

View File

@@ -1,10 +1,11 @@
#![forbid(unsafe_code)]
#![deny(
clippy::correctness,
missing_debug_implementations,
clippy::all,
clippy::indexing_slicing,
clippy::wildcard_imports
clippy::wildcard_imports,
clippy::needless_borrow,
unsafe_code
)]
#![allow(clippy::match_bool, clippy::eval_order_dependence)]
@@ -12,16 +13,10 @@
extern crate num_derive;
#[macro_use]
extern crate smallvec;
#[macro_use]
extern crate rusqlite;
extern crate strum;
#[macro_use]
extern crate strum_macros;
pub trait ToSql: rusqlite::ToSql + Send + Sync {}
impl<T: rusqlite::ToSql + Send + Sync> ToSql for T {}
#[macro_use]
pub mod log;
#[macro_use]

View File

@@ -1,8 +1,11 @@
//! Location handling
use std::convert::TryFrom;
use anyhow::{ensure, Error};
use async_std::prelude::*;
use bitflags::bitflags;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use sqlx::Row;
use crate::chat::{self, ChatId};
use crate::config::Config;
@@ -198,15 +201,15 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds:
if context
.sql
.execute(
"UPDATE chats \
sqlx::query(
"UPDATE chats \
SET locations_send_begin=?, \
locations_send_until=? \
WHERE id=?",
paramsv![
if 0 != seconds { now } else { 0 },
if 0 != seconds { now + seconds } else { 0 },
chat_id,
],
)
.bind(if 0 != seconds { now } else { 0 })
.bind(if 0 != seconds { now + seconds } else { 0 })
.bind(chat_id),
)
.await
.is_ok()
@@ -259,16 +262,17 @@ pub async fn is_sending_locations_to_chat(context: &Context, chat_id: Option<Cha
Some(chat_id) => context
.sql
.exists(
"SELECT id FROM chats WHERE id=? AND locations_send_until>?;",
paramsv![chat_id, time()],
sqlx::query("SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;")
.bind(chat_id)
.bind(time()),
)
.await
.unwrap_or_default(),
None => context
.sql
.exists(
"SELECT id FROM chats WHERE locations_send_until>?;",
paramsv![time()],
sqlx::query("SELECT COUNT(id) FROM chats WHERE locations_send_until>?;")
.bind(time()),
)
.await
.unwrap_or_default(),
@@ -281,28 +285,29 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
}
let mut continue_streaming = false;
if let Ok(chats) = context
if let Ok(mut chats) = context
.sql
.query_map(
"SELECT id FROM chats WHERE locations_send_until>?;",
paramsv![time()],
|row| row.get::<_, i32>(0),
|chats| chats.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.fetch(sqlx::query("SELECT id FROM chats WHERE locations_send_until>?;").bind(time()))
.await
.map(|rows| rows.map(|row| row?.try_get::<i32, _>(0)))
{
for chat_id in chats {
while let Some(chat_id) = chats.next().await {
let chat_id = match chat_id {
Ok(id) => id,
Err(_) => break,
};
if let Err(err) = context.sql.execute(
sqlx::query(
"INSERT INTO locations \
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
paramsv![
latitude,
longitude,
accuracy,
time(),
chat_id,
DC_CONTACT_ID_SELF,
]
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);"
)
.bind(latitude)
.bind(longitude)
.bind(accuracy)
.bind(time())
.bind(chat_id)
.bind(DC_CONTACT_ID_SELF)
).await {
warn!(context, "failed to store location {:?}", err);
} else {
@@ -324,10 +329,11 @@ pub async fn get_range(
contact_id: Option<u32>,
timestamp_from: i64,
mut timestamp_to: i64,
) -> Vec<Location> {
) -> Result<Vec<Location>, Error> {
if timestamp_to == 0 {
timestamp_to = time() + 10;
}
let (disable_chat_id, chat_id) = match chat_id {
Some(chat_id) => (0, chat_id),
None => (1, ChatId::new(0)), // this ChatId is unused
@@ -336,56 +342,52 @@ pub async fn get_range(
Some(contact_id) => (0, contact_id),
None => (1, 0), // this contact_id is unused
};
context
let list = context
.sql
.query_map(
"SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \
.fetch(
sqlx::query(
"SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \
COALESCE(m.id, 0) AS msg_id, l.from_id, l.chat_id, COALESCE(m.txt, '') AS txt \
FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \
AND (? OR l.from_id=?) \
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
paramsv![
disable_chat_id,
chat_id,
disable_contact_id,
contact_id as i32,
timestamp_from,
timestamp_to,
],
|row| {
let msg_id = row.get(6)?;
let txt: String = row.get(9)?;
let marker = if msg_id != 0 && is_marker(&txt) {
Some(txt)
} else {
None
};
let loc = Location {
location_id: row.get(0)?,
latitude: row.get(1)?,
longitude: row.get(2)?,
accuracy: row.get(3)?,
timestamp: row.get(4)?,
independent: row.get(5)?,
msg_id,
contact_id: row.get(7)?,
chat_id: row.get(8)?,
marker,
};
Ok(loc)
},
|locations| {
let mut ret = Vec::new();
for location in locations {
ret.push(location?);
}
Ok(ret)
},
)
.bind(disable_chat_id)
.bind(chat_id)
.bind(disable_contact_id)
.bind(contact_id as i64)
.bind(timestamp_from)
.bind(timestamp_to),
)
.await
.unwrap_or_default()
.await?
.map(|row| {
let row = row?;
let msg_id = row.try_get(6)?;
let txt: String = row.try_get(9)?;
let marker = if msg_id != 0 && is_marker(&txt) {
Some(txt)
} else {
None
};
let loc = Location {
location_id: row.try_get(0)?,
latitude: row.try_get(1)?,
longitude: row.try_get(2)?,
accuracy: row.try_get(3)?,
timestamp: row.try_get(4)?,
independent: row.try_get(5)?,
msg_id,
contact_id: row.try_get(7)?,
chat_id: row.try_get(8)?,
marker,
};
Ok(loc)
})
.collect::<sqlx::Result<_>>()
.await?;
Ok(list)
}
fn is_marker(txt: &str) -> bool {
@@ -401,7 +403,7 @@ fn is_marker(txt: &str) -> bool {
pub async fn delete_all(context: &Context) -> Result<(), Error> {
context
.sql
.execute("DELETE FROM locations;", paramsv![])
.execute(sqlx::query("DELETE FROM locations;"))
.await?;
context.emit_event(EventType::LocationChanged(None));
Ok(())
@@ -412,19 +414,23 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await
.await?
.unwrap_or_default();
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=?;",
paramsv![chat_id], |row| {
let send_begin: i64 = row.get(0)?;
let send_until: i64 = row.get(1)?;
let last_sent: i64 = row.get(2)?;
let (locations_send_begin, locations_send_until, locations_last_sent) = {
let row = context.sql.fetch_one(
sqlx::query(
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;"
)
.bind(chat_id)
).await?;
Ok((send_begin, send_until, last_sent))
})
.await?;
let send_begin: i64 = row.try_get(0)?;
let send_until: i64 = row.try_get(1)?;
let last_sent: i64 = row.try_get(2)?;
(send_begin, send_until, last_sent)
};
let now = time();
let mut location_count = 0;
@@ -435,40 +441,41 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
self_addr,
);
context.sql.query_map(
"SELECT id, latitude, longitude, accuracy, timestamp \
let mut rows = context.sql.fetch(
sqlx::query(
"SELECT id, latitude, longitude, accuracy, timestamp \
FROM locations WHERE from_id=? \
AND timestamp>=? \
AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \
AND independent=0 \
GROUP BY timestamp \
ORDER BY timestamp;",
paramsv![DC_CONTACT_ID_SELF, locations_send_begin, locations_last_sent, DC_CONTACT_ID_SELF],
|row| {
let location_id: i32 = row.get(0)?;
let latitude: f64 = row.get(1)?;
let longitude: f64 = row.get(2)?;
let accuracy: f64 = row.get(3)?;
let timestamp = get_kml_timestamp(row.get(4)?);
Ok((location_id, latitude, longitude, accuracy, timestamp))
},
|rows| {
for row in rows {
let (location_id, latitude, longitude, accuracy, timestamp) = row?;
ret += &format!(
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n",
timestamp,
accuracy,
longitude,
latitude
);
location_count += 1;
last_added_location_id = location_id as u32;
}
Ok(())
}
ORDER BY timestamp;"
)
.bind(DC_CONTACT_ID_SELF)
.bind(locations_send_begin)
.bind(locations_last_sent)
.bind(DC_CONTACT_ID_SELF)
).await?;
while let Some(row) = rows.next().await {
let row = row?;
let location_id: u32 = row.try_get(0)?;
let latitude: f64 = row.try_get(1)?;
let longitude: f64 = row.try_get(2)?;
let accuracy: f64 = row.try_get(3)?;
let timestamp = get_kml_timestamp(row.try_get(4)?);
ret += &format!(
"<Placemark><Timestamp><when>{}</when></Timestamp><Point><coordinates accuracy=\"{}\">{},{}</coordinates></Point></Placemark>\n",
timestamp,
accuracy,
longitude,
latitude
);
location_count += 1;
last_added_location_id = location_id;
}
ret += "</Document>\n</kml>";
}
@@ -509,8 +516,9 @@ pub async fn set_kml_sent_timestamp(
context
.sql
.execute(
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
paramsv![timestamp, chat_id],
sqlx::query("UPDATE chats SET locations_last_sent=? WHERE id=?;")
.bind(timestamp)
.bind(chat_id),
)
.await?;
Ok(())
@@ -524,8 +532,9 @@ pub async fn set_msg_location_id(
context
.sql
.execute(
"UPDATE msgs SET location_id=? WHERE id=?;",
paramsv![location_id, msg_id],
sqlx::query("UPDATE msgs SET location_id=? WHERE id=?;")
.bind(location_id)
.bind(msg_id),
)
.await?;
@@ -544,6 +553,11 @@ pub async fn save(
let mut newest_timestamp = 0;
let mut newest_location_id = 0;
let stmt_test = "SELECT COUNT(*) FROM locations WHERE timestamp=? AND from_id=?";
let stmt_insert = "INSERT INTO locations\
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
VALUES (?,?,?,?,?,?,?);";
for location in locations {
let &Location {
timestamp,
@@ -552,53 +566,33 @@ pub async fn save(
accuracy,
..
} = location;
let (loc_id, ts) = context
let exists = context
.sql
.with_conn(move |mut conn| {
let mut stmt_test = conn
.prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
let mut stmt_insert = conn.prepare_cached(
"INSERT INTO locations\
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
VALUES (?,?,?,?,?,?,?);",
)?;
let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?;
if independent || !exists {
stmt_insert.execute(paramsv![
timestamp,
contact_id as i32,
chat_id,
latitude,
longitude,
accuracy,
independent,
])?;
if timestamp > newest_timestamp {
// okay to drop, as we use cached prepared statements
drop(stmt_test);
drop(stmt_insert);
newest_timestamp = timestamp;
newest_location_id = crate::sql::get_rowid2(
&mut conn,
"locations",
"timestamp",
timestamp,
"from_id",
contact_id as i32,
)?;
}
}
Ok((newest_location_id, newest_timestamp))
})
.exists(sqlx::query(stmt_test).bind(timestamp).bind(contact_id))
.await?;
newest_timestamp = ts;
newest_location_id = loc_id;
if independent || !exists {
let row_id = context
.sql
.insert(
sqlx::query(stmt_insert)
.bind(timestamp)
.bind(contact_id)
.bind(chat_id)
.bind(latitude)
.bind(longitude)
.bind(accuracy)
.bind(independent),
)
.await?;
if timestamp > newest_timestamp {
newest_timestamp = timestamp;
newest_location_id = row_id;
}
}
}
Ok(newest_location_id)
Ok(u32::try_from(newest_location_id)?)
}
pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> job::Status {
@@ -611,15 +605,21 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
let rows = context
.sql
.query_map(
"SELECT id, locations_send_begin, locations_last_sent \
.fetch(
sqlx::query(
"SELECT id, locations_send_begin, locations_last_sent \
FROM chats \
WHERE locations_send_until>?;",
paramsv![now],
|row| {
let chat_id: ChatId = row.get(0)?;
let locations_send_begin: i64 = row.get(1)?;
let locations_last_sent: i64 = row.get(2)?;
)
.bind(now),
)
.await
.map(|rows| {
rows.map(|row| -> sqlx::Result<Option<_>> {
let row = row?;
let chat_id: ChatId = row.try_get(0)?;
let locations_send_begin: i64 = row.try_get(1)?;
let locations_last_sent: i64 = row.try_get(2)?;
continue_streaming = true;
// be a bit tolerant as the timer may not align exactly with time(NULL)
@@ -628,64 +628,55 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
} else {
Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
}
},
|rows| {
rows.filter_map(|v| v.transpose())
.collect::<Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await;
if rows.is_ok() {
let msgs = context
.sql
.with_conn(move |conn| {
let rows = rows.unwrap();
let mut stmt_locations = conn.prepare_cached(
"SELECT id \
FROM locations \
WHERE from_id=? \
AND timestamp>=? \
AND timestamp>? \
AND independent=0 \
ORDER BY timestamp;",
)?;
let mut msgs = Vec::new();
for (chat_id, locations_send_begin, locations_last_sent) in &rows {
if !stmt_locations
.exists(paramsv![
DC_CONTACT_ID_SELF,
*locations_send_begin,
*locations_last_sent,
])
.unwrap_or_default()
{
// if there is no new location, there's nothing to send.
// however, maybe we want to bypass this test eg. 15 minutes
} else {
// pending locations are attached automatically to every message,
// so also to this empty text message.
// DC_CMD_LOCATION is only needed to create a nicer subject.
//
// for optimisation and to avoid flooding the sending queue,
// we could sending these messages only if we're really online.
// the easiest way to determine this, is to check for an empty message queue.
// (might not be 100%, however, as positions are sent combined later
// and dc_set_location() is typically called periodically, this is ok)
let mut msg = Message::new(Viewtype::Text);
msg.hidden = true;
msg.param.set_cmd(SystemMessage::LocationOnly);
msgs.push((*chat_id, msg));
}
}
Ok(msgs)
})
.await
.unwrap_or_default();
.filter_map(|v| v.transpose())
});
let stmt = "SELECT COUNT(*) \
FROM locations \
WHERE from_id=? \
AND timestamp>=? \
AND timestamp>? \
AND independent=0 \
ORDER BY timestamp;";
if let Ok(mut rows) = rows {
let mut msgs = Vec::new();
while let Some(row) = rows.next().await {
let (chat_id, locations_send_begin, locations_last_sent) = match row {
Ok(row) => row,
Err(_) => break,
};
let exists = context
.sql
.exists(
sqlx::query(stmt)
.bind(DC_CONTACT_ID_SELF)
.bind(locations_send_begin)
.bind(locations_last_sent),
)
.await
.unwrap_or_default(); // TODO: better error handling
if !exists {
// if there is no new location, there's nothing to send.
// however, maybe we want to bypass this test eg. 15 minutes
} else {
// pending locations are attached automatically to every message,
// so also to this empty text message.
// DC_CMD_LOCATION is only needed to create a nicer subject.
//
// for optimisation and to avoid flooding the sending queue,
// we could sending these messages only if we're really online.
// the easiest way to determine this, is to check for an empty message queue.
// (might not be 100%, however, as positions are sent combined later
// and dc_set_location() is typically called periodically, this is ok)
let mut msg = Message::new(Viewtype::Text);
msg.hidden = true;
msg.param.set_cmd(SystemMessage::LocationOnly);
msgs.push((chat_id, msg));
}
}
for (chat_id, mut msg) in msgs.into_iter() {
// TODO: better error handling
@@ -711,16 +702,16 @@ pub(crate) async fn job_maybe_send_locations_ended(
let chat_id = ChatId::new(job.foreign_id);
let (send_begin, send_until) = job_try!(
context
.sql
.query_row(
let (send_begin, send_until) = job_try!(context
.sql
.fetch_one(
sqlx::query(
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
paramsv![chat_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
)
.await
);
.bind(chat_id)
)
.await
.and_then(|row| { Ok((row.try_get::<i64, _>(0)?, row.try_get::<i64, _>(1)?)) }));
if !(send_begin != 0 && time() <= send_until) {
// still streaming -
@@ -728,10 +719,19 @@ pub(crate) async fn job_maybe_send_locations_ended(
// 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=?",
paramsv![chat_id],
).await);
job_try!(
context
.sql
.execute(
sqlx::query(
"UPDATE chats \
SET locations_send_begin=0, locations_send_until=0 \
WHERE id=?"
)
.bind(chat_id)
)
.await
);
let stock_str = stock_str::msg_location_disabled(context).await;
chat::add_info_msg(context, chat_id, stock_str).await;

View File

@@ -1,4 +1,5 @@
//! # Logging macros
//! # Logging
use crate::context::Context;
#[macro_export]
macro_rules! info {
@@ -58,3 +59,95 @@ macro_rules! emit_event {
$ctx.emit_event($event);
};
}
pub trait LogExt<T, E>
where
Self: std::marker::Sized,
{
#[track_caller]
fn log_err_inner(self, context: &Context, msg: Option<&str>) -> Result<T, E>;
/// Emits a warning if the receiver contains an Err value.
///
/// Thanks to the [track_caller](https://blog.rust-lang.org/2020/08/27/Rust-1.46.0.html#track_caller)
/// feature, the location of the caller is printed to the log, just like with the warn!() macro.
///
/// Unfortunately, the track_caller feature does not work on async functions (as of Rust 1.50).
/// Once it is, you can add `#[track_caller]` to helper functions that use one of the log helpers here
/// so that the location of the caller can be seen in the log. (this won't work with the macros,
/// like warn!(), since the file!() and line!() macros don't work with track_caller)
/// See https://github.com/rust-lang/rust/issues/78840 for progress on this.
#[track_caller]
fn log_err(self, context: &Context, msg: &str) -> Result<T, E> {
self.log_err_inner(context, Some(msg))
}
/// Emits a warning if the receiver contains an Err value and returns an [`Option<T>`].
///
/// Example:
/// ```text
/// if let Err(e) = do_something() {
/// warn!(context, "{:#}", e);
/// }
/// ```
/// is equivalent to:
/// ```text
/// do_something().ok_or_log(context);
/// ```
///
/// For a note on the `track_caller` feature, see the doc comment on `log_err()`.
#[track_caller]
fn ok_or_log(self, context: &Context) -> Option<T> {
self.log_err_inner(context, None).ok()
}
/// Like `ok_or_log()`, but you can pass an extra message that is prepended in the log.
///
/// Example:
/// ```text
/// if let Err(e) = do_something() {
/// warn!(context, "Something went wrong: {:#}", e);
/// }
/// ```
/// is equivalent to:
/// ```text
/// do_something().ok_or_log_msg(context, "Something went wrong");
/// ```
/// and is also equivalent to:
/// ```text
/// use anyhow::Context as _;
/// do_something().context("Something went wrong").ok_or_log(context);
/// ```
///
/// For a note on the `track_caller` feature, see the doc comment on `log_err()`.
#[track_caller]
fn ok_or_log_msg(self, context: &Context, msg: &'static str) -> Option<T> {
self.log_err_inner(context, Some(msg)).ok()
}
}
impl<T: Default, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
#[track_caller]
fn log_err_inner(self, context: &Context, msg: Option<&str>) -> Result<T, E> {
if let Err(e) = &self {
let location = std::panic::Location::caller();
let separator = if msg.is_none() { "" } else { ": " };
let msg = msg.unwrap_or_default();
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
let full = format!(
"{file}:{line}: {msg}{separator}{e:#}",
file = location.file(),
line = location.line(),
msg = msg,
separator = separator,
e = e
);
// We can't use the warn!() macro here as the file!() and line!() macros
// don't work with #[track_caller]
emit_event!(context, crate::EventType::Warning(full));
};
self
}
}

View File

@@ -7,7 +7,7 @@ use crate::provider::{get_provider_by_id, Provider};
use crate::{context::Context, provider::Socket};
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
#[repr(i32)]
#[repr(u32)]
#[strum(serialize_all = "snake_case")]
pub enum CertificateChecks {
/// Same as AcceptInvalidCertificates unless overridden by
@@ -30,7 +30,7 @@ impl Default for CertificateChecks {
}
/// Login parameters for a single server, either IMAP or SMTP
#[derive(Default, Debug, Clone)]
#[derive(Default, Debug, Clone, PartialEq)]
pub struct ServerLoginParam {
pub server: String,
pub user: String,
@@ -43,7 +43,7 @@ pub struct ServerLoginParam {
pub certificate_checks: CertificateChecks,
}
#[derive(Default, Debug, Clone)]
#[derive(Default, Debug, Clone, PartialEq)]
pub struct LoginParam {
pub addr: String,
pub imap: ServerLoginParam,
@@ -54,91 +54,85 @@ pub struct LoginParam {
impl LoginParam {
/// Read the login parameters from the database.
pub async fn from_database(context: &Context, prefix: impl AsRef<str>) -> Self {
pub async fn from_database(
context: &Context,
prefix: impl AsRef<str>,
) -> crate::sql::Result<Self> {
let prefix = prefix.as_ref();
let sql = &context.sql;
let key = format!("{}addr", prefix);
let addr = sql
.get_raw_config(context, key)
.await
.get_raw_config(key)
.await?
.unwrap_or_default()
.trim()
.to_string();
let key = format!("{}mail_server", prefix);
let mail_server = sql.get_raw_config(context, key).await.unwrap_or_default();
let mail_server = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}mail_port", prefix);
let mail_port = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let mail_port = sql.get_raw_config_int(key).await?.unwrap_or_default();
let key = format!("{}mail_user", prefix);
let mail_user = sql.get_raw_config(context, key).await.unwrap_or_default();
let mail_user = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}mail_pw", prefix);
let mail_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
let mail_pw = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}mail_security", prefix);
let mail_security = sql
.get_raw_config_int(context, key)
.await
.get_raw_config_int(key)
.await?
.and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default();
let key = format!("{}imap_certificate_checks", prefix);
let imap_certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
if let Some(certificate_checks) = sql.get_raw_config_int(key).await? {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
} else {
Default::default()
};
let key = format!("{}send_server", prefix);
let send_server = sql.get_raw_config(context, key).await.unwrap_or_default();
let send_server = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}send_port", prefix);
let send_port = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let send_port = sql.get_raw_config_int(key).await?.unwrap_or_default();
let key = format!("{}send_user", prefix);
let send_user = sql.get_raw_config(context, key).await.unwrap_or_default();
let send_user = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}send_pw", prefix);
let send_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
let send_pw = sql.get_raw_config(key).await?.unwrap_or_default();
let key = format!("{}send_security", prefix);
let send_security = sql
.get_raw_config_int(context, key)
.await
.get_raw_config_int(key)
.await?
.and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default();
let key = format!("{}smtp_certificate_checks", prefix);
let smtp_certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
if let Some(certificate_checks) = sql.get_raw_config_int(key).await? {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
} else {
Default::default()
};
let key = format!("{}server_flags", prefix);
let server_flags = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let server_flags = sql.get_raw_config_int(key).await?.unwrap_or_default();
let key = format!("{}provider", prefix);
let provider = sql
.get_raw_config(context, key)
.await
.get_raw_config(key)
.await?
.and_then(|provider_id| get_provider_by_id(&provider_id));
LoginParam {
Ok(LoginParam {
addr,
imap: ServerLoginParam {
server: mail_server,
@@ -158,7 +152,7 @@ impl LoginParam {
},
provider,
server_flags,
}
})
}
/// Save this loginparam to the database.
@@ -171,63 +165,54 @@ impl LoginParam {
let sql = &context.sql;
let key = format!("{}addr", prefix);
sql.set_raw_config(context, key, Some(&self.addr)).await?;
sql.set_raw_config(key, Some(&self.addr)).await?;
let key = format!("{}mail_server", prefix);
sql.set_raw_config(context, key, Some(&self.imap.server))
.await?;
sql.set_raw_config(key, Some(&self.imap.server)).await?;
let key = format!("{}mail_port", prefix);
sql.set_raw_config_int(context, key, self.imap.port as i32)
.await?;
sql.set_raw_config_int(key, self.imap.port as i32).await?;
let key = format!("{}mail_user", prefix);
sql.set_raw_config(context, key, Some(&self.imap.user))
.await?;
sql.set_raw_config(key, Some(&self.imap.user)).await?;
let key = format!("{}mail_pw", prefix);
sql.set_raw_config(context, key, Some(&self.imap.password))
.await?;
sql.set_raw_config(key, Some(&self.imap.password)).await?;
let key = format!("{}mail_security", prefix);
sql.set_raw_config_int(context, key, self.imap.security as i32)
sql.set_raw_config_int(key, self.imap.security as i32)
.await?;
let key = format!("{}imap_certificate_checks", prefix);
sql.set_raw_config_int(context, key, self.imap.certificate_checks as i32)
sql.set_raw_config_int(key, self.imap.certificate_checks as i32)
.await?;
let key = format!("{}send_server", prefix);
sql.set_raw_config(context, key, Some(&self.smtp.server))
.await?;
sql.set_raw_config(key, Some(&self.smtp.server)).await?;
let key = format!("{}send_port", prefix);
sql.set_raw_config_int(context, key, self.smtp.port as i32)
.await?;
sql.set_raw_config_int(key, self.smtp.port as i32).await?;
let key = format!("{}send_user", prefix);
sql.set_raw_config(context, key, Some(&self.smtp.user))
.await?;
sql.set_raw_config(key, Some(&self.smtp.user)).await?;
let key = format!("{}send_pw", prefix);
sql.set_raw_config(context, key, Some(&self.smtp.password))
.await?;
sql.set_raw_config(key, Some(&self.smtp.password)).await?;
let key = format!("{}send_security", prefix);
sql.set_raw_config_int(context, key, self.smtp.security as i32)
sql.set_raw_config_int(key, self.smtp.security as i32)
.await?;
let key = format!("{}smtp_certificate_checks", prefix);
sql.set_raw_config_int(context, key, self.smtp.certificate_checks as i32)
sql.set_raw_config_int(key, self.smtp.certificate_checks as i32)
.await?;
let key = format!("{}server_flags", prefix);
sql.set_raw_config_int(context, key, self.server_flags)
.await?;
sql.set_raw_config_int(key, self.server_flags).await?;
if let Some(provider) = self.provider {
let key = format!("{}provider", prefix);
sql.set_raw_config(context, key, Some(provider.id)).await?;
sql.set_raw_config(key, Some(provider.id)).await?;
}
Ok(())
@@ -319,6 +304,8 @@ pub fn dc_build_tls(strict_tls: bool) -> async_native_tls::TlsConnector {
mod tests {
use super::*;
use crate::test_utils::TestContext;
#[test]
fn test_certificate_checks_display() {
use std::string::ToString;
@@ -328,4 +315,37 @@ mod tests {
CertificateChecks::AcceptInvalidCertificates.to_string()
);
}
#[async_std::test]
async fn test_save_load_login_param() -> anyhow::Result<()> {
let t = TestContext::new().await;
let param = LoginParam {
addr: "alice@example.com".to_string(),
imap: ServerLoginParam {
server: "imap.example.com".to_string(),
user: "alice".to_string(),
password: "foo".to_string(),
port: 123,
security: Socket::Starttls,
certificate_checks: CertificateChecks::Strict,
},
smtp: ServerLoginParam {
server: "smtp.example.com".to_string(),
user: "alice@example.com".to_string(),
password: "bar".to_string(),
port: 456,
security: Socket::Ssl,
certificate_checks: CertificateChecks::AcceptInvalidCertificates,
},
server_flags: 0,
provider: get_provider_by_id("example.com"),
};
param.save_to_database(&t, "foobar_").await?;
let loaded = LoginParam::from_database(&t, "foobar_").await?;
assert_eq!(param, loaded);
Ok(())
}
}

View File

@@ -1,5 +1,3 @@
use deltachat_derive::{FromSql, ToSql};
use crate::key::Fingerprint;
/// An object containing a set of values.
@@ -22,7 +20,7 @@ pub struct Lot {
}
#[repr(u8)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
pub enum Meaning {
None = 0,
Text1Draft = 1,
@@ -66,8 +64,8 @@ impl Lot {
}
}
#[repr(i32)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u32)]
pub enum LotState {
// Default
Undefined = 0,
@@ -83,7 +81,7 @@ pub enum LotState {
QrFprOk = 210,
/// id=contact
QrFprMissmatch = 220,
QrFprMismatch = 220,
/// test1=formatted fingerprint
QrFprWithoutAddr = 230,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,6 @@ use std::pin::Pin;
use anyhow::{bail, Result};
use charset::Charset;
use deltachat_derive::{FromSql, ToSql};
use lettre_email::mime::{self, Mime};
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
use once_cell::sync::Lazy;
@@ -12,10 +11,10 @@ use percent_encoding::percent_decode_str;
use crate::aheader::Aheader;
use crate::blob::BlobObject;
use crate::constants::Viewtype;
use crate::constants::{Viewtype, DC_DESIRED_TEXT_LEN, DC_ELLIPSE};
use crate::contact::addr_normalize;
use crate::context::Context;
use crate::dc_tools::dc_get_filemeta;
use crate::dc_tools::{dc_get_filemeta, dc_truncate};
use crate::dehtml::dehtml;
use crate::e2ee;
use crate::events::EventType;
@@ -86,8 +85,25 @@ pub(crate) enum AvatarAction {
Change(String),
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[repr(i32)]
#[derive(Debug, PartialEq)]
pub(crate) enum MailinglistType {
/// The message belongs to a mailing list and has a `ListId:`-header
/// that should be used to get a unique id.
ListIdBased,
/// The message belongs to a mailing list, but there is no `ListId:`-header;
/// `Sender:`-header should be used to get a unique id.
/// This method is used by implementations as Majordomo.
/// Note, that the `Sender:` header alone is not sufficient to detect these lists,
/// `get_mailinglist_type()` check additional conditions therefore.
SenderBased,
/// The message does not belong to a mailing list.
None,
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
#[repr(u32)]
pub enum SystemMessage {
Unknown = 0,
GroupNameChanged = 2,
@@ -240,10 +256,11 @@ impl MimeMessage {
is_mime_modified: false,
decoded_data: Vec::new(),
};
parser.parse_mime_recursive(context, &mail).await?;
parser.parse_mime_recursive(context, &mail, false).await?;
parser.maybe_remove_bad_parts();
parser.maybe_remove_inline_mailinglist_footer();
parser.heuristically_parse_ndn(context).await;
parser.parse_headers(context)?;
parser.parse_headers(context);
if warn_empty_signature && parser.signatures.is_empty() {
for part in parser.parts.iter_mut() {
@@ -259,7 +276,7 @@ impl MimeMessage {
}
/// Parses system messages.
fn parse_system_message_headers(&mut self, context: &Context) -> Result<()> {
fn parse_system_message_headers(&mut self, context: &Context) {
if self.get(HeaderDef::AutocryptSetupMessage).is_some() {
self.parts = self
.parts
@@ -287,7 +304,6 @@ impl MimeMessage {
self.is_system_message = SystemMessage::ChatProtectionDisabled;
}
}
Ok(())
}
/// Parses avatar action headers.
@@ -387,8 +403,8 @@ impl MimeMessage {
}
}
fn parse_headers(&mut self, context: &Context) -> Result<()> {
self.parse_system_message_headers(context)?;
fn parse_headers(&mut self, context: &Context) {
self.parse_system_message_headers(context);
self.parse_avatar_headers();
self.parse_videochat_headers();
self.squash_attachment_parts();
@@ -408,7 +424,7 @@ impl MimeMessage {
// For mailing lists, always add the subject because sometimes there are different topics
// and otherwise it might be hard to keep track:
if self.get(HeaderDef::ListId).is_some() {
if self.is_mailinglist_message() {
prepend_subject = true;
}
@@ -431,7 +447,7 @@ impl MimeMessage {
// See if an MDN is requested from the other side
if !self.decrypting_failed && !self.parts.is_empty() {
if let Some(ref dn_to) = self.chat_disposition_notification_to {
if let Some(ref from) = self.from.get(0) {
if let Some(from) = self.from.get(0) {
if from.addr.to_lowercase() == dn_to.addr.to_lowercase() {
if let Some(part) = self.parts.last_mut() {
part.param.set_int(Param::WantsMdn, 1);
@@ -464,8 +480,6 @@ impl MimeMessage {
self.parts.push(part);
}
Ok(())
}
fn avatar_action_from_header(&mut self, header_value: String) -> Option<AvatarAction> {
@@ -521,6 +535,7 @@ impl MimeMessage {
&'a mut self,
context: &'a Context,
mail: &'a mailparse::ParsedMail<'a>,
is_related: bool,
) -> Pin<Box<dyn Future<Output = Result<bool>> + 'a + Send>> {
use futures::future::FutureExt;
@@ -562,8 +577,9 @@ impl MimeMessage {
MimeS::Single
};
let is_related = is_related || mimetype == "multipart/related";
match m {
MimeS::Multiple => self.handle_multiple(context, mail).await,
MimeS::Multiple => self.handle_multiple(context, mail, is_related).await,
MimeS::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
@@ -571,9 +587,9 @@ impl MimeMessage {
}
let mail = mailparse::parse_mail(&raw).unwrap();
self.parse_mime_recursive(context, &mail).await
self.parse_mime_recursive(context, &mail, is_related).await
}
MimeS::Single => self.add_single_part_if_known(context, mail).await,
MimeS::Single => self.add_single_part_if_known(context, mail, is_related).await,
}
}
.boxed()
@@ -583,6 +599,7 @@ impl MimeMessage {
&mut self,
context: &Context,
mail: &mailparse::ParsedMail<'_>,
is_related: bool,
) -> Result<bool> {
let mut any_part_added = false;
let mimetype = get_mime_type(mail)?.0;
@@ -596,7 +613,9 @@ impl MimeMessage {
if get_mime_type(cur_data)?.0 == "multipart/mixed"
|| get_mime_type(cur_data)?.0 == "multipart/related"
{
any_part_added = self.parse_mime_recursive(context, cur_data).await?;
any_part_added = self
.parse_mime_recursive(context, cur_data, is_related)
.await?;
break;
}
}
@@ -604,7 +623,9 @@ impl MimeMessage {
/* search for text/plain and add this */
for cur_data in &mail.subparts {
if get_mime_type(cur_data)?.0.type_() == mime::TEXT {
any_part_added = self.parse_mime_recursive(context, cur_data).await?;
any_part_added = self
.parse_mime_recursive(context, cur_data, is_related)
.await?;
break;
}
}
@@ -612,7 +633,10 @@ impl MimeMessage {
if !any_part_added {
/* `text/plain` not found - use the first part */
for cur_part in &mail.subparts {
if self.parse_mime_recursive(context, cur_part).await? {
if self
.parse_mime_recursive(context, cur_part, is_related)
.await?
{
any_part_added = true;
break;
}
@@ -654,7 +678,9 @@ impl MimeMessage {
https://k9mail.github.io/2016/11/24/OpenPGP-Considerations-Part-I.html
for background information why we use encrypted+signed) */
if let Some(first) = mail.subparts.get(0) {
any_part_added = self.parse_mime_recursive(context, first).await?;
any_part_added = self
.parse_mime_recursive(context, first, is_related)
.await?;
}
}
(mime::MULTIPART, "report") => {
@@ -686,14 +712,19 @@ impl MimeMessage {
// Add all parts (we need another part, preferably text/plain, to show as an error message)
for cur_data in mail.subparts.iter() {
if self.parse_mime_recursive(context, cur_data).await? {
if self
.parse_mime_recursive(context, cur_data, is_related)
.await?
{
any_part_added = true;
}
}
}
Some(_) => {
if let Some(first) = mail.subparts.get(0) {
any_part_added = self.parse_mime_recursive(context, first).await?;
any_part_added = self
.parse_mime_recursive(context, first, is_related)
.await?;
}
}
}
@@ -703,7 +734,10 @@ impl MimeMessage {
// Add all parts (in fact, AddSinglePartIfKnown() later check if
// the parts are really supported)
for cur_data in mail.subparts.iter() {
if self.parse_mime_recursive(context, cur_data).await? {
if self
.parse_mime_recursive(context, cur_data, is_related)
.await?
{
any_part_added = true;
}
}
@@ -717,6 +751,7 @@ impl MimeMessage {
&mut self,
context: &Context,
mail: &mailparse::ParsedMail<'_>,
is_related: bool,
) -> Result<bool> {
// return true if a part was added
let (mime_type, msg_type) = get_mime_type(mail)?;
@@ -735,6 +770,7 @@ impl MimeMessage {
&raw_mime,
&mail.get_body_raw()?,
&filename,
is_related,
)
.await;
}
@@ -800,6 +836,14 @@ impl MimeMessage {
(simplified_txt, top_quote)
};
let simplified_txt =
if simplified_txt.len() > DC_DESIRED_TEXT_LEN + DC_ELLIPSE.len() {
self.is_mime_modified = true;
dc_truncate(&*simplified_txt, DC_DESIRED_TEXT_LEN).to_string()
} else {
simplified_txt
};
if !simplified_txt.is_empty() || simplified_quote.is_some() {
let mut part = Part {
dehtml_failed,
@@ -830,6 +874,7 @@ impl MimeMessage {
Ok(self.parts.len() > old_part_count)
}
#[allow(clippy::too_many_arguments)]
async fn do_add_single_file_part(
&mut self,
context: &Context,
@@ -838,6 +883,7 @@ impl MimeMessage {
raw_mime: &str,
decoded_data: &[u8],
filename: &str,
is_related: bool,
) {
if decoded_data.is_empty() {
return;
@@ -890,6 +936,7 @@ impl MimeMessage {
part.bytes = decoded_data.len();
part.param.set(Param::File, blob.as_name());
part.param.set(Param::MimeType, raw_mime);
part.is_related = is_related;
self.do_add_single_part(part);
}
@@ -901,15 +948,25 @@ impl MimeMessage {
self.parts.push(part);
}
pub fn is_mailinglist_message(&self) -> bool {
pub(crate) fn get_mailinglist_type(&self) -> MailinglistType {
if self.get(HeaderDef::ListId).is_some() {
return true;
return MailinglistType::ListIdBased;
} else if self.get(HeaderDef::Sender).is_some() {
// the `Sender:`-header alone is no indicator for mailing list
// as also used for bot-impersonation via `set_override_sender_name()`
if let Some(precedence) = self.get(HeaderDef::Precedence) {
if precedence == "list" || precedence == "bulk" {
return MailinglistType::SenderBased;
}
}
}
MailinglistType::None
}
if let Some(precedence) = self.get(HeaderDef::Precedence) {
precedence == "list" || precedence == "bulk"
} else {
false
pub(crate) fn is_mailinglist_message(&self) -> bool {
match self.get_mailinglist_type() {
MailinglistType::ListIdBased | MailinglistType::SenderBased => true,
MailinglistType::None => false,
}
}
@@ -923,7 +980,8 @@ impl MimeMessage {
}
pub fn get_rfc724_mid(&self) -> Option<String> {
self.get(HeaderDef::MessageId)
self.get(HeaderDef::XMicrosoftOriginalMessageId)
.or_else(|| self.get(HeaderDef::MessageId))
.and_then(|msgid| parse_message_id(msgid).ok())
}
@@ -1057,6 +1115,52 @@ impl MimeMessage {
} else if good_parts < self.parts.len() {
self.parts.retain(|p| !p.dehtml_failed);
}
// remove images that are descendants of multipart/related but the first one:
// - for newsletters or so, that is often the logo
// - for user-generated html-mails, that may be some drag'n'drop photo,
// so, the recipient sees at least the first image directly
// - all other images can be accessed by "show full message"
// - to ensure, there is such a button, we do removal only if
// `is_mime_modified` is set
if !self.has_chat_version() && self.is_mime_modified {
fn is_related_image(p: &&Part) -> bool {
(p.typ == Viewtype::Image || p.typ == Viewtype::Gif) && p.is_related
}
let related_image_cnt = self.parts.iter().filter(is_related_image).count();
if related_image_cnt > 1 {
let mut is_first_image = true;
self.parts.retain(|p| {
let retain = is_first_image || !is_related_image(&p);
if p.typ == Viewtype::Image || p.typ == Viewtype::Gif {
is_first_image = false;
}
retain
});
}
}
}
/// Remove unwanted, additional text parts used for mailing list footer.
/// Some mailinglist software add footers as separate mimeparts
/// eg. when the user-edited-content is html.
/// As these footers would appear as repeated, separate text-bubbles,
/// we remove them.
fn maybe_remove_inline_mailinglist_footer(&mut self) {
if self.is_mailinglist_message() {
let text_part_cnt = self
.parts
.iter()
.filter(|p| p.typ == Viewtype::Text)
.count();
if text_part_cnt == 2 {
if let Some(last_part) = self.parts.last() {
if last_part.typ == Viewtype::Text {
self.parts.pop();
}
}
}
}
}
/// Some providers like GMX and Yahoo do not send standard NDNs (Non Delivery notifications).
@@ -1108,10 +1212,16 @@ impl MimeMessage {
for original_message_id in
std::iter::once(&report.original_message_id).chain(&report.additional_message_ids)
{
if let Some((chat_id, msg_id)) =
message::handle_mdn(context, from_id, original_message_id, sent_timestamp).await
match message::handle_mdn(context, from_id, original_message_id, sent_timestamp)
.await
{
context.emit_event(EventType::MsgRead { chat_id, msg_id });
Ok(Some((chat_id, msg_id))) => {
context.emit_event(EventType::MsgRead { chat_id, msg_id });
}
Ok(None) => {}
Err(err) => {
warn!(context, "failed to handle_mdn: {:#}", err);
}
}
}
}
@@ -1138,9 +1248,8 @@ impl MimeMessage {
{
context
.sql
.query_get_value_result(
"SELECT timestamp FROM msgs WHERE rfc724_mid=?",
paramsv![field],
.query_get_value(
sqlx::query("SELECT timestamp FROM msgs WHERE rfc724_mid=?").bind(field),
)
.await?
} else {
@@ -1172,7 +1281,7 @@ async fn update_gossip_peerstates(
peerstate.apply_gossip(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else {
let p = Peerstate::from_gossip(context, header, message_time);
let p = Peerstate::from_gossip(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
@@ -1208,7 +1317,7 @@ pub(crate) struct FailureReport {
}
#[allow(clippy::indexing_slicing)]
pub(crate) fn parse_message_ids(ids: &str) -> Result<Vec<String>> {
pub(crate) fn parse_message_ids(ids: &str) -> Vec<String> {
// take care with mailparse::msgidparse() that is pretty untolerant eg. wrt missing `<` or `>`
let mut msgids = Vec::new();
for id in ids.split_whitespace() {
@@ -1223,11 +1332,11 @@ pub(crate) fn parse_message_ids(ids: &str) -> Result<Vec<String>> {
msgids.push(id);
}
}
Ok(msgids)
msgids
}
pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
if let Some(id) = parse_message_ids(ids)?.first() {
if let Some(id) = parse_message_ids(ids).first() {
Ok(id.to_string())
} else {
bail!("could not parse message_id: {}", ids);
@@ -1263,6 +1372,14 @@ pub struct Part {
org_filename: Option<String>,
pub error: Option<String>,
dehtml_failed: bool,
/// the part is a child or a descendant of multipart/related.
/// typically, these are images that are referenced from text/html part
/// and should not displayed inside chat.
///
/// note that multipart/related may contain further multipart nestings
/// and all of them needs to be marked with `is_related`.
is_related: bool,
}
/// return mimetype and viewtype for a parsed mail
@@ -1355,7 +1472,7 @@ fn get_attachment_filename(
let mut parts = name.splitn(3, '\'');
desired_filename =
if let (Some(charset), Some(value)) = (parts.next(), parts.last()) {
let decoded_bytes = percent_decode_str(&value);
let decoded_bytes = percent_decode_str(value);
if charset.to_lowercase() == "utf-8" {
Some(decoded_bytes.decode_utf8_lossy().to_string())
} else {
@@ -1459,6 +1576,7 @@ mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::constants::DC_MAX_GET_TEXT_LEN;
use crate::{
chatlist::Chatlist,
config::Config,
@@ -1597,8 +1715,8 @@ mod tests {
fn load_mail_with_attachment<'a>(t: &'a TestContext, raw: &'a [u8]) -> ParsedMail<'a> {
let mail = mailparse::parse_mail(raw).unwrap();
assert!(get_attachment_filename(&t, &mail).unwrap().is_none());
assert!(get_attachment_filename(&t, &mail.subparts[0])
assert!(get_attachment_filename(t, &mail).unwrap().is_none());
assert!(get_attachment_filename(t, &mail.subparts[0])
.unwrap()
.is_none());
mail
@@ -1802,8 +1920,9 @@ mod tests {
.ctx
.sql
.execute(
"INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)",
paramsv!["Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org", timestamp],
sqlx::query("INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)")
.bind("Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org")
.bind(timestamp),
)
.await
.expect("Failed to write to the database");
@@ -2479,23 +2598,23 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I=
#[test]
fn test_parse_message_ids() {
let test = parse_message_ids(" foo bar <foobar>").unwrap();
let test = parse_message_ids(" foo bar <foobar>");
assert_eq!(test.len(), 3);
assert_eq!(test[0], "foo");
assert_eq!(test[1], "bar");
assert_eq!(test[2], "foobar");
let test = parse_message_ids(" < foobar >").unwrap();
let test = parse_message_ids(" < foobar >");
assert_eq!(test.len(), 1);
assert_eq!(test[0], "foobar");
let test = parse_message_ids("").unwrap();
let test = parse_message_ids("");
assert!(test.is_empty());
let test = parse_message_ids(" ").unwrap();
let test = parse_message_ids(" ");
assert!(test.is_empty());
let test = parse_message_ids(" < ").unwrap();
let test = parse_message_ids(" < ");
assert!(test.is_empty());
}
@@ -2625,6 +2744,19 @@ On 2020-10-25, Bob wrote:
assert_eq!(mimeparser.parts[0].param.get(Param::Quote).unwrap(), "Now?");
}
#[async_std::test]
async fn test_allinkl_blockquote() {
// all-inkl.com puts quotes into `<blockquote> </blockquote>`.
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/allinkl-quote.eml");
let mimeparser = MimeMessage::from_bytes(&t, raw).await.unwrap();
assert!(mimeparser.parts[0].msg.starts_with("It's 1.0."));
assert_eq!(
mimeparser.parts[0].param.get(Param::Quote).unwrap(),
"What's the version?"
);
}
#[async_std::test]
async fn test_add_subj_to_multimedia_msg() {
let t = TestContext::new_alice().await;
@@ -2722,4 +2854,49 @@ On 2020-10-25, Bob wrote:
"mime-modified test mime-modified *set*; simplify is always regarded as lossy."
);
}
#[async_std::test]
async fn test_mime_modified_large_plain() {
let t = TestContext::new().await;
static REPEAT_TXT: &str = "this text with 42 chars is just repeated.\n";
static REPEAT_CNT: usize = 2000; // results in a text of 84k, should be more than DC_MAX_GET_TEXT_LEN
let long_txt = format!("From: alice@c.de\n\n{}", REPEAT_TXT.repeat(REPEAT_CNT));
assert!(DC_DESIRED_TEXT_LEN + DC_ELLIPSE.len() < DC_MAX_GET_TEXT_LEN);
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref())
.await
.unwrap();
assert_eq!(long_txt.matches("just repeated").count(), REPEAT_CNT);
assert!(long_txt.len() > DC_MAX_GET_TEXT_LEN);
assert!(mimemsg.is_mime_modified);
assert!(
mimemsg.parts[0].msg.matches("just repeated").count()
< DC_MAX_GET_TEXT_LEN / REPEAT_TXT.len()
);
assert!(mimemsg.parts[0].msg.len() <= DC_MAX_GET_TEXT_LEN);
}
#[async_std::test]
async fn test_x_microsoft_original_message_id() {
let t = TestContext::new().await;
let message = MimeMessage::from_bytes(&t, b"Date: Wed, 17 Feb 2021 15:45:15 +0000\n\
Chat-Version: 1.0\n\
Message-ID: <DBAPR03MB1180CE51A1BFE265BD018D4790869@DBAPR03MB6691.eurprd03.prod.outlook.com>\n\
To: Bob <bob@example.org>\n\
From: Alice <alice@example.org>\n\
Subject: Message from Alice\n\
Content-Type: text/plain\n\
X-Microsoft-Original-Message-ID: <Mr.6Dx7ITn4w38.n9j7epIcuQI@outlook.com>\n\
MIME-Version: 1.0\n\
\n\
Does it work with outlook now?\n\
")
.await
.unwrap();
assert_eq!(
message.get_rfc724_mid(),
Some("Mr.6Dx7ITn4w38.n9j7epIcuQI@outlook.com".to_string())
);
}
}

View File

@@ -2,6 +2,7 @@
use std::collections::HashMap;
use anyhow::Result;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use serde::Deserialize;
@@ -58,11 +59,7 @@ pub async fn dc_get_oauth2_url(
if let Some(oauth2) = Oauth2::from_address(addr).await {
if context
.sql
.set_raw_config(
context,
"oauth2_pending_redirect_uri",
Some(redirect_uri.as_ref()),
)
.set_raw_config("oauth2_pending_redirect_uri", Some(redirect_uri.as_ref()))
.await
.is_err()
{
@@ -82,31 +79,25 @@ pub async fn dc_get_oauth2_access_token(
addr: impl AsRef<str>,
code: impl AsRef<str>,
regenerate: bool,
) -> Option<String> {
) -> Result<Option<String>> {
if let Some(oauth2) = Oauth2::from_address(addr).await {
let lock = context.oauth2_mutex.lock().await;
// read generated token
if !regenerate && !is_expired(context).await {
let access_token = context
.sql
.get_raw_config(context, "oauth2_access_token")
.await;
if !regenerate && !is_expired(context).await? {
let access_token = context.sql.get_raw_config("oauth2_access_token").await?;
if access_token.is_some() {
// success
return access_token;
return Ok(access_token);
}
}
// generate new token: build & call auth url
let refresh_token = context
.sql
.get_raw_config(context, "oauth2_refresh_token")
.await;
let refresh_token = context.sql.get_raw_config("oauth2_refresh_token").await?;
let refresh_token_for = context
.sql
.get_raw_config(context, "oauth2_refresh_token_for")
.await
.get_raw_config("oauth2_refresh_token_for")
.await?
.unwrap_or_else(|| "unset".into());
let (redirect_uri, token_url, update_redirect_uri_on_success) =
@@ -115,8 +106,8 @@ pub async fn dc_get_oauth2_access_token(
(
context
.sql
.get_raw_config(context, "oauth2_pending_redirect_uri")
.await
.get_raw_config("oauth2_pending_redirect_uri")
.await?
.unwrap_or_else(|| "unset".into()),
oauth2.init_token,
true,
@@ -129,8 +120,8 @@ pub async fn dc_get_oauth2_access_token(
(
context
.sql
.get_raw_config(context, "oauth2_redirect_uri")
.await
.get_raw_config("oauth2_redirect_uri")
.await?
.unwrap_or_else(|| "unset".into()),
oauth2.refresh_token,
false,
@@ -166,7 +157,7 @@ pub async fn dc_get_oauth2_access_token(
let mut req = surf::post(post_url).build();
if let Err(err) = req.body_form(&post_param) {
warn!(context, "Error calling OAuth2 at {}: {:?}", token_url, err);
return None;
return Ok(None);
}
let client = surf::Client::new();
@@ -176,7 +167,7 @@ pub async fn dc_get_oauth2_access_token(
context,
"Failed to parse OAuth2 JSON response from {}: error: {:?}", token_url, parsed
);
return None;
return Ok(None);
}
// update refresh_token if given, typically on the first round, but we update it later as well.
@@ -184,14 +175,12 @@ pub async fn dc_get_oauth2_access_token(
if let Some(ref token) = response.refresh_token {
context
.sql
.set_raw_config(context, "oauth2_refresh_token", Some(token))
.await
.ok();
.set_raw_config("oauth2_refresh_token", Some(token))
.await?;
context
.sql
.set_raw_config(context, "oauth2_refresh_token_for", Some(code.as_ref()))
.await
.ok();
.set_raw_config("oauth2_refresh_token_for", Some(code.as_ref()))
.await?;
}
// after that, save the access token.
@@ -199,9 +188,8 @@ pub async fn dc_get_oauth2_access_token(
if let Some(ref token) = response.access_token {
context
.sql
.set_raw_config(context, "oauth2_access_token", Some(token))
.await
.ok();
.set_raw_config("oauth2_access_token", Some(token))
.await?;
let expires_in = response
.expires_in
// refresh a bit before
@@ -209,16 +197,14 @@ pub async fn dc_get_oauth2_access_token(
.unwrap_or_else(|| 0);
context
.sql
.set_raw_config_int64(context, "oauth2_timestamp_expires", expires_in)
.await
.ok();
.set_raw_config_int64("oauth2_timestamp_expires", expires_in)
.await?;
if update_redirect_uri_on_success {
context
.sql
.set_raw_config(context, "oauth2_redirect_uri", Some(redirect_uri.as_ref()))
.await
.ok();
.set_raw_config("oauth2_redirect_uri", Some(redirect_uri.as_ref()))
.await?;
}
} else {
warn!(context, "Failed to find OAuth2 access token");
@@ -226,11 +212,11 @@ pub async fn dc_get_oauth2_access_token(
drop(lock);
response.access_token
Ok(response.access_token)
} else {
warn!(context, "Internal OAuth2 error: 2");
None
Ok(None)
}
}
@@ -238,27 +224,33 @@ pub async fn dc_get_oauth2_addr(
context: &Context,
addr: impl AsRef<str>,
code: impl AsRef<str>,
) -> Option<String> {
let oauth2 = Oauth2::from_address(addr.as_ref()).await?;
oauth2.get_userinfo?;
) -> Result<Option<String>> {
let oauth2 = match Oauth2::from_address(addr.as_ref()).await {
Some(o) => o,
None => return Ok(None),
};
if oauth2.get_userinfo.is_none() {
return Ok(None);
}
if let Some(access_token) =
dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false).await
dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false).await?
{
let addr_out = oauth2.get_addr(context, access_token).await;
if addr_out.is_none() {
// regenerate
if let Some(access_token) = dc_get_oauth2_access_token(context, addr, code, true).await
if let Some(access_token) =
dc_get_oauth2_access_token(context, addr, code, true).await?
{
oauth2.get_addr(context, access_token).await
Ok(oauth2.get_addr(context, access_token).await)
} else {
None
Ok(None)
}
} else {
addr_out
Ok(addr_out)
}
} else {
None
Ok(None)
}
}
@@ -269,7 +261,7 @@ impl Oauth2 {
.find('@')
.map(|index| addr_normalized.split_at(index + 1).1)
{
if let Some(oauth2_authorizer) = provider::get_provider_info(&domain)
if let Some(oauth2_authorizer) = provider::get_provider_info(domain)
.await
.and_then(|provider| provider.oauth2_authorizer.as_ref())
{
@@ -317,21 +309,21 @@ impl Oauth2 {
}
}
async fn is_expired(context: &Context) -> bool {
async fn is_expired(context: &Context) -> Result<bool, crate::sql::Error> {
let expire_timestamp = context
.sql
.get_raw_config_int64(context, "oauth2_timestamp_expires")
.await
.get_raw_config_int64("oauth2_timestamp_expires")
.await?
.unwrap_or_default();
if expire_timestamp <= 0 {
return false;
return Ok(false);
}
if expire_timestamp > time() {
return false;
return Ok(false);
}
true
Ok(true)
}
fn replace_in_uri(uri: impl AsRef<str>, key: impl AsRef<str>, value: impl AsRef<str>) -> String {
@@ -399,7 +391,7 @@ mod tests {
let ctx = TestContext::new().await;
let addr = "dignifiedquire@gmail.com";
let code = "fail";
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code).await;
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code).await.unwrap();
// this should fail as it is an invalid password
assert_eq!(res, None);
}
@@ -419,7 +411,9 @@ mod tests {
let ctx = TestContext::new().await;
let addr = "dignifiedquire@gmail.com";
let code = "fail";
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false).await;
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false)
.await
.unwrap();
// this should fail as it is an invalid password
assert_eq!(res, None);
}

View File

@@ -23,7 +23,8 @@ pub enum Param {
File = b'f',
/// For messages: This name should be shown instead of contact.get_display_name()
/// (used if this is a mailinglist)
/// (used if this is a mailinglist
/// or explictly set using set_override_sender_name(), eg. by bots)
OverrideSenderDisplayname = b'O',
/// For Messages
@@ -125,7 +126,9 @@ pub enum Param {
/// For Chats
Selftalk = b'K',
/// For Chats: So that on sending a new message we can sent the subject to "Re: <last subject>"
/// For Chats: On sending a new message we set the subject to "Re: <last subject>".
/// Usually we just use the subject of the parent message, but if the parent message
/// is deleted, we use the LastSubject of the chat.
LastSubject = b't',
/// For Chats
@@ -330,7 +333,7 @@ impl Params {
pub fn get_msg_id(&self) -> Option<MsgId> {
self.get(Param::MsgId)
.and_then(|x| x.parse::<u32>().ok())
.and_then(|x| x.parse().ok())
.map(MsgId::new)
}

View File

@@ -5,6 +5,7 @@ use std::fmt;
use anyhow::{bail, Result};
use num_traits::FromPrimitive;
use sqlx::{query::Query, sqlite::Sqlite, Row};
use crate::aheader::{Aheader, EncryptPreference};
use crate::chat;
@@ -30,8 +31,7 @@ pub enum PeerstateVerifiedStatus {
}
/// Peerstate represents the state of an Autocrypt peer.
pub struct Peerstate<'a> {
pub context: &'a Context,
pub struct Peerstate {
pub addr: String,
pub last_seen: i64,
pub last_seen_autocrypt: i64,
@@ -47,7 +47,7 @@ pub struct Peerstate<'a> {
pub fingerprint_changed: bool,
}
impl<'a> PartialEq for Peerstate<'a> {
impl PartialEq for Peerstate {
fn eq(&self, other: &Peerstate) -> bool {
self.addr == other.addr
&& self.last_seen == other.last_seen
@@ -65,9 +65,9 @@ impl<'a> PartialEq for Peerstate<'a> {
}
}
impl<'a> Eq for Peerstate<'a> {}
impl Eq for Peerstate {}
impl<'a> fmt::Debug for Peerstate<'a> {
impl fmt::Debug for Peerstate {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Peerstate")
.field("addr", &self.addr)
@@ -94,10 +94,9 @@ pub enum ToSave {
All = 0x02,
}
impl<'a> Peerstate<'a> {
pub fn from_header(context: &'a Context, header: &Aheader, message_time: i64) -> Self {
impl Peerstate {
pub fn from_header(header: &Aheader, message_time: i64) -> Self {
Peerstate {
context,
addr: header.addr.clone(),
last_seen: message_time,
last_seen_autocrypt: message_time,
@@ -114,9 +113,8 @@ impl<'a> Peerstate<'a> {
}
}
pub fn from_gossip(context: &'a Context, gossip_header: &Aheader, message_time: i64) -> Self {
pub fn from_gossip(gossip_header: &Aheader, message_time: i64) -> Self {
Peerstate {
context,
addr: gossip_header.addr.clone(),
last_seen: 0,
last_seen_autocrypt: 0,
@@ -141,87 +139,94 @@ impl<'a> Peerstate<'a> {
}
}
pub async fn from_addr(context: &'a Context, addr: &str) -> Result<Option<Peerstate<'a>>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
pub async fn from_addr(context: &Context, addr: &str) -> Result<Option<Peerstate>> {
let query = sqlx::query(
"SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint \
FROM acpeerstates \
WHERE addr=? COLLATE NOCASE;";
Self::from_stmt(context, query, paramsv![addr]).await
WHERE addr=? COLLATE NOCASE;",
)
.bind(addr);
Self::from_stmt(context, query).await
}
pub async fn from_fingerprint(
context: &'a Context,
context: &Context,
_sql: &Sql,
fingerprint: &Fingerprint,
) -> Result<Option<Peerstate<'a>>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
) -> Result<Option<Peerstate>> {
let fp = fingerprint.hex();
let query = sqlx::query(
"SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint \
FROM acpeerstates \
WHERE public_key_fingerprint=? COLLATE NOCASE \
OR gossip_key_fingerprint=? COLLATE NOCASE \
ORDER BY public_key_fingerprint=? DESC;";
let fp = fingerprint.hex();
Self::from_stmt(context, query, paramsv![fp, fp, fp]).await
ORDER BY public_key_fingerprint=? DESC;",
)
.bind(&fp)
.bind(&fp)
.bind(&fp);
Self::from_stmt(context, query).await
}
async fn from_stmt(
context: &'a Context,
query: &str,
params: Vec<&dyn crate::ToSql>,
) -> Result<Option<Peerstate<'a>>> {
let peerstate = context
.sql
.query_row_optional(query, params, |row| {
/* all the above queries start with this: SELECT
addr, last_seen, last_seen_autocrypt, prefer_encrypted,
public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
gossip_key_fingerprint, verified_key, verified_key_fingerprint
*/
async fn from_stmt<'q, E>(
context: &Context,
query: Query<'q, Sqlite, E>,
) -> Result<Option<Peerstate>>
where
E: 'q + sqlx::IntoArguments<'q, sqlx::Sqlite>,
{
if let Some(row) = context.sql.fetch_optional(query).await? {
// all the above queries start with this: SELECT
// addr, last_seen, last_seen_autocrypt, prefer_encrypted,
// public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
// gossip_key_fingerprint, verified_key, verified_key_fingerprint
let res = Peerstate {
context,
addr: row.get(0)?,
last_seen: row.get(1)?,
last_seen_autocrypt: row.get(2)?,
prefer_encrypt: EncryptPreference::from_i32(row.get(3)?).unwrap_or_default(),
public_key: row
.get(4)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
public_key_fingerprint: row
.get::<_, Option<String>>(7)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
gossip_key: row
.get(6)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
gossip_key_fingerprint: row
.get::<_, Option<String>>(8)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
gossip_timestamp: row.get(5)?,
verified_key: row
.get(9)
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
verified_key_fingerprint: row
.get::<_, Option<String>>(10)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
to_save: None,
fingerprint_changed: false,
};
let peerstate = Peerstate {
addr: row.try_get(0)?,
last_seen: row.try_get(1)?,
last_seen_autocrypt: row.try_get(2)?,
prefer_encrypt: EncryptPreference::from_i32(row.try_get(3)?).unwrap_or_default(),
public_key: row
.try_get::<&[u8], _>(4)
.ok()
.and_then(|blob| SignedPublicKey::from_slice(blob).ok()),
public_key_fingerprint: row
.try_get::<Option<String>, _>(7)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
gossip_key: row
.try_get::<&[u8], _>(6)
.ok()
.and_then(|blob| SignedPublicKey::from_slice(blob).ok()),
gossip_key_fingerprint: row
.try_get::<Option<String>, _>(8)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
gossip_timestamp: row.try_get(5)?,
verified_key: row
.try_get::<&[u8], _>(9)
.ok()
.and_then(|blob| SignedPublicKey::from_slice(blob).ok()),
verified_key_fingerprint: row
.try_get::<Option<String>, _>(10)?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
to_save: None,
fingerprint_changed: false,
};
Ok(res)
})
.await?;
Ok(peerstate)
Ok(Some(peerstate))
} else {
Ok(None)
}
}
pub fn recalc_fingerprint(&mut self) {
@@ -270,9 +275,8 @@ impl<'a> Peerstate<'a> {
if self.fingerprint_changed {
if let Some(contact_id) = context
.sql
.query_get_value_result(
"SELECT id FROM contacts WHERE addr=?;",
paramsv![self.addr],
.query_get_value(
sqlx::query("SELECT id FROM contacts WHERE addr=?;").bind(&self.addr),
)
.await?
{
@@ -433,42 +437,59 @@ impl<'a> Peerstate<'a> {
pub async fn save_to_db(&self, sql: &Sql, create: bool) -> crate::sql::Result<()> {
if self.to_save == Some(ToSave::All) || create {
sql.execute(
if create {
"INSERT INTO acpeerstates (last_seen, last_seen_autocrypt, prefer_encrypted, \
public_key, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint, addr \
) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
(if create {
sqlx::query(
"INSERT INTO acpeerstates ( \
last_seen, \
last_seen_autocrypt, \
prefer_encrypted, \
public_key, \
gossip_timestamp, \
gossip_key, \
public_key_fingerprint, \
gossip_key_fingerprint, \
verified_key, \
verified_key_fingerprint, \
addr \
) VALUES(?,?,?,?,?,?,?,?,?,?,?)",
)
} else {
"UPDATE acpeerstates \
SET last_seen=?, last_seen_autocrypt=?, prefer_encrypted=?, \
public_key=?, gossip_timestamp=?, gossip_key=?, public_key_fingerprint=?, gossip_key_fingerprint=?, \
verified_key=?, verified_key_fingerprint=? \
WHERE addr=?"
},
paramsv![
self.last_seen,
self.last_seen_autocrypt,
self.prefer_encrypt as i64,
self.public_key.as_ref().map(|k| k.to_bytes()),
self.gossip_timestamp,
self.gossip_key.as_ref().map(|k| k.to_bytes()),
self.public_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verified_key.as_ref().map(|k| k.to_bytes()),
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.addr,
],
).await?;
sqlx::query(
"UPDATE acpeerstates \
SET last_seen=?, \
last_seen_autocrypt=?, \
prefer_encrypted=?, \
public_key=?, \
gossip_timestamp=?, \
gossip_key=?, \
public_key_fingerprint=?, \
gossip_key_fingerprint=?, \
verified_key=?, \
verified_key_fingerprint=? \
WHERE addr=?",
)
})
.bind(self.last_seen)
.bind(self.last_seen_autocrypt)
.bind(self.prefer_encrypt as i64)
.bind(self.public_key.as_ref().map(|k| k.to_bytes()))
.bind(self.gossip_timestamp)
.bind(self.gossip_key.as_ref().map(|k| k.to_bytes()))
.bind(self.public_key_fingerprint.as_ref().map(|fp| fp.hex()))
.bind(self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()))
.bind(self.verified_key.as_ref().map(|k| k.to_bytes()))
.bind(self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()))
.bind(&self.addr),
)
.await?;
} else if self.to_save == Some(ToSave::Timestamps) {
sql.execute(
"UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \
WHERE addr=?;",
paramsv![
self.last_seen,
self.last_seen_autocrypt,
self.gossip_timestamp,
self.addr
],
sqlx::query("UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \
WHERE addr=?;").bind(
self.last_seen).bind(
self.last_seen_autocrypt).bind(
self.gossip_timestamp).bind(
&self.addr)
)
.await?;
}
@@ -485,12 +506,6 @@ impl<'a> Peerstate<'a> {
}
}
impl From<crate::key::FingerprintError> for rusqlite::Error {
fn from(_source: crate::key::FingerprintError) -> Self {
Self::InvalidColumnType(0, "Invalid fingerprint".into(), rusqlite::types::Type::Text)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -505,7 +520,6 @@ mod tests {
let pub_key = alice_keypair().public;
let mut peerstate = Peerstate {
context: &ctx.ctx,
addr: addr.into(),
last_seen: 10,
last_seen_autocrypt: 11,
@@ -549,7 +563,6 @@ mod tests {
let pub_key = alice_keypair().public;
let peerstate = Peerstate {
context: &ctx.ctx,
addr: addr.into(),
last_seen: 10,
last_seen_autocrypt: 11,
@@ -583,7 +596,6 @@ mod tests {
let pub_key = alice_keypair().public;
let mut peerstate = Peerstate {
context: &ctx.ctx,
addr: addr.into(),
last_seen: 10,
last_seen_autocrypt: 11,
@@ -626,7 +638,7 @@ mod tests {
// can be loaded without errors.
ctx.ctx
.sql
.execute("INSERT INTO acpeerstates (addr) VALUES(?)", paramsv![addr])
.execute(sqlx::query("INSERT INTO acpeerstates (addr) VALUES(?)").bind(addr))
.await
.expect("Failed to write to the database");
@@ -644,13 +656,11 @@ mod tests {
#[async_std::test]
async fn test_peerstate_degrade_reordering() {
let context = crate::test_utils::TestContext::new().await.ctx;
let addr = "example@example.org";
let pub_key = alice_keypair().public;
let header = Aheader::new(addr.to_string(), pub_key, EncryptPreference::Mutual);
let mut peerstate = Peerstate {
context: &context,
addr: addr.to_string(),
last_seen: 0,
last_seen_autocrypt: 0,

File diff suppressed because it is too large Load Diff

View File

@@ -10,24 +10,24 @@ use chrono::{NaiveDateTime, NaiveTime};
#[derive(Debug, Display, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Status {
OK = 1,
PREPARATION = 2,
BROKEN = 3,
Ok = 1,
Preparation = 2,
Broken = 3,
}
#[derive(Debug, Display, PartialEq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Protocol {
SMTP = 1,
IMAP = 2,
Smtp = 1,
Imap = 2,
}
#[derive(Debug, Display, PartialEq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum Socket {
Automatic = 0,
SSL = 1,
STARTTLS = 2,
Ssl = 1,
Starttls = 2,
Plain = 3,
}
@@ -40,8 +40,8 @@ impl Default for Socket {
#[derive(Debug, PartialEq, Clone)]
#[repr(u8)]
pub enum UsernamePattern {
EMAIL = 1,
EMAILLOCALPART = 2,
Email = 1,
Emaillocalpart = 2,
}
#[derive(Debug, PartialEq)]
@@ -51,7 +51,7 @@ pub enum Oauth2Authorizer {
Gmail = 2,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct Server {
pub protocol: Protocol,
pub socket: Socket,
@@ -60,13 +60,13 @@ pub struct Server {
pub username_pattern: UsernamePattern,
}
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub struct ConfigDefault {
pub key: Config,
pub value: &'static str,
}
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub struct Provider {
/// Unique ID, corresponding to provider database filename.
pub id: &'static str,
@@ -151,9 +151,11 @@ pub async fn get_provider_by_mx(domain: impl AsRef<str>) -> Option<&'static Prov
None
}
// TODO: uncomment when clippy starts complaining about it
//#[allow(clippy::manual_map)] // Can't use .map() because the lifetime is not propagated
pub fn get_provider_by_id(id: &str) -> Option<&'static Provider> {
if let Some(provider) = PROVIDER_IDS.get(id) {
Some(&provider)
Some(provider)
} else {
None
}
@@ -181,34 +183,34 @@ mod tests {
#[test]
fn test_get_provider_by_domain_mixed_case() {
let provider = get_provider_by_domain("nAUta.Cu").unwrap();
assert!(provider.status == Status::OK);
assert!(provider.status == Status::Ok);
}
#[test]
fn test_get_provider_by_domain() {
let addr = "nauta.cu";
let provider = get_provider_by_domain(addr).unwrap();
assert!(provider.status == Status::OK);
assert!(provider.status == Status::Ok);
let server = &provider.server[0];
assert_eq!(server.protocol, Protocol::IMAP);
assert_eq!(server.socket, Socket::STARTTLS);
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);
assert_eq!(server.username_pattern, UsernamePattern::Email);
let server = &provider.server[1];
assert_eq!(server.protocol, Protocol::SMTP);
assert_eq!(server.socket, Socket::STARTTLS);
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);
assert_eq!(server.username_pattern, UsernamePattern::Email);
let provider = get_provider_by_domain("gmail.com").unwrap();
assert!(provider.status == Status::PREPARATION);
assert!(provider.status == Status::Preparation);
assert!(!provider.before_login_hint.is_empty());
assert!(!provider.overview_page.is_empty());
let provider = get_provider_by_domain("googlemail.com").unwrap();
assert!(provider.status == Status::PREPARATION);
assert!(provider.status == Status::Preparation);
}
#[test]

View File

@@ -103,8 +103,8 @@ def process_data(data, file):
if username_pattern != "EMAIL" and username_pattern != "EMAILLOCALPART":
raise TypeError("bad username pattern")
server += (" Server { protocol: " + protocol + ", socket: " + socket + ", hostname: \""
+ hostname + "\", port: " + str(port) + ", username_pattern: " + username_pattern + " },\n")
server += (" Server { protocol: " + protocol.capitalize() + ", socket: " + socket.capitalize() + ", hostname: \""
+ hostname + "\", port: " + str(port) + ", username_pattern: " + username_pattern.capitalize() + " },\n")
config_defaults = process_config_defaults(data)
@@ -123,7 +123,7 @@ def process_data(data, file):
if (not has_imap and not has_smtp) or (has_imap and has_smtp):
provider += "static " + file2varname(file) + ": Lazy<Provider> = Lazy::new(|| Provider {\n"
provider += " id: \"" + file2id(file) + "\",\n"
provider += " status: Status::" + status + ",\n"
provider += " status: Status::" + status.capitalize() + ",\n"
provider += " before_login_hint: \"" + before_login_hint + "\",\n"
provider += " after_login_hint: \"" + after_login_hint + "\",\n"
provider += " overview_page: \"" + file2url(file) + "\",\n"
@@ -175,7 +175,7 @@ if __name__ == "__main__":
"use crate::provider::Protocol::*;\n"
"use crate::provider::Socket::*;\n"
"use crate::provider::UsernamePattern::*;\n"
"use crate::provider::*;\n"
"use crate::provider::{Config, ConfigDefault, Oauth2Authorizer, Provider, Server, Status};\n"
"use std::collections::HashMap;\n\n"
"use once_cell::sync::Lazy;\n\n")

114
src/qr.rs
View File

@@ -27,11 +27,11 @@ const HTTP_SCHEME: &str = "http://";
const HTTPS_SCHEME: &str = "https://";
// Make it easy to convert errors into the final `Lot`.
impl Into<Lot> for Error {
fn into(self) -> Lot {
let mut l = Lot::new();
impl From<Error> for Lot {
fn from(error: Error) -> Self {
let mut l = Self::new();
l.state = LotState::QrError;
l.text1 = Some(self.to_string());
l.text1 = Some(error.to_string());
l
}
@@ -72,6 +72,7 @@ pub async fn check_qr(context: &Context, qr: impl AsRef<str>) -> Lot {
/// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH`
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR`
#[allow(clippy::indexing_slicing)]
async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
let payload = &qr[OPENPGP4FPR_SCHEME.len()..];
@@ -169,6 +170,14 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Lot {
.unwrap_or_default();
chat::add_info_msg(context, id, format!("{} verified.", peerstate.addr)).await;
} else if let Some(addr) = addr {
lot.state = LotState::QrFprMismatch;
lot.id = match Contact::lookup_id_by_addr(context, &addr, Origin::Unknown).await {
Ok(contact_id) => contact_id.unwrap_or_default(),
Err(err) => {
return format_err!("Error looking up contact {:?}: {}", addr, err).into()
}
};
} else {
lot.state = LotState::QrFprWithoutAddr;
lot.text1 = Some(fingerprint.to_string());
@@ -365,9 +374,9 @@ static VCARD_NAME_RE: Lazy<regex::Regex> =
static VCARD_EMAIL_RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
/// Extract address for the matmsg scheme.
/// Extract address for the vcard scheme.
///
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;
/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;`
#[allow(clippy::indexing_slicing)]
async fn decode_vcard(context: &Context, qr: &str) -> Lot {
let name = VCARD_NAME_RE
@@ -427,7 +436,7 @@ fn normalize_address(addr: &str) -> Result<String, Error> {
let new_addr = percent_decode_str(addr).decode_utf8()?;
let new_addr = addr_normalize(&new_addr);
ensure!(may_be_valid_addr(&new_addr), "Bad e-mail address");
ensure!(may_be_valid_addr(new_addr), "Bad e-mail address");
Ok(new_addr.to_string())
}
@@ -436,7 +445,10 @@ fn normalize_address(addr: &str) -> Result<String, Error> {
mod tests {
use super::*;
use crate::test_utils::TestContext;
use crate::aheader::EncryptPreference;
use crate::key::DcKey;
use crate::peerstate::ToSave;
use crate::test_utils::{alice_keypair, TestContext};
#[async_std::test]
async fn test_decode_http() {
@@ -625,6 +637,59 @@ mod tests {
assert_eq!(contact.get_name(), "");
}
#[async_std::test]
async fn test_decode_openpgp_fingerprint() {
let ctx = TestContext::new().await;
let contact_id = Contact::create(&ctx, "Alice", "alice@example.com")
.await
.expect("failed to create contact");
let pub_key = alice_keypair().public;
let peerstate = Peerstate {
addr: "alice@example.com".to_string(),
last_seen: 1,
last_seen_autocrypt: 1,
prefer_encrypt: EncryptPreference::Mutual,
public_key: Some(pub_key.clone()),
public_key_fingerprint: Some(pub_key.fingerprint()),
gossip_key: None,
gossip_timestamp: 0,
gossip_key_fingerprint: None,
verified_key: None,
verified_key_fingerprint: None,
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
assert!(
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
"failed to save peerstate"
);
let res = check_qr(
&ctx.ctx,
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.com",
)
.await;
assert_eq!(res.get_state(), LotState::QrFprMismatch);
assert_eq!(res.get_id(), contact_id);
let res = check_qr(
&ctx.ctx,
format!("OPENPGP4FPR:{}#a=alice@example.com", pub_key.fingerprint()),
)
.await;
assert_eq!(res.get_state(), LotState::QrFprOk);
assert_eq!(res.get_id(), contact_id);
let res = check_qr(
&ctx.ctx,
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=bob@example.org",
)
.await;
assert_eq!(res.get_state(), LotState::QrFprMismatch);
assert_eq!(res.get_id(), 0);
}
#[async_std::test]
async fn test_decode_openpgp_without_addr() {
let ctx = TestContext::new().await;
@@ -726,20 +791,39 @@ mod tests {
async fn test_set_config_from_qr() {
let ctx = TestContext::new().await;
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none());
assert!(ctx
.ctx
.get_config(Config::WebrtcInstance)
.await
.unwrap()
.is_none());
let res = set_config_from_qr(&ctx.ctx, "badqr:https://example.org/").await;
assert!(!res.is_ok());
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none());
assert!(ctx
.ctx
.get_config(Config::WebrtcInstance)
.await
.unwrap()
.is_none());
let res = set_config_from_qr(&ctx.ctx, "https://no.qr").await;
assert!(!res.is_ok());
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await.is_none());
assert!(ctx
.ctx
.get_config(Config::WebrtcInstance)
.await
.unwrap()
.is_none());
let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await;
assert!(res.is_ok());
assert_eq!(
ctx.ctx.get_config(Config::WebrtcInstance).await.unwrap(),
ctx.ctx
.get_config(Config::WebrtcInstance)
.await
.unwrap()
.unwrap(),
"https://example.org/"
);
@@ -747,7 +831,11 @@ mod tests {
set_config_from_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://foo.bar/?$ROOM&test").await;
assert!(res.is_ok());
assert_eq!(
ctx.ctx.get_config(Config::WebrtcInstance).await.unwrap(),
ctx.ctx
.get_config(Config::WebrtcInstance)
.await
.unwrap()
.unwrap(),
"basicwebrtc:https://foo.bar/?$ROOM&test"
);
}

View File

@@ -77,7 +77,11 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
Some(job) => {
// Let the fetch run, but return back to the job afterwards.
jobs_loaded = 0;
if ctx.get_config_bool(Config::InboxWatch).await {
if ctx
.get_config_bool(Config::InboxWatch)
.await
.unwrap_or_default()
{
info!(ctx, "postponing imap-job {} to run fetch...", job);
fetch(&ctx, &mut connection).await;
}
@@ -93,9 +97,16 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
maybe_add_time_based_warnings(&ctx).await;
info = if ctx.get_config_bool(Config::InboxWatch).await {
info = if ctx
.get_config_bool(Config::InboxWatch)
.await
.unwrap_or_default()
{
fetch_idle(&ctx, &mut connection, Config::ConfiguredInboxFolder).await
} else {
if let Err(err) = connection.scan_folders(&ctx).await {
warn!(ctx, "{}", err);
}
connection.fake_idle(&ctx, None).await
};
}
@@ -118,50 +129,60 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
async fn fetch(ctx: &Context, connection: &mut Imap) {
match ctx.get_config(Config::ConfiguredInboxFolder).await {
Some(watch_folder) => {
if let Err(err) = connection.connect_configured(&ctx).await {
Ok(Some(watch_folder)) => {
if let Err(err) = connection.connect_configured(ctx).await {
error_network!(ctx, "{}", err);
return;
}
// fetch
if let Err(err) = connection.fetch(&ctx, &watch_folder).await {
if let Err(err) = connection.fetch(ctx, &watch_folder).await {
connection.trigger_reconnect();
warn!(ctx, "{:#}", err);
}
}
None => {
Ok(None) => {
warn!(ctx, "Can not fetch inbox folder, not set");
connection.fake_idle(&ctx, None).await;
connection.fake_idle(ctx, None).await;
}
Err(err) => {
warn!(
ctx,
"Can not fetch inbox folder, failed to get config: {:?}", err
);
connection.fake_idle(ctx, None).await;
}
}
}
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> InterruptInfo {
match ctx.get_config(folder).await {
Some(watch_folder) => {
Ok(Some(watch_folder)) => {
// connect and fake idle if unable to connect
if let Err(err) = connection.connect_configured(&ctx).await {
if let Err(err) = connection.connect_configured(ctx).await {
warn!(ctx, "imap connection failed: {}", err);
return connection.fake_idle(&ctx, Some(watch_folder)).await;
return connection.fake_idle(ctx, Some(watch_folder)).await;
}
// fetch
if let Err(err) = connection.fetch(&ctx, &watch_folder).await {
if let Err(err) = connection.fetch(ctx, &watch_folder).await {
connection.trigger_reconnect();
warn!(ctx, "{:#}", err);
}
if let Err(err) = connection.scan_folders(&ctx).await {
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
// but maybe just one folder can't be selected or something
warn!(ctx, "{}", err);
if folder == Config::ConfiguredInboxFolder {
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
if let Err(err) = connection.scan_folders(ctx).await {
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
// but maybe just one folder can't be selected or something
warn!(ctx, "{}", err);
}
}
// idle
if connection.can_idle() {
connection
.idle(&ctx, Some(watch_folder))
.idle(ctx, Some(watch_folder))
.await
.unwrap_or_else(|err| {
connection.trigger_reconnect();
@@ -169,12 +190,19 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> Int
InterruptInfo::new(false, None)
})
} else {
connection.fake_idle(&ctx, Some(watch_folder)).await
connection.fake_idle(ctx, Some(watch_folder)).await
}
}
None => {
Ok(None) => {
warn!(ctx, "Can not watch {} folder, not set", folder);
connection.fake_idle(&ctx, None).await
connection.fake_idle(ctx, None).await
}
Err(err) => {
warn!(
ctx,
"Can not watch {} folder, failed to retrieve config: {:?}", folder, err
);
connection.fake_idle(ctx, None).await
}
}
}
@@ -293,7 +321,11 @@ impl Scheduler {
}))
};
if ctx.get_config_bool(Config::MvboxWatch).await {
if ctx
.get_config_bool(Config::MvboxWatch)
.await
.unwrap_or_default()
{
let ctx = ctx.clone();
mvbox_handle = Some(task::spawn(async move {
simple_imap_loop(
@@ -311,7 +343,11 @@ impl Scheduler {
.expect("mvbox start send, missing receiver");
}
if ctx.get_config_bool(Config::SentboxWatch).await {
if ctx
.get_config_bool(Config::SentboxWatch)
.await
.unwrap_or_default()
{
let ctx = ctx.clone();
sentbox_handle = Some(task::spawn(async move {
simple_imap_loop(
@@ -517,9 +553,9 @@ impl SmtpConnectionState {
};
let state = ConnectionState {
idle_interrupt_sender,
shutdown_receiver,
stop_sender,
idle_interrupt_sender,
};
let conn = SmtpConnectionState { state };
@@ -564,9 +600,9 @@ impl ImapConnectionState {
};
let state = ConnectionState {
idle_interrupt_sender,
shutdown_receiver,
stop_sender,
idle_interrupt_sender,
};
let conn = ImapConnectionState { state };

View File

@@ -60,14 +60,11 @@ pub struct BobStateHandle<'a> {
impl<'a> BobStateHandle<'a> {
/// Creates a new instance, upholding the guarantee that [`BobState`] must exist.
pub fn from_guard(mut guard: MutexGuard<'a, Option<BobState>>) -> Option<Self> {
match guard.take() {
Some(bobstate) => Some(Self {
guard,
bobstate,
clear_state_on_drop: false,
}),
None => None,
}
guard.take().map(|bobstate| Self {
guard,
bobstate,
clear_state_on_drop: false,
})
}
/// Returns the [`ChatId`] of the 1:1 chat with the inviter (Alice).
@@ -93,8 +90,10 @@ impl<'a> BobStateHandle<'a> {
info!(context, "Handling securejoin message for BobStateHandle");
match self.bobstate.handle_message(context, mime_message).await {
Ok(Some(stage)) => {
if matches!(stage, BobHandshakeStage::Completed | BobHandshakeStage::Terminated(_))
{
if matches!(
stage,
BobHandshakeStage::Completed | BobHandshakeStage::Terminated(_)
) {
self.finish_protocol(context).await;
}
Some(stage)
@@ -117,9 +116,11 @@ impl<'a> BobStateHandle<'a> {
/// allowing a new handshake to be started from [`Bob`].
///
/// Note that the state is only cleared on Drop since otherwise the invariant that the
/// state is always consistent is violated. However the "ongoing" prococess is released
/// state is always consistent is violated. However the "ongoing" process is released
/// here a little bit earlier as this requires access to the Context, which we do not
/// have on Drop (Drop can not run asynchronous code).
/// have on Drop (Drop can not run asynchronous code). Stopping the "ongoing" process
/// will release [`securejoin`](super::securejoin) which in turn will finally free the
/// ongoing process using [`Context::free_ongoing`].
///
/// [`InnerContext::bob`]: crate::context::InnerContext::bob
/// [`Bob`]: super::Bob
@@ -187,7 +188,7 @@ impl BobState {
let chat_id = chat::create_by_contact_id(context, invite.contact_id())
.await
.map_err(JoinError::UnknownContact)?;
if fingerprint_equals_sender(context, invite.fingerprint(), chat_id).await {
if fingerprint_equals_sender(context, invite.fingerprint(), chat_id).await? {
// The scanned fingerprint matches Alice's key, we can proceed to step 4b.
info!(context, "Taking securejoin protocol shortcut");
let state = Self {
@@ -296,7 +297,7 @@ impl BobState {
self.next = SecureJoinStep::Terminated;
return Ok(Some(BobHandshakeStage::Terminated(reason)));
}
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.chat_id).await {
if !fingerprint_equals_sender(context, self.invite.fingerprint(), self.chat_id).await? {
self.next = SecureJoinStep::Terminated;
return Ok(Some(BobHandshakeStage::Terminated("Fingerprint mismatch")));
}

View File

@@ -4,6 +4,7 @@ use std::convert::TryFrom;
use std::time::{Duration, Instant};
use anyhow::{bail, Context as _, Error, Result};
use async_std::channel::Receiver;
use async_std::sync::Mutex;
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
@@ -70,6 +71,20 @@ pub(crate) struct Bob {
inner: Mutex<Option<BobState>>,
}
/// Return value for [`Bob::start_protocol`].
///
/// This indicates which protocol variant was started and provides the required information
/// about it.
enum StartedProtocolVariant {
/// The setup-contact protocol, to verify a contact.
SetupContact,
/// The secure-join protocol, to join a group.
SecureJoin {
ongoing_receiver: Receiver<()>,
group_id: String,
},
}
impl Bob {
/// Starts the securejoin protocol with the QR `invite`.
///
@@ -78,19 +93,32 @@ impl Bob {
///
/// This function takes care of starting the "ongoing" mechanism if required and
/// handling errors while starting the protocol.
async fn start_protocol(&self, context: &Context, invite: QrInvite) -> Result<(), JoinError> {
///
/// # Returns
///
/// If the started protocol is joining a group the returned struct contains information
/// about the group and ongoing process.
async fn start_protocol(
&self,
context: &Context,
invite: QrInvite,
) -> Result<StartedProtocolVariant, JoinError> {
let mut guard = self.inner.lock().await;
if guard.is_some() {
return Err(JoinError::AlreadyRunning);
}
let did_alloc_ongoing = match invite {
QrInvite::Group { .. } => {
if context.alloc_ongoing().await.is_err() {
return Err(JoinError::OngoingRunning);
let variant = match invite {
QrInvite::Group { ref grpid, .. } => {
let receiver = context
.alloc_ongoing()
.await
.map_err(|_| JoinError::OngoingRunning)?;
StartedProtocolVariant::SecureJoin {
ongoing_receiver: receiver,
group_id: grpid.clone(),
}
true
}
_ => false,
_ => StartedProtocolVariant::SetupContact,
};
match BobState::start_protocol(context, invite).await {
Ok((state, stage)) => {
@@ -98,11 +126,11 @@ impl Bob {
joiner_progress!(context, state.invite().contact_id(), 400);
}
*guard = Some(state);
Ok(())
Ok(variant)
}
Err(err) => {
if did_alloc_ongoing {
context.stop_ongoing().await;
if let StartedProtocolVariant::SecureJoin { .. } = variant {
context.free_ongoing().await;
}
Err(err)
}
@@ -145,9 +173,16 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> O
let invitenumber = token::lookup_or_new(context, token::Namespace::InviteNumber, group).await;
let auth = token::lookup_or_new(context, token::Namespace::Auth, group).await;
let self_addr = match context.get_config(Config::ConfiguredAddr).await {
Some(addr) => addr,
None => {
error!(context, "Not configured, cannot generate QR code.",);
Ok(Some(addr)) => addr,
Ok(None) => {
error!(context, "Not configured, cannot generate QR code.");
return None;
}
Err(err) => {
error!(
context,
"Unable to retrieve configuration, cannot generate QR code: {:?}", err
);
return None;
}
};
@@ -155,6 +190,7 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> O
let self_name = context
.get_config(Config::Displayname)
.await
.ok()?
.unwrap_or_default();
let fingerprint: Fingerprint = match get_self_fingerprint(context).await {
@@ -174,7 +210,7 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> O
if let Ok(chat) = Chat::load_from_db(context, group).await {
let group_name = chat.get_name();
let group_name_urlencoded =
utf8_percent_encode(&group_name, NON_ALPHANUMERIC).to_string();
utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
Some(format!(
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
@@ -233,6 +269,10 @@ pub enum JoinError {
// Note that this can only occur if we failed to create the chat correctly.
#[error("No Chat found for group (this is a bug)")]
MissingChat(#[source] sql::Error),
#[error("Ongoing sender dropped (this is a bug)")]
OngoingSenderDropped,
#[error("Other")]
Other(#[from] anyhow::Error),
}
/// Take a scanned QR-code and do the setup-contact/join-group/invite handshake.
@@ -260,12 +300,11 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
info!(context, "Requesting secure-join ...",);
let qr_scan = check_qr(context, &qr).await;
let invite = QrInvite::try_from(qr_scan)?;
context.bob.start_protocol(context, invite.clone()).await?;
match invite {
QrInvite::Contact { .. } => {
match context.bob.start_protocol(context, invite.clone()).await? {
StartedProtocolVariant::SetupContact => {
// for a one-to-one-chat, the chat is already known, return the chat-id,
// the verification runs in background
let chat_id = chat::create_by_contact_id(context, invite.contact_id())
@@ -273,11 +312,15 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
.map_err(JoinError::UnknownContact)?;
Ok(chat_id)
}
QrInvite::Group { ref grpid, .. } => {
// for a group-join, wait until the secure-join is done and the group is created
while !context.shall_stop_ongoing().await {
async_std::task::sleep(Duration::from_millis(50)).await;
}
StartedProtocolVariant::SecureJoin {
ongoing_receiver,
group_id,
} => {
// for a group-join, wait until the protocol is finished and the group is created
ongoing_receiver
.recv()
.await
.map_err(|_| JoinError::OngoingSenderDropped)?;
// handle_securejoin_handshake() calls Context::stop_ongoing before the group
// chat is created (it is created after handle_securejoin_handshake() returns by
@@ -287,10 +330,11 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
let start = Instant::now();
let chatid = loop {
{
match chat::get_chat_id_by_grpid(context, grpid).await {
match chat::get_chat_id_by_grpid(context, &group_id).await {
Ok((chatid, _is_protected, _blocked)) => break chatid,
Err(err) => {
if start.elapsed() > Duration::from_secs(7) {
context.free_ongoing().await;
return Err(JoinError::MissingChat(err));
}
}
@@ -298,6 +342,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
}
async_std::task::sleep(Duration::from_millis(50)).await;
};
context.free_ongoing().await;
Ok(chatid)
}
}
@@ -356,11 +401,11 @@ async fn send_handshake_msg(
Ok(())
}
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> u32 {
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await[..] {
contact_id
async fn chat_id_2_contact_id(context: &Context, contact_chat_id: ChatId) -> Result<u32, Error> {
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await?[..] {
Ok(contact_id)
} else {
0
Ok(0)
}
}
@@ -368,8 +413,8 @@ async fn fingerprint_equals_sender(
context: &Context,
fingerprint: &Fingerprint,
contact_chat_id: ChatId,
) -> bool {
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await[..] {
) -> Result<bool, Error> {
if let [contact_id] = chat::get_chat_contacts(context, contact_chat_id).await?[..] {
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
let peerstate = match Peerstate::from_addr(context, contact.get_addr()).await {
Ok(peerstate) => peerstate,
@@ -380,7 +425,7 @@ async fn fingerprint_equals_sender(
contact.get_addr(),
err
);
return false;
return Ok(false);
}
};
@@ -388,12 +433,12 @@ async fn fingerprint_equals_sender(
if peerstate.public_key_fingerprint.is_some()
&& fingerprint == peerstate.public_key_fingerprint.as_ref().unwrap()
{
return true;
return Ok(true);
}
}
}
}
false
Ok(false)
}
/// What to do with a Secure-Join handshake message after it was handled.
@@ -489,7 +534,7 @@ pub(crate) async fn handle_securejoin_handshake(
return Ok(HandshakeMessage::Ignore);
}
};
if !token::exists(context, token::Namespace::InviteNumber, &invitenumber).await {
if !token::exists(context, token::Namespace::InviteNumber, invitenumber).await {
warn!(context, "Secure-join denied (bad invitenumber).");
return Ok(HandshakeMessage::Ignore);
}
@@ -518,7 +563,7 @@ pub(crate) async fn handle_securejoin_handshake(
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
Some(BobHandshakeStage::Terminated(why)) => {
could_not_establish_secure_connection(context, bobstate.chat_id(), why)
.await;
.await?;
Ok(HandshakeMessage::Done)
}
Some(_stage) => {
@@ -547,7 +592,7 @@ pub(crate) async fn handle_securejoin_handshake(
contact_chat_id,
"Fingerprint not provided.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
};
@@ -557,16 +602,16 @@ pub(crate) async fn handle_securejoin_handshake(
contact_chat_id,
"Auth not encrypted.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await {
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id).await? {
could_not_establish_secure_connection(
context,
contact_chat_id,
"Fingerprint mismatch on inviter-side.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
info!(context, "Fingerprint verified.",);
@@ -579,13 +624,13 @@ pub(crate) async fn handle_securejoin_handshake(
contact_chat_id,
"Auth not provided.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
};
if !token::exists(context, token::Namespace::Auth, &auth_0).await {
if !token::exists(context, token::Namespace::Auth, auth_0).await {
could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.")
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
if mark_peer_as_verified(context, &fingerprint).await.is_err() {
@@ -594,12 +639,12 @@ pub(crate) async fn handle_securejoin_handshake(
contact_chat_id,
"Fingerprint mismatch on inviter-side.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await;
info!(context, "Auth verified.",);
secure_connection_established(context, contact_chat_id).await;
secure_connection_established(context, contact_chat_id).await?;
emit_event!(context, EventType::ContactsChanged(Some(contact_id)));
inviter_progress!(context, contact_id, 600);
if join_vg {
@@ -659,12 +704,12 @@ pub(crate) async fn handle_securejoin_handshake(
Some(mut bobstate) => match bobstate.handle_message(context, mime_message).await {
Some(BobHandshakeStage::Terminated(why)) => {
could_not_establish_secure_connection(context, bobstate.chat_id(), why)
.await;
.await?;
Ok(HandshakeMessage::Done)
}
Some(BobHandshakeStage::Completed) => {
// Can only be BobHandshakeStage::Completed
secure_connection_established(context, bobstate.chat_id()).await;
secure_connection_established(context, bobstate.chat_id()).await?;
Ok(retval)
}
Some(_) => {
@@ -778,7 +823,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
contact_chat_id,
"Message not encrypted correctly.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
let fingerprint: Fingerprint = match mime_message.get(HeaderDef::SecureJoinFingerprint)
@@ -790,7 +835,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
contact_chat_id,
"Fingerprint not provided, please update Delta Chat on all your devices.",
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
};
@@ -800,7 +845,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
contact_chat_id,
format!("Fingerprint mismatch on observing {}.", step).as_ref(),
)
.await;
.await?;
return Ok(HandshakeMessage::Ignore);
}
Ok(if step.as_str() == "vg-member-added" {
@@ -813,8 +858,11 @@ pub(crate) async fn observe_securejoin_on_other_device(
}
}
async fn secure_connection_established(context: &Context, contact_chat_id: ChatId) {
let contact_id: u32 = chat_id_2_contact_id(context, contact_chat_id).await;
async fn secure_connection_established(
context: &Context,
contact_chat_id: ChatId,
) -> Result<(), Error> {
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?;
let contact = Contact::get_by_id(context, contact_id).await;
let addr = if let Ok(ref contact) = contact {
@@ -826,14 +874,16 @@ async fn secure_connection_established(context: &Context, contact_chat_id: ChatI
chat::add_info_msg(context, contact_chat_id, msg).await;
emit_event!(context, EventType::ChatModified(contact_chat_id));
info!(context, "StockMessage::ContactVerified posted to 1:1 chat");
Ok(())
}
async fn could_not_establish_secure_connection(
context: &Context,
contact_chat_id: ChatId,
details: &str,
) {
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await;
) -> Result<(), Error> {
let contact_id = chat_id_2_contact_id(context, contact_chat_id).await?;
let contact = Contact::get_by_id(context, contact_id).await;
let msg = stock_str::contact_not_verified(
context,
@@ -850,6 +900,8 @@ async fn could_not_establish_secure_connection(
context,
"StockMessage::ContactNotVerified posted to 1:1 chat ({})", details
);
Ok(())
}
async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) -> Result<(), Error> {
@@ -940,6 +992,7 @@ mod tests {
dc_join_securejoin(&bob.ctx, &qr).await.unwrap();
let sent = bob.pop_sent_msg().await;
assert!(!bob.ctx.has_ongoing().await);
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
@@ -1026,6 +1079,7 @@ mod tests {
let chat = alice.create_chat(&bob).await;
let msg_id = chat::get_chat_msgs(&alice.ctx, chat.get_id(), 0x1, None)
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),
@@ -1074,6 +1128,7 @@ mod tests {
let chat = bob.create_chat(&alice).await;
let msg_id = chat::get_chat_msgs(&bob.ctx, chat.get_id(), 0x1, None)
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),
@@ -1124,7 +1179,6 @@ mod tests {
// Ensure Bob knows Alice_FP
let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await.unwrap();
let peerstate = Peerstate {
context: &bob.ctx,
addr: "alice@example.com".into(),
last_seen: 10,
last_seen_autocrypt: 10,
@@ -1171,6 +1225,7 @@ mod tests {
_ => panic!("Wrong event type"),
}
}
assert!(!bob.ctx.has_ongoing().await);
// Check Bob sent the right handshake message.
let sent = bob.pop_sent_msg().await;
@@ -1286,6 +1341,7 @@ mod tests {
};
let sent = bob.pop_sent_msg().await;
assert!(bob.ctx.has_ongoing().await);
assert_eq!(sent.recipient(), "alice@example.com".parse().unwrap());
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
@@ -1404,5 +1460,6 @@ mod tests {
let bob_chatid = joiner.await;
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await.unwrap();
assert!(bob_chat.is_protected());
assert!(!bob.ctx.has_ongoing().await)
}
}

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