Compare commits

..

621 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
holger krekel
ce09988ee5 try running the upload script on python3.6 2021-02-13 16:57:09 +01:00
holger krekel
a83293102e another round of python3.5 fixes and retagging 2021-02-13 15:45:31 +01:00
holger krekel
c3232e6d8f fix python3.5 2021-02-13 14:29:40 +01:00
holger krekel
d0f0728245 fix according to @r10s suggestion 2021-02-13 13:54:02 +01:00
holger krekel
5e4dde12e2 try working with empty circle_branch env vars 2021-02-13 13:54:02 +01:00
B. Petersen
ced3a56da4 add XEP-0392 to standards.md 2021-02-13 13:37:11 +03:00
link2xt
a2c3233c19 Implement Consistent Color Generation (XEP-0392)
This should make colors used by Delta Chat for emails similar to colors
used by XMPP clients implementing the same specification[1], such as
Conversations and Snikket.

Previously Delta Chat used only hardcoded 16 colors, so this change
should also increase the number of colors and make it easier to
distinguish different contacts.

[1] https://xmpp.org/extensions/xep-0392.html
2021-02-13 03:09:08 +03:00
B. Petersen
982dc53dc1 add some tests for lookup_by_contact_id() 2021-02-12 18:43:34 +01:00
Floris Bruynooghe
be7cee2c37 Avoid ChatId::is_unset when querying location sending
Avoid using the 0 ChatID as a special value, use Options instead.  For
get_range() also do this for contact_id.
2021-02-12 18:37:24 +01:00
Floris Bruynooghe
148ad31024 clippy 2021-02-12 18:28:51 +01:00
Floris Bruynooghe
6fddcd83c1 Fix doc comments 2021-02-12 18:28:51 +01:00
Floris Bruynooghe
46a3226e43 Bunch of feedback: renames, simple functions, no cow 2021-02-12 18:28:51 +01:00
Floris Bruynooghe
29f184b4c4 Strongly type stock strings
This changes the internal stock strings API to be more strongly typed,
ensuring that the caller can not construct the stock string in the
wrong way.

The old approach left it to the callers to figure out how a stock
string should be created, now each stock string has their specific
arguments and callers can not make mistakes.  In particular all the
subtleties and different ways of calling stock_system_msg() disappear.

This could not use a trait for stock strings, as this would not allow
having per-message typed arguments.  So we needed a type per message
with a custom method, only by convention this method is .stock_str().
The type is a enum without variants to avoid allowing someone to
create the type.

Sadly the fallback string and substitutions are still far away from
each other, but it is now only one place which needs to know how to
construct the string instead of many.
2021-02-12 18:28:51 +01:00
B. Petersen
ad640e163c add dc_contact_get_auth_name() 2021-02-12 11:53:29 +01:00
link2xt
40d9a1ec22 Do not return quoted messages from the trash chat 2021-02-12 10:47:47 +01:00
link2xt
0601b05cb7 Use footer as a contact status 2021-02-11 13:57:49 +03:00
holger krekel
59f9fc7cbf the test machinery is too stateful in waiting for account configure+start -- tests from deltabot got into a deadlock while setting up accounts. Be more specific with the "get online acocunt" helpers. 2021-02-10 18:41:51 +01:00
B. Petersen
a5c8c977db bump version to 1.51.0-alpha.0
1.51.0-alpha.0 is somewhere between 1.50 and 1.51,
as various beta-versions come or already came out with the new core,
this is helps in logs/debugging.
2021-02-10 15:47:25 +01:00
Hocuri
10435a10e9 Don't scan watched folders
This should fix the duplicate-messages-bug
2021-02-10 16:56:52 +03:00
link2xt
e82ec23024 python: support location markers 2021-02-10 08:36:56 +01:00
B. Petersen
c41f1b42df update docs for dc_contact_get_name() 2021-02-09 16:52:26 +01:00
link2xt
3f9242a610 Fix contact name update rules
The following rules apply now:
1. "name" column is only updated manually and never over the network
2. "authname" column is only updated over the network and never manually
3. Displayname is a "name" if it is non-empty, otherwise it is "authname".

This fixes a known (pytest.xfail) problem of "name" being changed over
the network when user has set it to non-empty string manually.

This also fixes the problem when "name" and "authname"
became unsynchronized accidentally, when they were equal and then
Origin::IncomingUnknownTo update arrived, setting "name" but not
"authname". Rust regression test is added for this case.
2021-02-09 17:44:25 +03:00
B. Petersen
6a4624be25 add a test to make sure, contacts cannot be added/removed to one-to-one chats 2021-02-09 12:54:09 +01:00
B. Petersen
5922069b77 rename mailinglists, remove real_group_exists()
renaming mailing lists and setting mailinglists images
(possible locally, but not synced)
was blocked because the function real_group_exists() checked
the chattype on a numerically.

it is not 100% clear to me,
why this function exist at all,
it just checks if a record with type=120 (group) exists under the given id -
this is the same as loading the chat and check chat.typ afterwards.

might be a premature optimisation relict from core-c
or made some error handling easier there.
2021-02-09 12:54:09 +01:00
link2xt
a2d64cbb4c Show sender name override in chatlist summary 2021-02-09 10:54:13 +01:00
Floris Bruynooghe
65fb2d791b Avoid ChatId::is_unset for searching messages
Using a ChatId value of 0 to indicate global search is a left over
from C-land.  Refactor this to be an option instead.
2021-02-08 22:12:33 +01:00
B. Petersen
6aeda98c0a comment on the quoted-encded test 2021-02-08 22:06:31 +01:00
Floris Bruynooghe
a8c389c3b4 Disable debug info in the dev profile by default
This does not affect the release profile and it significantly speeds
up compilation.  In the rare cases where panics are not sufficient to
debug something you can manually enable this again.
2021-02-08 21:38:07 +01:00
Floris Bruynooghe
3c2d698f4c Fixup example 2021-02-08 18:55:16 +01:00
Floris Bruynooghe
61964707d3 Remove use of ChatId::is_unset
The ChatId::is_unset method is something we need to get rid of to
clean up the type.  This removes one use-case in generating QR codes
for securejoin.
2021-02-08 18:55:16 +01:00
Floris Bruynooghe
d3b66cf724 Remove obsolete ChatId::is_unset check
The get_chat_id_by_grpid() call is already resultified.  The database
can only contain consistent data since on creation it inserts rows for
the special chats which do not have a grpid set so can never match.
Thus we can safely remove another use of the ChatId::is_unset call
which should be removed to clean up the type.
2021-02-08 10:45:30 +03:00
link2xt
24602ed8a8 Use .first_word() to get enhanced status code (#2202)
.get(0) gets the whole first line
2021-02-08 01:51:31 +01:00
jikstra
e741cb3646 Implement immediate failing for x.1.1 and x.1.2 status codes too. 2021-02-08 02:20:33 +03:00
jikstra
93ba6c1ce8 Transient errors with the extended smtp status code 4.1.2 should fail immediatly 2021-02-08 02:20:33 +03:00
Hocuri
396ec131fc Implement receiving mailing lists (#1964)
* Copypaste-merge my old work

* Start implementing mailinglists the new way

* Create pseudo contact

* Fine-tune docs

* Remove some unnecessary changes

* style

* Make a stock str

* Fix a crash. Yes, this line caused a panic when reconfiguring on Android

(without a reasonable error log). Also, I could not receive any messages
anymore.

* rfmt

* Add tests and make them pass

* Even more tests

* rfmt

* Enhance test and fix bug

* Don't update the sender name when prefetching because maybe it's a mailing list

* Use an enum instead of numbers for the decision

* Don't remove anyone from mailing lists

* Fix bug in the regex

* Adjust error msg

* Compile error after rebase

* Correctly emit event

* Add dc_msg_is_mailing_list so that you can find out whether messages in the deaddrop belong to mailing lists.

* Add received headers to unit tests

* Comments, small tweaks

* Use dc_msg_get_override_sender_name instead of dc_msg_get_sender_name

* Add dc_msg_get_sender_first_name() because sometimes the first name was not correctly shown in mailing lists

* small fixes, don't let the user modify mailing list groups

* Hide contacts for addresses like noreply@github.com and reply+AEJ...@reply.github.com

When testing mailing lists, I noticed that sometimes a mailing list
contact got a name (like, hocuri <noreply@github.com>). It turned out
that ages ago, I had accidentally written an email to - in this example
- hocuri <noreply@github.com> and it had been added to the contacts
list.

This hides email addresses from the contacts list that are obviously not
meant to be written at and prevents updating the names.

* Comment, clippy

* Replace u32 with ChatId

* Resolve lost of small issues from the reviews

* remove dc_msg_get_sender_first_name

* add dc_msg_get_real_chat_id()

this allows to check if a contact request belongs to a mailing list
and to show name/avatar/whatever of the mailinglist.

another approach was to duplicate some chat-apis for messages
(eg. dc_msg_is_mailing_list()) however that would require far more new apis.

the idea to change the behavior of dc_msg_get_chat_id() would be
more clean, however, that easily breaks existing implementations
and can led to hard to find bugs.

* remove now unused Message.is_mailing_list()

* if a name for a mailing list is missing, use List-ID

* fix comment

* fix error message

* document how dc_get_chat_contacts() works for mailing lists

* refine decide api (#2185)

* add DC_DECIDE* constants to deltachat.h, tweak documentation

* use StartChat/Block/NotNow instead of Yes/Never/NotNow

* decide_on_contact_request works on ctx/msg-id

functions working on message-objects
usually do not read or write directly on the database.

therefore, decide_on_contact_request()
should not work with message-objects as well,
it is even a bit misleading, as eg. chat-id of the object is not modified.

instead, the function works on context,
similar to dc_send_msg(), dc_create_chat() and so on.
for now, i moved it to context, could maybe be part od MsgId.

* Update src/chatlist.rs

Co-authored-by: Hocuri <hocuri@gmx.de>

Co-authored-by: Hocuri <hocuri@gmx.de>

* refine documentation

* re-add accidentally deleted Param::MailingList

* remove pseudo-contact in domain @mailing.list

1. the pseudo-contact was added to the mailing list contacts,
   which is not needed.
   might be that we want to add the recent contacts there in a subsequent pr
   (we do not know all contacts of a mailing list)

2. the pseudo-contact was used to block
   mailing lists; this is done by setting the chat to Blocked::Manually now

3- the pseudo-contact was used for unblocking;
   as it is very neat not to require additional ui for mailing list unblocking,
   might be that we introduce a similar pseudo-contact for this limited purpose
   in a subsequent pr, however, the pseudo-contact needs to exist only
   during unblocking then, maybe also the special domain is not needed,
   we'll see :)

* Move dc_decide_on_contact_request() up to the dc_context section as it's a member of dc_context now

More specifically, to the "handle messages" section

* re-introduce Chattype::Mailinglist (#2195)

* re-introduce Chattype::Mailinglist

* exhaustive chattype-check in fetch_existing_msgs() and get_summary2()

* exhaustive chattype-check in ndn_maybe_add_info_msg()

* exhaustive chattype-check in message::fill()

* remove dc_chat_is_mailing_list() from ffi

Co-authored-by: B. Petersen <r10s@b44t.com>
2021-02-07 23:37:04 +01:00
B. Petersen
5acf8e1aac support videochat-services not suppporting rooms
some videochat-services do not support adding random rooms to the url,
users let people in manually, so they can always use the same url.

as hacks as hiding the genrated random room behind `#$ROOM` or `?=foo=$ROOM`
does not always work, this pr introduces the parameter `$NOROOM` that is
just replaced by the empty string.

see https://support.delta.chat/t/videochat-with-webex/1412
for further details.
2021-02-07 19:43:36 +01:00
link2xt
0cd8710289 Add dc_get_chat_encrinfo() 2021-02-07 20:29:43 +03:00
link2xt
fbec12393d Improve dc_get_contact_encrinfo()
Return error for special IDs. Previously "No encryption." was returned for DC_CONTACT_ID_SELF.

Show "No encryption." if peerstate is reset.

Never talk about transport encryption: it is misleading. Even if TLS is used for client IMAP and SMTP connections, MTA-MTA connection may be unencrypted if MX does not support STARTTLS.

Add a Rust test.
2021-02-07 20:29:43 +03:00
B. Petersen
a34cfd56b4 update link to 'Go' repo 2021-02-06 02:02:08 +01:00
bjoern
0ef6a3060a fix From:-header bug (#2193)
* add failing test for From:-header bug

the test fails on the last check for quote-encapsulated utf-8 strings.

* Update mailparse

Co-authored-by: link2xt <link2xt@testrun.org>
2021-02-05 20:57:38 +01:00
link2xt
dc893bf5cd fake_idle: unwrap watch_folder early to avoid doing it in a loop
This also makes info message "IMAP-fake-IDLEing folder=..." nicer,
because the folder name is not wrapped into Some() anymore.
2021-02-05 19:57:42 +03:00
Hocuri
b0a3a0046c Add new sentbox_move to get_info() 2021-02-04 20:26:00 +01:00
Hocuri
8ac2cdd929 Add set_config_bool, fix rust test to test the current expected behavior 2021-02-04 20:26:00 +01:00
Hocuri
e820d072f5 Disable moving to Sentbox for now
fix https://github.com/deltachat/deltachat-core-rust/issues/2176

we can re-enable it when DC determines automatically which folders to watch and
this is deployed for at least 1 release.
2021-02-04 20:26:00 +01:00
link2xt
6bb0c164f9 Parse QR codes without using Param
Param should only be used to parse dictionaries stored in SQL database, not external data.

Since Param parser has been extended to escape newlines with newlines, QR-codes parser started to treat && as escaped &, which is wrong.

This change also uses String keys like "n" for "name" instead of completely unrelated constants like Param::SetLongitude.
2021-02-03 21:26:23 +03:00
Floris Bruynooghe
5ee4bb58cd Tidy up imports 2021-01-31 12:46:01 +01:00
Floris Bruynooghe
002ea8ed98 Limit async-std to <= 1.8
The stop-token crate does net yet support a more recent async-std.
2021-01-31 12:46:01 +01:00
Floris Bruynooghe
fe9c419e5d Update tests to new async-std channel api 2021-01-31 12:46:01 +01:00
dignifiedquire
2407fbd1f0 fix scheduler shutdown 2021-01-31 12:46:01 +01:00
dignifiedquire
b3fe74e0f0 update deps
and switch to new channels  in async-std@1.8
2021-01-31 12:46:01 +01:00
link2xt
93bd9422e7 Improve formatting of ephemeral timer change messages
This adds support for more timer values and better fallback for unknown values.

For example, 90 seconds is formatted as "1.5 minutes".
2021-01-31 00:44:57 +03:00
link2xt
b53415fed5 ffi: add missing DC_STR_SERVER_TURNED_OFF #define 2021-01-31 00:44:57 +03:00
Floris Bruynooghe
4a30cb6cd6 Explain more what's going on 2021-01-30 18:36:47 +01:00
Floris Bruynooghe
9ef0fefb75 Non-translated messages to the log 2021-01-30 18:36:47 +01:00
Floris Bruynooghe
1e2ec8e264 Adapt to new TestContext api
The newer API is more consistent with it's naming.  This is somehow
fallout from a master rebase.
2021-01-30 18:36:47 +01:00
Floris Bruynooghe
0c27e8ccaa PR review feedback
- doc fixes
- make BobStateHandle safer by moving the state out of the handle.
- handle more match cases explicit in BobState returns
- fewer mutable variables
2021-01-30 18:36:47 +01:00
Floris Bruynooghe
6a834c9756 Move BobState to a private submodule
This does not only organise things better, but most importantly the
BobStateHandle is now not in the same module as its users.  This means
it can guarantee safety about how it is initialised and use
unreachable!() to simplify it's API.
2021-01-30 18:36:47 +01:00
Floris Bruynooghe
803452cbde Move QrInvite out to a submodle 2021-01-30 18:36:47 +01:00
Floris Bruynooghe
11e3380f65 Introduce a state machine for Bob's secure-join
This introduces a state machine which takes care of managing the
handshake transitions in the secure-join protocol.  This separates
user interactions from the protocol state handling.

This means that while handling the protocol state there are a bunch of
failures no longer possible due to all state information being
guaranteed to be present.  As part of this the QR-code state has been
extracted from the generic Lot structure to something suitable just
for the SecureJoin protocol.

A LogSink has been added to the testing tools allowing log messages to
be correctly displayed on test failures.
2021-01-30 18:36:47 +01:00
Floris Bruynooghe
4508eced37 Use bail macro
This seems to be our standard way to do this.
2021-01-28 01:16:11 +03:00
link2xt
47a6e31047 fix: allow emojis for location markers
is_marker() should check for length in characters,
not bytes.

This was broken during the Rust translation
in changeset a5553f98af
2021-01-28 00:56:35 +03:00
Floris Bruynooghe
785cc795e3 Resultify Contact::lookup_id_by_addr
Using a Contact ID as an error type is risky and a C leftover.
2021-01-27 21:42:05 +01:00
B. Petersen
6b9b39b953 the virtualenv.pypa.io-link without .html extension shows a 404 2021-01-27 16:35:32 +03:00
B. Petersen
1ac44e5a77 fix internal link in README.rst
there must not be a space between link text and the link in angle brackets:
https://sublime-and-sphinx-guide.readthedocs.io/en/latest/references.html
2021-01-27 13:18:38 +01:00
bjoern
1e6d8063c8 add dc_msg_set_html() api (#2153)
* draft dc_msg_set_html() api

* implement setting 'html to be send'

* test sending html-parts

* more flexible html-partbuilder

* write html-parts to database and also send them

* add 'sendhtml' command to repl tool
2021-01-27 12:56:22 +01:00
link2xt
c8c2724c28 Apply gossiped encryption preference to new peerstates
Encryption preference was already applied since commit 78d855c5ca,
but only for already existing peerstates.

As a result, new users ignored gossiped encryption preference in the
"member added" message and had to wait until someone gossiped encryption
preferences for inactive users the second time.
2021-01-26 23:18:36 +03:00
B. Petersen
e6dd963ebb #define removed ffi-funcion dc_contact_get_first_name as dc_contact_get_display_name to make updating easier 2021-01-26 03:02:19 +03:00
link2xt
564d681bca Try to extract group ID from In-Reply-To and References as a last resort
This should help if parent message can't be found because messages were
reordered or deleted.

This does not prevent group IDs from being removed from Message-IDs in
the future, in which case it will become dead code.
2021-01-26 03:00:13 +03:00
link2xt
c469798734 Assign classic replies to two-member groups to group chat
Previously it was assigned to 1:1 chat.

To keep "reply privately" working, old behavior is retained for chat messages.
2021-01-26 02:56:57 +03:00
Floris Bruynooghe
355e0145c0 Depend on anyhow directly
This removes the proxy via crate::error to depend on anyhow directly.
There is no benefit to this indirection and this makes it simpler to
see which error types are used.
2021-01-24 17:29:52 +03:00
link2xt
7b5a3a8346 Stop extracting group IDs from Message-IDs 2021-01-24 01:24:39 +03:00
link2xt
3d072a81b4 Assign replies to chats based on In-Reply-To and References 2021-01-24 01:24:39 +03:00
link2xt
3fe5eb31d4 Remove member list heuristic for ad-hoc groups
It is going to be replaced by simpler and more reliable
matching by the parent message.
2021-01-24 01:24:39 +03:00
link2xt
b938d5facd add_parts: call get_parent_message only once 2021-01-24 01:24:39 +03:00
bjoern
e9c582c4e4 forward html-messages properly (#2151)
* add failing tests for forwarding html-mails

* let MsgId.get_html() return an option

* write html-part to local database on forwarding

* add html-part to forwarded non-dc messages

* read HTML-parts from encrypted messages

* avoid clone()

* Received:-header is no longer needed since #2152

* Update src/html.rs

Co-authored-by: Floris Bruynooghe <flub@devork.be>

* Update src/html.rs

Co-authored-by: Floris Bruynooghe <flub@devork.be>

* Update src/mimeparser.rs

Co-authored-by: Floris Bruynooghe <flub@devork.be>

* prefer 'orig' over 'org' as abbreviation for 'original'

* improve comment on tests

* prefer 'try_into()' over 'as u32' to avoid panics on bad data

* simplify ffi

Co-authored-by: Floris Bruynooghe <flub@devork.be>
2021-01-23 23:21:18 +01:00
B. Petersen
2a8c418d54 remove dc_contact_get_first_name() api
instead, dc_contact_get_display_name() should be used.

dc_contact_get_first_name() was created to save some space on the screen,
esp. on mobile devices,
however, this does not always work and has issues on its own
with some names ("Dr. Strangelove", ":) Name" and so on).

as with mailing lists, more apis with first_name() would be needed,
we decided to drop that instead of following that way.

it is also less an issue as some years ago as screens have become larger,
if really, needed, the ui can handle that more gracefully,
however, just using dc_contact_get_display_name() should be fine as well.
2021-01-23 18:50:22 +03:00
Floris Bruynooghe
b31becea2b Only document main crate for now
We don't really have docs in the other crates yet, and there's a
naming conflict between deltachat and deltachat_ffi that needs to
sorted to build both.  But deltachat_ffi is still documented with
doxygen for now so there's no need to sort this now.
2021-01-22 18:42:13 +01:00
Floris Bruynooghe
c4ebb0a31e Add rustdoc to CI and fix some doc comments
This fixes a bunch of doc comments and also adds the run to CI so that
failures get caught early.
2021-01-22 18:42:13 +01:00
Floris Bruynooghe
934dc420a8 Add Debug back to TestContext
I accidentally removed this in an earlier PR.
2021-01-22 08:04:35 +01:00
Hocuri
b67fbedcef Create get_configured_provider(), remove delete_server_after == "1" check 2021-01-21 10:36:00 +01:00
Hocuri
687db252b6 do not disable server-deletion when enabled by provider-db
fix #2157
2021-01-21 10:36:00 +01:00
bjoern
5561aada45 update get_info() (#2156)
* add failing test checking for missing get_info() values

* add missing get_info() values

* remove unused imap_folder resp. ImapFolder config
2021-01-19 22:30:19 +01:00
Floris Bruynooghe
412e3c22df Attempt to please cargo fmt
Even though for some reason my local one was already happy?
2021-01-19 20:59:12 +01:00
Floris Bruynooghe
687c92d738 Add more detail to securejoin tests
This also checks that some of the correct user interactions happen,
checking we get a joiner event and the verified chat messages.

It also extends the test utils with the ability to distinguish the
different context logs by having them named.
2021-01-19 20:59:12 +01:00
Floris Bruynooghe
83dd1c6232 New version of clippy has a lot of new lints
Lots of new clippy lints due to toolchain upgrade.

Made the Message::error field pub(crate) again, it was the odd one out
and it seemed a reasonable way to shut up clippy.
2021-01-19 11:20:33 +01:00
Floris Bruynooghe
2435803fa3 Update toolchain in all our files
We need to do more than just update rust-toolchain sadly.
2021-01-19 11:20:33 +01:00
Simon Laux
6d5ccdf721 Merge pull request #2132 from deltachat/add_chat_audit_log_view
add flag to get only info/system messages of chat
2021-01-18 21:44:16 +01:00
Simon Laux
43e95ce68b fix formatting of python code 2021-01-18 18:56:52 +01:00
B. Petersen
dae20d90ed add missing import 2021-01-18 17:45:57 +01:00
Simon Laux
7154a47f85 Update deltachat-ffi/deltachat.h
Co-authored-by: bjoern <r10s@b44t.com>
2021-01-18 17:35:58 +01:00
Simon Laux
9957bad83d rename DC_GCM_SYSTEM_ONLY to DC_GCM_INFO_ONLY
apply suggestions of r10s
2021-01-18 17:35:58 +01:00
Simon Laux
93845c2a18 include constant and use it 2021-01-18 17:35:58 +01:00
Simon Laux
7b80801cb7 clarifiy test name
(that it test without daymarker)
2021-01-18 17:35:57 +01:00
Simon Laux
32cbdc630d add test for chat audit log 2021-01-18 17:35:57 +01:00
Simon Laux
1d62448903 add flag to get only info/system messages of chat
useful for creating an "audit log" view of a chat that helps to find
actions like who added/removed who quickly and without endless scrolling
2021-01-18 17:35:57 +01:00
bjoern
f7ecf34ead document DC_STR in ffi (#2143)
* fix typo

* document DC_STR_* constants and related parts
2021-01-18 14:07:28 +01:00
Hocuri
ccee289a5c Don't ignore incoming mails without Received-header
fix #2149
2021-01-17 18:17:19 +01:00
Hocuri
08c46af3aa Switch to Rust 1.49.0
Notable improvements:
- [track_caller](https://github.com/rust-lang/rfcs/blob/master/text/2091-inline-semantic.md): We can get the location of the caller of a message, which makes logging easier as you can have dedicated functions (not only macros), e.g. write a function `log(self: Result, msg: string) -> Option`
- The output after a panic is shorter and hides lots of uninteresting
function calls
- Test output is better captured

After merging, we should probably wait a bit with using the track_caller
feature to make going back easier, should we get problems again.
2021-01-17 04:43:41 +03:00
Hocuri
4636785449 Revert "Add verbose logging for #2065"
This reverts commit 83df69f43c.
2021-01-15 15:06:05 +01:00
B. Petersen
e077ee9238 make clippy happy 2021-01-15 07:13:58 +03:00
B. Petersen
662735c233 word-encode group-name-changed if needed, use more meaningful names for the parameter 2021-01-15 07:13:58 +03:00
B. Petersen
fef2a48054 add a maybe_encode_words() function
maybe_encode_words() calls encode_words()
when needs_encoding() returns true.
2021-01-15 07:13:58 +03:00
Alexander Krotov
8412affe37 ffi: remove dc_is_io_running()
It is misleading and should never be checked.

dc_stop_io() also stops pending ephemeral message deletion tasks, so it
should be called in any case before releasing context.
2021-01-15 07:07:45 +03:00
Hocuri
ebccdbbcb9 Improve onboarding by scanning all folders from time to time (#2067)
Start implementing #1994

TODO (in later PRs):

- Add a hint to the watch settings that all folders are fetched from time to time (to be done in the individual UIs)
- folder names are case-insensitive, so double-check that all comparisons are case-insensitive
- The `scan_folders.rs` file didn't get as large as I expected and it's probably not worth it having an extra file for it. But if there are no objections, I'll make another PR to rename it to `folders.rs` and also put into it `configure_folders()` from `imap/mod.rs` and `needs_move()` with all its tests from `message.rs`.

Done:

- Most mailboxes have a "Drafts" folder where constantly new emails appear but we don't actually want to show them, what do we do about this? The most reliable way to detect such messages that we found up to now is:
  If there is no `Received` header AND it's not in the `ConfiguredSentbox`, then ignore the email.
- before or after INBOX idle trigger a new "scan all folders for messages".  It does a "list folders" and then goes through all folders with select-statements, checking if "next-uid" was changed since checked last time.  This might be batchable but in any case should not consume a lot of traffic. We might debounce this scan activity to happen at most every N minutes 

- if next-uid changed for a folder, we "prefetch" and "fetch" DC-messages as as needed ("dc-messages" are not just those with "Chat-Version" headers, but can also be regular emails) 

- if we discover DC-messages in folders that have the "/Spam" flag (maybe excluding ContactRequests) we automatically move them to INBOX/DeltaChat folder to help  provider-spam-systems to regard this contact/mail as non-spam 

- for now, we do not change any user visible option, but introduce this "scan all" automatically and on top of what exists.   The DeltaChat folder-watching does not perform scan-all-folders (maybe with the exception to trigger scan-all also with DeltaChat if INBOX is not watched)

- Tests (except if you have ideas to improve them)
- all folders, their last uidvalidity, next-seen etc. are kept in a separate "imap-sync" sqlite table.  Maybe this can be used to streamline some of the "Sent" folder and "DeltaChat" folder detection code we already have. 

- We now also move self-sent messages from the Inbox to the Sent folder if `mvbox_move` is off, as this was very easy to do now. This way, we now behave more like a normal MUA if the user wants this.

FOR LATER PRs:

- maybe for the first 50 messages or so, we could reduce the IDLE-timeout (currently 23 minutes or so) to faster detect messages sent to non-inbox folders. However, on Android and iOS, we would likely trigger scan-all when the app moves to foreground, and so  it might not be neccessary to reduce the current idle-timeout at least for them.  We can leave this "faster discovery" question for the end, after we move to real-life testing. 

- (Later on, after the above works, we can consider heuristics on which folders to perform IDLE on, and remove the Watch-folder options (inbox, deltachat, sent). We tried to find a safe scheme for already doing it but failed to fine one, too many unknowns, also some questions regarding multi-device (you might have different settings with each of it, one moves, the other doesn't etc.) so we postponed this in favor of the above incremental suggestion.)




* Start implementing #1994

* Add debug logs, it seems like the SQL migration can go into another pr

* Let fetch_new_messages return whether there are new emails

* Code style

* Don't prefetch if there are no new emails

* clippy

* Even more debug logs

* If the folder was not newly selected, return always try to fetch as

uid_next is probably outdated

* Fix new bug

* Recognize spam folder

* if we discover DC-messages in folders that have the "/Spam" flag (excluding ContactRequests) we automatically move them to INBOX/DeltaChat folder to help provider-spam-systems to regard this contact/mail as non-spam

* Clippy, prioritize folder_meaning over folder_name_meaning

* Add a first test, for the first day after installation only debounce to 2s

* Start adding two tests (both of them fail)

* Don't abort folder scan if one folder fails

* More consts

* Replace bool return value by enum

* Split test up into multiple tests

* Print logs during rust tests

* Rust tests pass now

* .

* One of the Python tests passes now - reconfigure folders during scanning

* Make the last test pass - Delete emails in all folders when starting the test, not only inbox and mvbox

The problem had been that emails were left in the folder "xyz"

* lint

* DB migration (untested)

* Store uid_next in SQL instead of lastseen in a config

* Revert "If Inbox-watch is disabled and enabled again, do not fetch emails from in between"

all folders are always watched, anyway

* clippy, rm debug logs, comments

* Codestyle, comments

* fixing things again

* Fix another test: don't fetch from uid_next-1 but uid_next; make some {} to {:#} so that we can use `.context(...)`

* move self-sent, non-setupmessage chat messages to the Sent folder if `mvbox_move` is off

* comment

* Comments, make sure things work even if there is no uid_next

* Style

* Comments

* The rust test tested wrongly

* comments, small codestyle change

* Ignore emails that are probably only drafts

Most mailboxes have a "Drafts" folder where constantly new emails appear
but we don't actually want to show them.
So: If there is no Received header AND it's not in the ConfiguredSentbox,
then ignore the email.

Also: Add test.

* Fix occasional test failure, it was introduced as DC now moves messages from Inbox to Sent

* Add `Received` header to the rust tests

* After this PR we will always watch all folders and delete messages there if server_delete is enabled. So, for people who have server_delete on, disable it and add a hint to the devicechat

* comment, small fix

* link2xt's first review

* Use ON CONFLICT(FOLDER) DO to update and if it doesn't exist, then insert

Reason from link2xt: We had a problem with multiple peerstates inserted due to key fingerprint parsing error previously. With logic in Rust a similar problem can occur: an UPDATE can fail for reasons other than a conflict. PRIMARY KEY should ensure uniqueness in this case, but anyway.

* Remove two TODO statements, remove fetch_new_messages: ignoring uid {}, uid_next was {} log

* Next TODO: Make uidvalidity and uid_next DEFAULT 0

* rm two TODOs, Seems like we are not going to `exclude folders that are watched anyway` in this PR

* small tweak: Handle instants more carefully

* Add scan_all_folders_debounce_secs config for tests, set debounce to 60s (before it was just 2s during the first day)

* Don't use bold letters for the device message

* React to changes in the folders better

Before, if there was a configured Sent folder, but then it got
removed and replaced with another folder with a name meaning "Sent" but
without Sent flag, it would be ignored.

So, instead of checking against ConfiguredSentboxFolder,
create two Option variables at the beginning of the loop and replace
them with Some if it is None. At the end of the loop, store the new
values into ConfiguredSendboxFolder and ConfiguredSpamFolder, even if it
is None.

Also, derive some useful traits.

* move job: Return a meaningful error if server_folder is None instead of panicing

* small error-handling fix

* Fix test_fetch_existing() python test

Before, we sometimes got a race condition where scan_folders() sees that
there is a Sentbox and saves this info after we set the
ConfiguredSentbox to None and before the message is sent.

So, just expect that the message is moved to the sentbox.

* migration is 72 now

* rm 2 TODOs, Don't infinitely retry when dc_receive_imf() returns Err

* clippy: Remove glob imports

* Delete created folders at the beginning of tests

(some created folders made problems in the next tests because)

* Improve resetting accounts between tests
2021-01-13 14:09:51 +01:00
Alexander Krotov
3c387a3cb3 refactor: get rid of ctx1 variables using rebinding 2021-01-13 14:22:01 +03:00
Alexander Krotov
5e8e77dfb6 clippy: forbid wildcard_imports 2021-01-12 12:06:13 +03:00
Alexander Krotov
eeba70eb49 Fix a clippy warning 2021-01-12 12:06:13 +03:00
bjoern
e2688f6355 add option to access original message (#2125)
* draft API to deal with uncut message texts

* add column mime_modified

* add mime_modified flag to MimeParser and save it in the database

* save mime_headers also when mime_modified is set

* cargo fmt

* set mime_modified on parsed html-texts and when there are multiple alternative-parts; add test for that

* prototype functions, add to repl and ffi

* use correct mime_modified flag

* basically parse Mime-Structure to HTML

* add basic tests for HTML-parsing

* convert text/plain to html for getting original

* respect charset for plain texts

* make test more specific

* fix handling non-utf-8 charsets for plain messages

* add test for plain_to_html()

* add failing test for plaintext linkify

* linkify urls in plain text

* fix regex

* plain text linkify: add failing test for encapsulated links as <https://domain.com>

* plain text linkify: make encapsulated links as <https://domain.com> work

* plain text linkify: require word boundary at beginning of link, add tests for that

* plain text linkify: linkify emails

* plain text: support format=flowed

* plain text: support quotes

* make clippy happy

* set mime-modified also when simplify() cuts non-html messages, add tests for that

* streamline mime recursion

* repl tool: write original html to file for further processing

* convert cid:- to data:-protocol

* add a test for cid: to data: conversion

* make clippy happy

* fix html-tests to work with windows-lineends

* clarify what the returned html-code may contain

* add some more detailed doc comments

* add mime_modified column only if not exist

this additional check is needed
as the column may added with another dbversion in
some shipped beta-versions.

* incorporate documentation suggestions from review

* rename get_original_mime_html() to more simple get_html()

* rename api is_mime_modified() to more simple has_html(); internally, mime_modified-flag stays as-is, however

* rename MimeS to MimeMultipartType

* do not set mime-modified flag for encrypted messages that need extra-handling for saved mime-structure

* fix typo

* move get_msg_html() to MsgId.get_html()

* incorporate more documentation suggestions from review

* remove unused return value from collect_texts_recursive()

* avoid mime_modified being mutable in write-parts-loop

* move 'use futures::future::FutureExt' atop of html.rs

* move attributes defining plain-text to a dedicated structure

* more PlainText to separate file

* escape cid when building regex

* let dc_get_msg_html() return NULL when calling with bad param
2021-01-11 17:40:35 +01:00
Alexander Krotov
bb9e6038c4 python: add error property to messages 2021-01-10 23:31:50 +03:00
bjoern
0e1ca4323c Merge pull request #2134 from deltachat/sync-db-version
use dbversion=71 for updating ConfiguredProvider
2021-01-10 01:06:26 +01:00
B. Petersen
1b9cd18e33 use dbversion=71 for updating ConfiguredProvider
in addition to merged pr #2123,
dbversion=70 was also used by pr #2125.

pr #2125 is not yet merged, however beta-versions were shipped with
that pr, so, dbversion=70 is already in use on some systems and on these
systems ConfiguredProvider won't be set.

the other way round, that ConfiguredProvider is set twice on some systems
affects only dev-installations and should not harm.

once this pr is merged, #2125 needs a rebase and will use dbversion=72 for
its changes to add the new column also n the dev-installations :)
(we need to check that the column to add does not exist, however)
2021-01-10 00:40:59 +01:00
Alexander Krotov
f4c8ffca4c Query MX records during provider autoconfiguration
Previously MX records were queried only for OAuth 2 configuration and
did not affect the list of servers tried. User was required to manually
configure the servers for Google Workspace (former GSuite) domains.

Now MX records are queried during configuration. If provider is found in
offline database, its ID, corresponding to the filename, is saved as
`configured_provider`.

`configured_provider` is also set during database migration if email
address uses the domain from the provider database, but no MX querying
is done.
2021-01-10 00:19:43 +03:00
B. Petersen
6edff503aa add reference to RFC 3676 to standards.md
RFC 3676 is understood for incoming messages
and used for outgoing messages.
2021-01-08 19:33:52 +03:00
Alexander Krotov
31ae099e19 configure: simplify get_autoconfig() 2021-01-06 16:55:15 +03:00
Alexander Krotov
0f90d50385 repl: update list of commands
send-garbage has been removed, and setqr was not listed in help
2021-01-03 20:38:47 +03:00
Alexander Krotov
5bbbe4b79c Missing paren in the comment 2020-12-29 01:30:27 +03:00
bjoern
c243b89f7d Merge pull request #2121 from deltachat/strict-tls-default
Enable strict TLS by default
2020-12-27 00:47:54 +01:00
Alexander Krotov
4690ba017f Update provider database with server fixes 2020-12-27 02:37:14 +03:00
Alexander Krotov
38ead5b72c Enable strict TLS by default
Now strict TLS should be explicitly turned off.

By default it is only turned off for nauta provider.
2020-12-27 02:36:11 +03:00
Alexander Krotov
30c334d887 Resolve MX domain only once per OAuth2 provider
Currently we have only one provider in the list, so there is no
difference, but more providers (e.g. Yandex.Connect) may be added later.
2020-12-26 21:33:59 +03:00
Hocuri
78fd0c285b Delete messages more thoroughly (and at all) (#2114)
- Make sure delete_expired_messages and housekeeping runs once a day
- delete more info about messages when putting them to trash (esp. also
delete txt_raw, from_id and to_id as we don't need those anymore, so
they are data that was unnecessarily kept)

fix #1926, fix #2090

Also:

* Nicer test_utils: add send_text() and print_chat()

* Adapt ephemeral messages for testing (make them accurate to the second)

* Add test for ephemeral messages

* Make pop_sent_msg() really pop the last sent message
2020-12-26 18:29:43 +01:00
Alexander Krotov
86a8767d94 Update vendored OpenSSL version
This fixes linking of OpenSSL on Ubuntu 20.04.1 LTS.

Related issue: https://github.com/openssl/openssl/issues/10842
2020-12-25 18:48:45 +03:00
Alexander Krotov
4bc4aa0705 Fix compilation of python bindings in release mode 2020-12-25 07:54:40 +03:00
Alexander Krotov
f98aa0d906 Make maybe_remove_bad_parts() non-async 2020-12-22 20:51:57 +03:00
Alexander Krotov
179dd0f3a1 Fix "dehtlm_failed" typo 2020-12-22 20:51:42 +03:00
bjoern
8979232cfb Merge pull request #2111 from deltachat/add-subj-to-multimedia-msg
Add subject to multimedia message text
2020-12-21 13:56:30 +01:00
Hocuri
d3aba4e817 Simplify if condition 2020-12-20 20:04:19 +01:00
Hocuri
53fed91a17 Fix marking read receipts as seen
If mvbox_move was on and an mdn was received, it was not marked as read.

Also remove a confusing log that said that we are adding a markseen job, even if we weren't. As job::save() logs itself, there is no need to log this again.
2020-12-20 20:04:19 +01:00
Alexander Krotov
ff3dd878c5 Use Python 3 for all Python scripts 2020-12-19 14:55:22 +03:00
Hocuri
03232eb79c Adapt other tests to the fact that now the subject is prepended more often 2020-12-18 18:44:16 +01:00
Hocuri
9edc6702f1 Fix #2078 2020-12-18 18:44:16 +01:00
Hocuri
2b207e1375 rustfmt 2020-12-18 18:35:54 +01:00
Hocuri
4d2c2130e8 Replace all t.ctx with t
Not sure if that's the way to go because, you know, merge conflicts, but
we could do this now.
2020-12-18 18:35:54 +01:00
Hocuri
af8a6d7722 Create and use some test helper functions 2020-12-18 18:35:54 +01:00
Alexander Krotov
bc67fa3204 python: use configured_mail_{port,security} when creating IMAPClient
Previously only TLS on port 993 was allowed.
2020-12-18 18:39:30 +03:00
Hocuri
29991f1caf Add test 2020-12-16 13:41:14 +01:00
Hocuri
e982549046 Next try to re-enable fetch-existing-msgs, fixing #2097 2020-12-16 13:41:14 +01:00
Hocuri
ec83fae314 Parse name="quote" divs (#2104)
fix #1560 Replies in html-only format are not converted nicely wrt Quoting
2020-12-13 18:02:20 +01:00
Alexander Krotov
518e87b0cf Compare Chat-Disposition-Notification-To: address case-insensitively
Otherwise users with uppercase letters in their addresses never receive
read receipts.
2020-12-06 23:29:11 +03:00
bjoern
09113e2579 correct and refine docs for DC_MSG_IMAGE (#2099)
* correct and refine docs for DC_MSG_IMAGE

* unify 'eg.' to 'e.g.'
2020-12-06 20:34:03 +01:00
Alexander Krotov
eb693a4a21 Add Rust test for ephemeral timer messaging 2020-12-06 21:09:13 +03:00
Alexander Krotov
00a223b574 test_utils: set message state to OutDelivered on .pop_sent_msg()
Also remove SentMessage.id. It was set to foreign_id of the job, which
is a MsgId, not ChatId, so tests using it were not correct anyway.
2020-12-06 21:09:13 +03:00
Alexander Krotov
93e038e056 Ignore ephemeral timer changes if replying to old message
This should prevent timer changes back when someone is replying before
receiving timer change.
2020-12-06 21:09:13 +03:00
Alexander Krotov
dea9630380 Better typed Message.ephemeral_timer 2020-12-06 21:09:13 +03:00
Alexander Krotov
30e7f84770 Add get_parent_message() 2020-12-06 21:09:13 +03:00
Alexander Krotov
fc1f44c6d6 Spellcheck 2020-12-06 04:26:20 +03:00
B. Petersen
f774665921 be more graceful when the apostrophed-encoding is used erroneous - either by other MUA or by Delta Chat up to core1.50 2020-12-06 04:24:39 +03:00
B. Petersen
7b291c1416 use correct key when sending filenames
adding an asterisk to the filename key
REQUIRES two apostrope-delimiters in the value as of charset'lang'name,
see rfc2231

as we do use the value this way, we MUST not add the asterisk.

things worked correctly in the past as
consuming MUAs are pretty graceful.

i assume the error comes from the c to rust conversion -
core-c did add the asterisk, however also did the apostrope-encoding.
core-rust leaves the asterisk but changes encoding.
2020-12-06 04:24:39 +03:00
B. Petersen
8fcb8c3788 add failing test for erroneous apostrophed-encoding 2020-12-06 04:24:39 +03:00
bjoern
cdb7f1dd9f Merge pull request #2094 from deltachat/fix-non-utf8-filenames
test and fix non-utf8 filenames
2020-12-04 16:32:03 +01:00
B. Petersen
af045c245d warn about encoding errors; this requires Context 2020-12-04 12:25:45 +01:00
B. Petersen
0bdd1b7dc2 avoid unwrap() when falling back to latin1 charset 2020-12-04 11:42:14 +01:00
B. Petersen
c6f6751c89 mention used RFCs in standards.md 2020-12-04 01:45:00 +01:00
B. Petersen
e77706f7d0 add test apostrophed-encoding with cp1252 charset (aka ANSI aka Windows-1252) 2020-12-04 01:28:24 +01:00
B. Petersen
8de73c4566 handle apostrophed-encoding with non-utf-8 charsets 2020-12-04 01:19:03 +01:00
B. Petersen
56e6c2712b add failing test for apostroped windows-1251 encoding 2020-12-04 00:19:19 +01:00
B. Petersen
b510d74c4a add test for apostrophed-encoded filenames 2020-12-03 17:39:00 +01:00
B. Petersen
966712019f add test for bad encoded-words delimiter 2020-12-03 17:38:55 +01:00
B. Petersen
f919e4962d add test for cyrillic encoded-words filenames 2020-12-03 17:01:32 +01:00
B. Petersen
412645e1ce add test for binary word-encoded filenames 2020-12-03 17:01:32 +01:00
Hocuri
7c9624e822 Don't download ranges of messages (i.e. first:last) (#2061) 2020-12-03 10:19:56 +01:00
Alexander Krotov
e79533ae54 Add diff .gitattributes for .{py,rs,md} 2020-12-03 03:46:51 +03:00
bjoern
d8babe2d0c Merge pull request #2080 from deltachat/fix-filenames
fix decoding of continued filenames
2020-12-02 11:41:52 +01:00
Hocuri
83df69f43c Add verbose logging for #2065 2020-12-01 09:06:56 +01:00
Hocuri
2a9d06d817 Re-enable Export to the new backup format, add backup progress, add a test for the backup progress (#2023)
* Add progress for backup import/export

* Export to the new backup format

* Add tests for the new imex progress
2020-12-01 09:05:25 +01:00
B. Petersen
4ef2a7c8d7 prefer exhaustive 'match' over 'if' 2020-11-29 14:47:54 +03:00
B. Petersen
75d79dc79c use different avatar-sizes for different media_quality settings 2020-11-29 14:47:54 +03:00
B. Petersen
2720d34594 use split off image recoding function for avatars 2020-11-29 14:47:54 +03:00
B. Petersen
7c15e4e948 fix rotation for small images and avatars 2020-11-29 14:47:54 +03:00
B. Petersen
4f836950bc split off image recoding to separate function 2020-11-29 14:47:54 +03:00
B. Petersen
1321a78f87 streamline test 2020-11-24 14:40:28 +01:00
B. Petersen
210d8bad04 allow specification of both, and and also do the apostrope-decoding when specified by a trailing asterix in the key. add tests for that. 2020-11-24 14:40:28 +01:00
B. Petersen
e19d2cccfc prefer 'if let' over 'unwrap()' 2020-11-24 14:40:28 +01:00
B. Petersen
a29dc514d3 decode filenames as CHARSET'LANG'test%2E%70%64%66 2020-11-24 14:40:28 +01:00
B. Petersen
07109e9b17 add passing test for simple-filename and failing test for continued-filename 2020-11-24 14:40:27 +01:00
B. Petersen
889f4673ad make loading eml reusable 2020-11-23 23:08:21 +01:00
bjoern
7fa794cd72 Merge pull request #2088 from deltachat/prep-1.50
prep 1.50
2020-11-19 15:44:41 +01:00
B. Petersen
b21508fdb7 bump version to 1.50 2020-11-19 14:06:53 +01:00
B. Petersen
92175b27ab update changelog for 1.50 2020-11-19 14:06:52 +01:00
bjoern
4d2a39febb Merge pull request #2087 from deltachat/imbox-dont-fetch-old-msgs
Don't fetch messages that arrived in the meantime if InboxWatch is disabled and re-enabled
2020-11-18 14:22:39 +01:00
bjoern
4fa667d834 Merge pull request #2084 from deltachat/alphaversions
let core carry a 1.50.0-alpha.0 version
2020-11-18 12:27:32 +01:00
Hocuri
38ed94367c Update src/config.rs
Co-authored-by: bjoern <r10s@b44t.com>
2020-11-18 12:08:28 +01:00
Hocuri
3a7bd8b49d Don't fetch messages that arrived in the meantime if InboxWatch is
disabled and re-enabled

That's another narrow-stitching patch for a scenario where many emails could
be deleted all at once and surprisingly: user disables inbox-watch, enables delete-from-server, after a moth enables inbox-watch again -> currently all emails that arrived in the meantime will be deleted (if emails are not fetched, they won't be deleted)
2020-11-17 14:13:27 +01:00
Hocuri
c8d4eee794 Don't fetch from INBOX if it is disabled
Before, if there were more than 20 jobs at once, we unconditionally
fetched from inbox
2020-11-16 17:03:52 +01:00
holger krekel
d66174e55a let core carry a 1.50.0-alpha.0 version (which sits between 1.49.X and 1.50.0).
This way we can better distinguish tagged from untagged core releases, also in logs etc.
We might, from time to time, increase the alpha.N "N" number if we are entering testing etc.
2020-11-16 16:12:55 +01:00
holger krekel
8d2a5cd242 fix link 2020-11-16 11:47:48 +01:00
holger krekel
2fbef80df8 detailing/rewriting the group-sync draft a little. 2020-11-16 11:47:48 +01:00
link2xt
07768133d5 Merge pull request #2083 from deltachat/smtp-plain
smtp: do not use STARTTLS when PLAIN connection is requested
2020-11-15 23:37:41 +03:00
Alexander Krotov
9df88745dc smtp: do not use STARTTLS when PLAIN connection is requested
Also do not allow downgrade if STARTTLS is not available
2020-11-15 22:40:41 +03:00
B. Petersen
bd856d90db remove unused AccountConfig::name
the field was never set or read.

to get the name for an account,
use `dc_get_config(account, "displayname")`.
2020-11-10 03:52:09 +03:00
bjoern
1f4403d149 Merge pull request #2071 from deltachat/prep-1.49
prep 1.49
2020-11-09 15:40:41 +01:00
B. Petersen
6345e57720 bump version to 1.49 2020-11-09 14:11:01 +01:00
B. Petersen
332f32e799 update changelog for 1.49 2020-11-09 14:10:07 +01:00
Alexander Krotov
deb506cb52 Add timestamps to images and videos
It is already done for voice messages and makes saving attachments to
one folder easier.
2020-11-08 22:16:42 +03:00
Alexander Krotov
66907c17d3 mimeparser: preserve quotes in messages with attachments 2020-11-08 12:01:35 +03:00
Alexander Krotov
bf82dd9c60 Document account_id parameter for dc_accounts_remove_account 2020-11-08 01:12:57 +03:00
Alexander Krotov
46c544a5ca deltachat-ffi: forbid quoting messages from another context 2020-11-05 05:57:19 +03:00
Alexander Krotov
c53c6cdf90 deltachat.h documentation fixes 2020-11-01 01:33:15 +03:00
bjoern
c6c2fb562e Merge pull request #2043 from deltachat/prep-1.48
prepare 1.48
2020-10-31 23:12:23 +01:00
B. Petersen
d30bedda96 update changelog to most recent changes 2020-10-31 22:16:33 +01:00
B. Petersen
f7a4f5debf correct/enhance 1.47 changelog 2020-10-31 15:52:59 +01:00
B. Petersen
ea759f17d0 bump version to 1.48 2020-10-31 15:52:59 +01:00
B. Petersen
236faafe0f update changelog for 1.48 2020-10-31 15:52:59 +01:00
bjoern
769f9af861 Merge pull request #2056 from deltachat/smtp-multi-send
send messages via SMTP in configurable chunks
2020-10-31 15:44:32 +01:00
B. Petersen
bf83f6d4ad read settings from provider-db-globals, not from -config-defaults 2020-10-31 14:20:16 +01:00
B. Petersen
8232a148aa send messages via SMTP in configurable chunks
providers may have a maximum for the SMTP RCPT TO: header,
therefore, an outgoing message is split into several chunks.
the chunk size defaults to 50, however, if known to be larger or smaller
by a dedicated provider, this setting may be changed.

this does not affect the MIME To: headers,
if a provider has a maximum here, this is change would not help.
2020-10-31 14:20:16 +01:00
bjoern
04a4424664 Merge pull request #2059 from deltachat/outdated-test
fix outdated test
2020-10-31 14:18:34 +01:00
B. Petersen
da729a8345 fix outdated test
the test just did not work on the last day of a month.
2020-10-31 14:06:17 +01:00
bjoern
25513a6e42 Merge pull request #2050 from deltachat/restore-saved-messages-hint
add hint about how to restore saved-messages chat
2020-10-31 11:59:04 +01:00
bjoern
9063725729 Merge pull request #2057 from deltachat/fix-mistakenly-unarchive
fix mistakenly unarchive
2020-10-31 11:52:34 +01:00
B. Petersen
46833ca4f2 do not unarchive one-to-one on receiving read-receipts 2020-10-30 19:03:02 +01:00
B. Petersen
dbdea787a7 fix typo that results in a not-working test 2020-10-30 19:02:10 +01:00
Alexander Krotov
5c1bbc5d6a Cancel ephemeral task in Context.stop_io()
ephemeral_task holds a reference to Context, preventing event emitter
from returning NULL and terminating event loop. Prior to this change,
there was no way to quickly terminate pending ephemeral_task.
2020-10-28 01:10:23 +03:00
Alexander Krotov
f30c319fbf Remove trailing space in Python code 2020-10-27 23:27:00 +03:00
B. Petersen
b2b59852a7 adapt test for updating device chats 2020-10-27 14:30:23 +01:00
B. Petersen
4c4a9b52de show hint how to restore saved-messages
the saved-messages can be deleted as any other chat;
if the feature is unneeded, this probably also makes some sense.

however, if saved-messages was deleted accidentally (yeah, i've seen that :),
it is not intuitive to see how the saved-messages feature can be restored.

this change just drops a note about how-to-restore to the device chat.
2020-10-27 14:26:23 +01:00
Hocuri
c8242b12fe Add docs for clone_online_account 2020-10-27 08:00:26 +01:00
holger krekel
622d99a971 remove option<path> from inner/imex handling to simplify the code 2020-10-26 20:34:52 +01:00
holger krekel
45ea41262c fix export/import self-key roundtrip 2020-10-26 20:34:52 +01:00
holger krekel
ed5167babc fix export of self private keys 2020-10-26 20:34:52 +01:00
Alexander Krotov
11d9fcad35 Display a quote if top posting is detected
Previously quote at the end was always displayed as [...].
2020-10-26 18:03:30 +03:00
bjoern
fa7b6c001e Merge pull request #2046 from deltachat/fix2026
remove reference to star-commands.
2020-10-25 19:32:40 +01:00
holger krekel
42bd1bc806 remove reference to star-commands. 2020-10-25 18:53:55 +01:00
Hocuri
31bf34890a Always show the cause for which a msg landed in trash 2020-10-25 17:42:26 +01:00
Hocuri
34af492afb Rename fetch_existing to fetch_existing_msgs, add comment (#2042) 2020-10-24 12:10:36 +02:00
holger krekel
ef245b5759 reduce code a bit and shortcut contact lookup to make it easier to remove an existing contact whether blocked or not. 2020-10-23 21:10:12 +02:00
Hocuri
41b2dee4ca Use get_contact() instead of create_contact() 2020-10-23 21:10:12 +02:00
adbenitez
1ce1a01d49 improve docstring 2020-10-23 21:10:12 +02:00
adbenitez
1cd3ee6a05 add failing test 2020-10-23 21:10:12 +02:00
B. Petersen
75df8f762c update provider-db 2020-10-23 17:19:47 +02:00
Hocuri
e5da5c48f1 second review 2020-10-23 17:19:23 +02:00
Hocuri
5b5c6a9c31 Alex' review 2020-10-23 17:19:23 +02:00
Hocuri
4ae1a17cc0 Add test for "Saved-messages do not pop up in original chat in multi-device anymore" 2020-10-23 17:19:23 +02:00
bjoern
0781316c97 Merge pull request #2040 from deltachat/workaround-spawned-tasks
workaround executer-blocking-handling bug
2020-10-23 16:20:19 +02:00
B. Petersen
8eb73a5ade workaround executer-blocking-handling-bug in async-std 2020-10-23 16:04:36 +02:00
Hocuri
e6b7a7e292 saved-messages do not pop up in original chat in multi-device anymore
fix #2020

When forwarding a message, the original `in_reply_to` stays in place.
I did not find any recent commit that changed anything about this,
but IIRC there was a bug that prevented setting the `in_reply_to` at
all.

So, for chat messages, do not look at the InReplyTo header (and also not
at the References header)
2020-10-23 12:33:25 +03:00
Hocuri
c438691b73 Disable fetch-existing for now 2020-10-23 08:31:49 +02:00
adbenitez
1cacfb30ff avoid usage of deprecated contact.set_blocked() 2020-10-23 08:25:46 +02:00
Simon Laux
39a00929c7 fix function reference in changelog 2020-10-21 18:29:00 +03:00
Alexander Krotov
8156692e5a configure: always try at least one IMAP and SMTP server
Previously empty autoconfig resulted in no servers being tried and no
error displayed.
2020-10-21 02:30:16 +03:00
holger krekel
5a9a4dbbab introduce Account.get_blocked_contacts
also introduce Contact.block() and Contact.unblock() methods
and deprecate the c-ish "Contact.set_blocked()" api.
fixes #2011
2020-10-20 11:56:02 +02:00
bjoern
df56b76182 Update CHANGELOG.md
Co-authored-by: Hocuri <hocuri@gmx.de>
2020-10-19 18:44:39 +02:00
B. Petersen
0fc1134bab bump version to 1.47 2020-10-19 18:44:39 +02:00
B. Petersen
dfecd033a7 update changelog for 1.47 2020-10-19 18:44:39 +02:00
Hocuri
6c5eaaed2c Don't peek-receipients/fetch-existing if this is a bot 2020-10-19 18:10:54 +02:00
Hocuri
dcc00075b0 extract variable 2020-10-19 15:23:18 +02:00
Hocuri
a320fb9d6c Doesn't look great, but this was the only way to make compiler happy 2020-10-19 15:23:18 +02:00
Hocuri
3bef4909d5 Update src/message.rs
Co-authored-by: holger krekel  <holger@merlinux.eu>
2020-10-19 15:23:18 +02:00
Hocuri
26aeacc6be @link2xt's review 2020-10-19 15:23:18 +02:00
Hocuri
0b1288fc17 Mark all failed messages as failed when receiving an NDN
There may be multiple messages with the same `Message-Id`-header alias
rfc724_mid because an email with multiple attachments was split up.
2020-10-19 15:23:18 +02:00
Hocuri
c005f756d6 Do not allow to save drafts in non-writeable chats, fix #1986 (#1997) 2020-10-19 13:15:48 +02:00
Hocuri
3c6d52842e Doc comments are show in HTML documentation.
This is not a proper documentation, just a note on implementation.
2020-10-19 13:07:55 +02:00
Hocuri
4d2542cee5 Don't show HTML if there is no content and there is a file attached
Fix https://github.com/deltachat/deltachat-core-rust/issues/1982
2020-10-19 13:07:55 +02:00
B. Petersen
9bf8799484 fix processing of protection-changed messages
by #2001, processing of protection-enabled and protection-disabled
messages is only done if verification-checks pass.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Update deltachat-ffi/deltachat.h

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

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

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

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

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

updates

- pgp
- async-std
- surf
- mailparse

* simplify dev deps

* more deps

* fixup

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

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

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

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

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

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

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

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

Configuration failure is still indicated by DC_EVENT_CONFIGURE_PROGRESS
with data1 set to 0. Python tests from TestOnlineConfigurationFails
group are changed to only wait for this event.
2020-09-05 21:17:26 +03:00
Alexander Krotov
9b741825ef Attempt IMAP and SMTP configuration in parallel
SMTP configurations are tested in a separate async task.
2020-09-05 21:09:47 +03:00
Alexander Krotov
f4e0c6b5f1 Remove Peerstate::new()
Create and return immutable Peerstate instead.
2020-09-05 21:07:58 +03:00
Alexander Krotov
a68528479f Remove dead code markers 2020-09-05 21:07:28 +03:00
Alexander Krotov
383c5ba7fd Smtp.send: join recipients list without .collect 2020-09-05 21:06:32 +03:00
Floris Bruynooghe
ee27c7d9d4 Run clippy on tests and examples 2020-09-05 18:13:16 +02:00
172 changed files with 28139 additions and 12709 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,64 +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.5.2
- 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:
requires:
- remote_tests_python
filters:
branches:
only: master
tags:
only: /.*/
- upload_docs_wheels:
requires:
@@ -204,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

3
.gitattributes vendored
View File

@@ -12,3 +12,6 @@ test-data/* text=false
*.gif binary
*.ico binary
*.py diff=python
*.rs diff=rust
*.md diff=markdown

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.45.0
toolchain: 1.50.0
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
@@ -29,17 +29,39 @@ jobs:
run_clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: 1.45.0
toolchain: 1.50.0
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all
args: --workspace --tests --examples
docs:
name: Rust doc comments
runs-on: ubuntu-latest
env:
RUSTDOCFLAGS: -Dwarnings
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Install rust stable toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
components: rust-docs
override: true
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v1
- name: Rustdoc
uses: actions-rs/cargo@v1
with:
command: doc
args: --document-private-items --no-deps
build_and_test:
name: Build and test
@@ -47,8 +69,10 @@ jobs:
continue-on-error: ${{ matrix.experimental }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
rust: [1.45.0]
# macOS disabled due to random failures related to caching
#os: [ubuntu-latest, windows-latest, macOS-latest]
os: [ubuntu-latest, windows-latest]
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,347 @@
# 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
- fix: do not fetch from INBOX if inbox_watch is disabled #2085
- fix: do not use STARTTLS when PLAIN connection is requested
and do not allow downgrade if STARTTLS is not available #2071
## 1.49.0
- add timestamps to image and video filenames #2068
- forbid quoting messages from another context #2069
- fix: preserve quotes in messages with attachments #2070
## 1.48.0
- `fetch_existing` renamed to `fetch_existing_msgs` and disabled by default
#2035 #2042
- skip fetch existing messages/contacts if config-option `bot` set #2017
- always log why a message is sorted to trash #2045
- display a quote if top posting is detected #2047
- add ephemeral task cancellation to `dc_stop_io()`;
before, there was no way to quickly terminate pending ephemeral tasks #2051
- when saved-messages chat is deleted,
a device-message about recreation is added #2050
- use `max_smtp_rcpt_to` from provider-db,
sending messages to many recipients in configurable chunks #2056
- fix handling of empty autoconfigure files #2027
- fix adding saved messages to wrong chats on multi-device #2034 #2039
- fix hang on android4.4 and other systems
by adding a workaround to executer-blocking-handling bug #2040
- fix secret key export/import roundtrip #2048
- fix mistakenly unarchived chats #2057
- fix outdated-reminder test that fails only 7 days a year,
including halloween :) #2059
- improve python bindings #2021 #2036 #2038
- update provider-database #2037
## 1.47.0
- breaking change: `dc_update_device_chats()` removed;
this is now done automatically during configure
unless the new config-option `bot` is set #1957
- breaking change: split `DC_EVENT_MSGS_NOTICED` off `DC_EVENT_MSGS_CHANGED`
and remove `dc_marknoticed_all_chats()` #1942 #1981
- breaking change: remove unused starring options #1965
- breaking change: `DC_CHAT_TYPE_VERIFIED_GROUP` replaced by
`dc_chat_is_protected()`; also single-chats may be protected now, this may
happen over the wire even if the UI do not offer an option for that #1968
- breaking change: split quotes off message text,
UIs should use at least `dc_msg_get_quoted_text()` to show quotes now #1975
- new api for quote handling: `dc_msg_set_quote()`, `dc_msg_get_quoted_text()`,
`dc_msg_get_quoted_msg()` #1975 #1984 #1985 #1987 #1989 #2004
- require quorum to enable encryption #1946
- speed up and clean up account creation #1912 #1927 #1960 #1961
- configure now collects recent contacts and fetches last messages
unless disabled by `fetch_existing` config-option #1913 #2003
EDIT: `fetch_existing` renamed to `fetch_existing_msgs` in 1.48.0 #2042
- emit `DC_EVENT_CHAT_MODIFIED` on contact rename
and set contact-id on `DC_EVENT_CONTACTS_CHANGED` #1935 #1936 #1937
- add `dc_set_chat_protection()`; the `protect` parameter in
`dc_create_group_chat()` will be removed in an upcoming release;
up to then, UIs using the "verified group" paradigm
should not use `dc_set_chat_protection()` #1968 #2014 #2001 #2012 #2007
- remove unneeded `DC_STR_COUNT` #1991
- mark all failed messages as failed when receiving an NDN #1993
- check some easy cases for bad system clock and outdated app #1901
- fix import temporary directory usage #1929
- fix forcing encryption for reset peers #1998
- fix: do not allow to save drafts in non-writeable chats #1997
- fix: do not show HTML if there is no content and there is an attachment #1988
- fix recovering offline/lost connections, fixes background receive bug #1983
- fix ordering of accounts returned by `dc_accounts_get_all()` #1909
- fix whitespace for summaries #1938
- fix: improve sentbox name guessing #1941
- fix: avoid manual poll impl for accounts events #1944
- fix encoding newlines in param as a preparation for storing quotes #1945
- fix: internal and ffi error handling #1967 #1966 #1959 #1911 #1916 #1917 #1915
- fix ci #1928 #1931 #1932 #1933 #1934 #1943
- update provider-database #1940 #2005 #2006
- update dependencies #1919 #1908 #1950 #1963 #1996 #2010 #2013
## 1.46.0
- breaking change: `dc_configure()` report errors in
`DC_EVENT_CONFIGURE_PROGRESS`: capturing error events is no longer working
#1886 #1905
- breaking change: removed `DC_LP_{IMAP|SMTP}_SOCKET*` from `server_flags`;
added `mail_security` and `send_security` using `DC_SOCKET` enum #1835
- parse multiple servers in Mozilla autoconfig #1860
- try multiple servers for each protocol #1871
- do IMAP and SMTP configuration in parallel #1891
- configuration cleanup and speedup #1858 #1875 #1889 #1904 #1906
- secure-join cleanup, testing, fixing #1876 #1877 #1887 #1888 #1896 #1899 #1900
- do not reset peerstate on encrypted messages,
ignore reordered autocrypt headers #1885 #1890
- always sort message replies after parent message #1852
- add an index to significantly speed up `get_fresh_msg_cnt()` #1881
- improve mimetype guessing for PDF and many other formats #1857 #1861
- improve accepting invalid html #1851
- improve tests, cleanup and ci #1850 #1856 #1859 #1861 #1884 #1894 #1895
- tweak HELO command #1908
- make `dc_accounts_get_all()` return accounts sorted #1909
- fix KML coordinates precision used for location streaming #1872
- fix cancelling import/export #1855
## 1.45.0
- add `dc_accounts_t` account manager object and related api functions #1784
@@ -704,4 +1046,3 @@
For a full list of changes, please see our closed Pull Requests:
https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed

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)

2295
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,87 +1,91 @@
[package]
name = "deltachat"
version = "1.45.0"
version = "1.53.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
[profile.dev]
debug = 0
[profile.release]
lto = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
libc = "0.2.51"
pgp = { version = "0.6.0", default-features = false }
hex = "0.4.0"
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 = "0.3"
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.6.1", features = ["unstable"] }
base64 = "0.12"
charset = "0.1"
percent-encoding = "2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4.6"
indexmap = "1.3.0"
kamadak-exif = "0.5"
lazy_static = "1.4.0"
regex = "1.1.6"
rusqlite = { version = "0.23", features = ["bundled"] }
r2d2_sqlite = "0.16.0"
r2d2 = "0.8.5"
strum = "0.18.0"
strum_macros = "0.18.0"
backtrace = "0.3.33"
byteorder = "1.3.1"
itertools = "0.9.0"
quick-xml = "0.18.1"
escaper = "0.1.0"
bitflags = "1.1.0"
sanitize-filename = "0.2.1"
stop-token = { version = "0.1.1", features = ["unstable"] }
mailparse = "0.12.1"
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"
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"] }
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 }
dirs = { version = "3.0.1", optional=true }
toml = "0.5.6"
[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.0", features = ["unstable", "attributes"] }
smol = "0.1.10"
tempfile = "3.0"
[workspace]
members = [
"deltachat-ffi",
"deltachat_derive",
]
[[example]]
@@ -95,10 +99,21 @@ path = "examples/repl/main.rs"
required-features = ["repl"]
[[bench]]
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:
@@ -123,7 +120,7 @@ Language bindings are available for:
- [C](https://c.delta.chat)
- [Node.js](https://www.npmjs.com/package/deltachat-node)
- [Python](https://py.delta.chat)
- [Go](https://github.com/hugot/go-deltachat/)
- [Go](https://github.com/deltachat/go-deltachat/)
- [Free Pascal](https://github.com/deltachat/deltachat-fp/)
- **Java** and **Swift** (contained in the Android/iOS repos)

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);

26
benches/create_account.rs Normal file
View File

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

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.45.0"
version = "1.53.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"

View File

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

View File

@@ -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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

@@ -5,69 +5,95 @@ Problem: missing eventual group consistency
If group members are concurrently adding new members,
the new members will miss each other's additions, example:
- Alice and Bob are in a two-member group
1. Alice and Bob are in a two-member group
- Alice adds Carol, concurrently Bob adds Doris
2. Then Alice adds Carol, while concurrently Bob adds Doris
- Carol will see a three-member group (Alice, Bob, Carol),
Doris will see a different three-member group (Alice, Bob, Doris),
and only Alice and Bob will have all four members.
Right now, the group has inconsistent memberships:
Note that for verified groups any mitigation mechanism likely
needs to make all clients to know who originally added a member.
- Alice and Carol see a (Alice, Carol, Bob) group
- Bob and Doris see a (Bob, Doris, Alice)
This then leads to "sender is unknown" messages in the chat,
for example when Alice receives a message from Doris,
or when Bob receives a message from Carol.
There are also other sources for group membership inconsistency:
- leaving/deleting/adding in larger groups, while being offline,
increases chances for inconsistent group membership
- dropped group-membership messages
- group-membership messages landing in "Spam"
solution: memorize+attach (possible encrypted) chat-meta mime messages
----------------------------------------------------------------------
Note that all these problems (can) also happen with verified groups,
then raising "false alarms" which could lure people to ignore such issues.
For reference, please see https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#add-and-remove-members how MemberAdded/Removed messages are shaped.
IOW, it's clear we need to do something about it to improve overall
reliability in group-settings.
- All Chat-Group-Member-Added/Removed messages are recorded in their
full raw (signed and encrypted) mime-format in the DB
- If an incoming member-add/member-delete messages has a member list
which is, apart from the added/removed member, not consistent
with our own view, broadcast a "Chat-Group-Member-Correction" message to
all members, attaching the original added/removed mime-message for all mismatching
contacts. If we have no relevant add/del information, don't send a
correction message out.
Solution: replay group modification messages on inconsistencies
------------------------------------------------------------------
- Upong receiving added/removed attachments we don't do the
check_consistency+correction message dance.
This avoids recursion problems and hard-to-reason-about chatter.
For brevity let's abbreviate "group membership modification" as **GMM**.
Notes:
Delta chat has explicit GMM messages, typically encrypted to the group members
as seen by the device that sends the GMM. The `Spec <https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#add-and-remove-members>`_ details the Mime headers and format.
- mechanism works for both encrypted and unencrypted add/del messages
If we detect membership inconsistencies we can resend relevant GMM messages
to the respective chat. The receiving devices can process those GMM messages
as if it would be an incoming message. If for example they have already seen
the Message-ID of the GMM message, they will ignore the message. It's
probably useful to record GMM message in their original MIME-format and
not invent a new recording format. Few notes on three aspects:
- we already have a "mime_headers" column in the DB for each incoming message.
We could extend it to also include the payload and store mime unconditionally
for member-added/removed messages.
- **group-membership-tracking**: All valid GMM messages are persisted in
their full raw (signed/encrypted?) MIME-format in the DB. Note that GMM messages
already are in the msgs table, and there is a mime_header column which we could
extend to contain the raw Mime GMM message.
- multiple member-added/removed messages can be attached in a single
correction message
- **consistency_checking**: If an incoming GMM has a member list which is
not consistent with our own view, broadcast a "Group-Member-Correction"
message to all members containing a multipart list of GMMs.
- it is minimal on the number of overall messages to reach group consistency
(best-case: no extra messages, the ABCD case above: max two extra messages)
- **correcting_memberships**: Upon receiving a Group-Member-Correction
message we pass the contained GMMs to the "incoming mail pipeline"
(without **consistency_checking** them, to avoid recursion issues)
- somewhat backward compatible: older clients will probably ignore
messages which are signed by someone who is not the outer From-address.
- the correction-protocol also helps with dropped messages. If a member
did not see a member-added/removed message, the next member add/removed
message in the group will likely heal group consistency for this member.
Alice/Carol and Bob/Doris getting on the same page
++++++++++++++++++++++++++++++++++++++++++++++++++
Recall that Alice/Carol and Bob/Doris had a differening view of
group membership. With the proposed solution, when Bob receives
Alice's "Carol added" message, he will notice that Alice (and thus
also carol) did not know about Doris. Bob's device sends a
"Chat-Group-Member-Correction" message containing his own GMM
when adding Doris. Therefore, the group's membership is healed
for everyone in a single broadcast message.
Alice might also send a Group-member-Correction message,
so there is a second chance that the group gets to know all GMMs.
Note, for example, that if for some reason Bobs and Carols provider
drop GMM messages between them (spam) that Alice and Doris can heal
it by resending GMM messages whenever they detect them to be out of sync.
- we can quite easily extend the mechanism to also provide the group-avatar or
other meta-information.
Discussions of variants
++++++++++++++++++++++++
- instead of acting on MemberAdded/Removed message we could send
corrections for any received message that addresses inconsistent group members but
a) this would delay group-membership healing
- instead of acting on GMM messages we could send corrections
for any received message that addresses inconsistent group members but
a) this could delay group-membership healing
b) could lead to a lot of members sending corrections
c) means we might rely on "To-Addresses" which we also like to strike
at least for protected chats.
- instead of broadcasting correction messages we could only send it to
the sender of the inconsistent member-added/removed message.
@@ -83,44 +109,3 @@ Discussions of variants
while both being in an offline or bad-connection situation).
solution2: repeat member-added/removed messages
---------------------------------------------------
Introduce a new Chat-Group-Member-Changed header and deprecate Chat-Group-Member-Added/Removed
but keep sending out the old headers until the new protocol is sufficiently deployed.
The new Chat-Group-Member-Changed header contains a Time-to-Live number (TTL)
which controls repetition of the signed "add/del e-mail address" payload.
Example::
Chat-Group-Member-Changed: TTL add "somedisplayname" someone@example.org
owEBYQGe/pANAwACAY47A6J5t3LWAcsxYgBeTQypYWRkICJzb21lZGlzcGxheW5h
bWUiIHNvbWVvbmVAZXhhbXBsZS5vcmcgCokBHAQAAQIABgUCXk0MqQAKCRCOOwOi
ebdy1hfRB/wJ74tgFQulicthcv9n+ZsqzwOtBKMEVIHqJCzzDB/Hg/2z8ogYoZNR
iUKKrv3Y1XuFvdKyOC+wC/unXAWKFHYzY6Tv6qDp6r+amt+ad+8Z02q53h9E55IP
FUBdq2rbS8hLGjQB+mVRowYrUACrOqGgNbXMZjQfuV7fSc7y813OsCQgi3tjstup
b+uduVzxCp3PChGhcZPs3iOGCnQvSB8VAaLGMWE2d7nTo/yMQ0Jx69x5qwfXogTk
mTt5rOJyrosbtf09TMKFzGgtqBcEqHLp3+mQpZQ+WHUKAbsRa8Jc9DOUOSKJ8SNM
clKdskprY+4LY0EBwLD3SQ7dPkTITCRD
=P6GG
TTL is set to "2" on an initial Chat-Group-Member-Changed add/del message.
Receivers will apply the add/del change to the group-membership,
decrease the TTL by 1, and if TTL>0 re-sent the header.
The "add|del e-mail address" payload is pgp-signed and repeated verbatim.
This allows to propagate, in a cryptographically secured way,
who added a member. This is particularly important for allowing
to show in verified groups who added a member (planned).
Disadvantage to solution 1:
- requires to specify encoding and precise rules for what/how is signed.
- causes O(N^2) extra messages
- Not easily extendable for other things (without introducing a new
header / encoding)

View File

@@ -2,25 +2,29 @@ extern crate dirs;
use std::str::FromStr;
use anyhow::{bail, ensure};
use anyhow::{bail, ensure, Error};
use async_std::path::Path;
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility};
use deltachat::chat::{
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
};
use deltachat::chatlist::*;
use deltachat::constants::*;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::dc_receive_imf::*;
use deltachat::dc_tools::*;
use deltachat::error::Error;
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() {
@@ -193,8 +199,8 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
&contact_name,
contact_id,
msgtext.unwrap_or_default(),
if msg.is_starred() { "" } else { "" },
if msg.get_from_id() == 1 as libc::c_uint {
if msg.has_html() { "[HAS-HTML]" } else { "" },
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,9 +358,8 @@ 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\
createverified <name>\n\
createprotected <name>\n\
addmember <contact-id>\n\
removemember <contact-id>\n\
groupname <name>\n\
@@ -369,9 +370,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
dellocations\n\
getlocations [<contact-id>]\n\
send <text>\n\
send-garbage\n\
sendimage <file> [<text>]\n\
sendfile <file> [<text>]\n\
sendhtml <file for html-part> [<text for plain-part>]\n\
videochat\n\
draft [<text>]\n\
devicemsg <text>\n\
@@ -380,15 +381,22 @@ 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\
html <msg-id>\n\
listfresh\n\
forward <msg-id> <chat-id>\n\
markseen <msg-id>\n\
star <msg-id>\n\
unstar <msg-id>\n\
delmsg <msg-id>\n\
===========================Contact commands==\n\
listcontacts [<query>]\n\
@@ -397,15 +405,18 @@ 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\
checkqr <qr-content>\n\
setqr <qr-content>\n\
providerinfo <addr>\n\
event <event-id to test>\n\
fileinfo <file>\n\
estimatedeletion <seconds>\n\
emptyserver <flags> (1=MVBOX 2=INBOX)\n\
clear -- clear screen\n\
exit or quit\n\
============================================="
@@ -445,20 +456,20 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"export-backup" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(&context, ImexMode::ExportBackup, Some(&dir)).await?;
imex(&context, ImexMode::ExportBackup, &dir).await?;
println!("Exported to {}.", dir.to_string_lossy());
}
"import-backup" => {
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
imex(&context, ImexMode::ImportBackup, Some(arg1)).await?;
imex(&context, ImexMode::ImportBackup, arg1).await?;
}
"export-keys" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(&context, ImexMode::ExportSelfKeys, Some(&dir)).await?;
imex(&context, ImexMode::ExportSelfKeys, &dir).await?;
println!("Exported to {}.", dir.to_string_lossy());
}
"import-keys" => {
imex(&context, ImexMode::ImportSelfKeys, Some(arg1)).await?;
imex(&context, ImexMode::ImportSelfKeys, arg1).await?;
}
"export-setup" => {
let setup_code = create_setup_code(&context);
@@ -502,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 };
@@ -514,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 {
@@ -527,16 +536,18 @@ 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 => "📦",
ChatVisibility::Pinned => "📌",
},
if chat.is_protected() { "🛡️" } else { "" },
);
let lot = chatlist.get_summary(&context, i, Some(&chat)).await;
let statestr = if chat.visibility == ChatVisibility::Archived {
@@ -571,7 +582,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
);
}
}
if location::is_sending_locations_to_chat(&context, ChatId::new(0)).await {
if location::is_sending_locations_to_chat(&context, None).await {
println!("Location streaming enabled.");
}
println!("{} chats", cnt);
@@ -591,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 {
@@ -601,33 +615,41 @@ 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(),
},
_ => "".to_string(),
},
if sel_chat.is_protected() {
"🛡️"
} else {
""
},
);
log_msglist(&context, &msglist).await?;
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
@@ -636,47 +658,77 @@ 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.");
let chat_id =
chat::create_group_chat(&context, VerifiedStatus::Unverified, arg1).await?;
chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?;
println!("Group#{} created successfully.", chat_id);
}
"createverified" => {
"createprotected" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id = chat::create_group_chat(&context, VerifiedStatus::Verified, arg1).await?;
let chat_id =
chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?;
println!("VerifiedGroup#{} created successfully.", chat_id);
println!("Group#{} created and protected successfully.", chat_id);
}
"addmember" => {
ensure!(sel_chat.is_some(), "No chat selected");
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
{
@@ -688,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?;
@@ -718,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;
@@ -727,7 +779,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
contacts.len(),
location::is_sending_locations_to_chat(
&context,
sel_chat.as_ref().unwrap().get_id()
Some(sel_chat.as_ref().unwrap().get_id())
)
.await,
);
@@ -735,15 +787,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"getlocations" => {
ensure!(sel_chat.is_some(), "No chat selected.");
let contact_id = arg1.parse().unwrap_or_default();
let contact_id: Option<u32> = arg1.parse().ok();
let locations = location::get_range(
&context,
sel_chat.as_ref().unwrap().get_id(),
Some(sel_chat.as_ref().unwrap().get_id()),
contact_id,
0,
0,
)
.await;
.await?;
let default_marker = "-".to_string();
for location in &locations {
let marker = location.marker.as_ref().unwrap_or(&default_marker);
@@ -826,6 +878,22 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"sendhtml" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "No html-file given.");
let path: &Path = arg1.as_ref();
let html = &*fs::read(&path)?;
let html = String::from_utf8_lossy(html);
let mut msg = Message::new(Viewtype::Text);
msg.set_html(Some(html.to_string()));
msg.set_text(Some(if arg2.is_empty() {
path.file_name().unwrap().to_string_lossy().to_string()
} else {
arg2.to_string()
}));
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"videochat" => {
ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
@@ -834,15 +902,18 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(!arg1.is_empty(), "Argument <query> missing.");
let chat = if let Some(ref sel_chat) = sel_chat {
sel_chat.get_id()
Some(sel_chat.get_id())
} else {
ChatId::new(0)
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.");
@@ -855,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
@@ -863,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.");
}
}
@@ -876,9 +947,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
msg.set_text(Some(arg1.to_string()));
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
}
"updatedevicechats" => {
context.update_device_chats().await?;
}
"listmedia" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -889,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 {
@@ -910,7 +978,39 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"archive" => ChatVisibility::Archived,
"unarchive" | "unpin" => ChatVisibility::Normal,
"pin" => ChatVisibility::Pinned,
_ => panic!("Unexpected command (This should never happen)"),
_ => unreachable!("arg0={:?}", arg0),
},
)
.await?;
}
"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()?);
chat_id
.set_protection(
&context,
match arg0 {
"protect" => ProtectionStatus::Protected,
"unprotect" => ProtectionStatus::Unprotected,
_ => unreachable!("arg0={:?}", arg0),
},
)
.await?;
@@ -923,11 +1023,21 @@ 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" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?);
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();
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());
@@ -949,12 +1059,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
msg_ids[0] = MsgId::new(arg1.parse()?);
message::markseen_msgs(&context, msg_ids).await;
}
"star" | "unstar" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut msg_ids = vec![MsgId::new(0); 1];
msg_ids[0] = MsgId::new(arg1.parse()?);
message::star_msgs(&context, msg_ids, arg0 == "star").await;
}
"delmsg" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut ids = [MsgId::new(0); 1];
@@ -965,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),
)
@@ -988,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(),
}
@@ -1025,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;
@@ -1045,7 +1164,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
match provider::get_provider_info(arg1) {
match provider::get_provider_info(arg1).await {
Some(info) => {
println!("Information for provider belonging to {}:", arg1);
println!("status: {}", info.status as u32);
@@ -1069,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" => {
@@ -1092,11 +1211,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
seconds, device_cnt, server_cnt
);
}
"emptyserver" => {
ensure!(!arg1.is_empty(), "Argument <flags> missing");
message::dc_empty_server(&context, arg1.parse()?).await;
}
"" => (),
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
}

View File

@@ -80,11 +80,21 @@ fn receive_event(event: EventType) {
yellow.paint(format!("Received LOCATION_CHANGED(contact={:?})", contact))
);
}
EventType::ConfigureProgress(progress) => {
info!(
"{}",
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
);
EventType::ConfigureProgress { progress, comment } => {
if let Some(comment) = comment {
info!(
"{}",
yellow.paint(format!(
"Received CONFIGURE_PROGRESS({} ‰, {})",
progress, comment
))
);
} else {
info!(
"{}",
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
);
}
}
EventType::ImexProgress(progress) => {
info!(
@@ -158,12 +168,14 @@ const DB_COMMANDS: [&str; 9] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 27] = [
const CHAT_COMMANDS: [&str; 34] = [
"listchats",
"listarchived",
"chat",
"createchat",
"createchatbymsg",
"decidestartchat",
"decideblock",
"decidenotnow",
"creategroup",
"createverified",
"addmember",
@@ -178,6 +190,7 @@ const CHAT_COMMANDS: [&str; 27] = [
"send",
"sendimage",
"sendfile",
"sendhtml",
"videochat",
"draft",
"listmedia",
@@ -185,25 +198,30 @@ const CHAT_COMMANDS: [&str; 27] = [
"unarchive",
"pin",
"unpin",
"mute",
"unmute",
"protect",
"unprotect",
"delchat",
];
const MESSAGE_COMMANDS: [&str; 8] = [
const MESSAGE_COMMANDS: [&str; 6] = [
"listmsgs",
"msginfo",
"listfresh",
"forward",
"markseen",
"star",
"unstar",
"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",
@@ -290,7 +308,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
.output_stream(OutputStreamType::Stdout)
.build();
let mut selected_chat = ChatId::default();
let (reader_s, reader_r) = async_std::sync::channel(100);
let (reader_s, reader_r) = async_std::channel::bounded(100);
let input_loop = async_std::task::spawn_blocking(move || {
let h = DcHelper {
completer: FilenameCompleter::new(),
@@ -313,7 +331,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
Ok(line) => {
// TODO: ignore "set mail_pw"
rl.add_history_entry(line.as_str());
async_std::task::block_on(reader_s.send(line));
async_std::task::block_on(reader_s.send(line)).unwrap();
}
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
println!("Exiting...");
@@ -372,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() {
@@ -390,9 +408,8 @@ async fn handle_cmd(
}
"getqr" | "getbadqr" => {
ctx.start_io().await;
if let Some(mut qr) =
dc_get_securejoin_qr(&ctx, ChatId::new(arg1.parse().unwrap_or_default())).await
{
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
if let Some(mut qr) = dc_get_securejoin_qr(&ctx, group).await {
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
qr.replace_range(12..22, "0000000000")
@@ -410,7 +427,7 @@ async fn handle_cmd(
"joinqr" => {
ctx.start_io().await;
if !arg0.is_empty() {
dc_join_securejoin(&ctx, arg1).await;
dc_join_securejoin(&ctx, arg1).await?;
}
}
"exit" | "quit" => return Ok(ExitResult::Exit),

View File

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

View File

@@ -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

@@ -11,10 +11,10 @@ Installing pre-built packages (Linux-only)
========================================================
If you have a Linux system you may try to install the ``deltachat`` binary "wheel" packages
without any "build-from-source" steps. Otherwise you need to `compile the Delta Chat bindings
yourself <sourceinstall>`_.
without any "build-from-source" steps.
Otherwise you need to `compile the Delta Chat bindings yourself <#sourceinstall>`_.
We recommend to first `install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
We recommend to first `install virtualenv <https://virtualenv.pypa.io/en/stable/installation.html>`_,
then create a fresh Python virtual environment and activate it in your shell::
virtualenv venv # or: python -m venv
@@ -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

@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
"""
setup a python binding development in-place install with cargo debug symbols.
@@ -19,8 +19,7 @@ if __name__ == "__main__":
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
if target == 'release':
extra = " -C lto=on -C embed-bitcode=yes"
os.environ["RUSTFLAGS"] = os.environ.get("RUSTFLAGS", "") + extra
os.environ["CARGO_PROFILE_RELEASE_LTO"] = "on"
cmd.append("--release")
print("running:", " ".join(cmd))

View File

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

View File

@@ -145,9 +145,12 @@ def extract_defines(flags):
| DC_STR
| DC_CONTACT_ID
| DC_GCL
| DC_GCM
| DC_SOCKET
| 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.
@@ -179,17 +195,6 @@ class Account(object):
if not self.is_configured():
raise ValueError("need to configure first")
def empty_server_folders(self, inbox=False, mvbox=False):
""" empty server folders. """
flags = 0
if inbox:
flags |= const.DC_EMPTY_INBOX
if mvbox:
flags |= const.DC_EMPTY_MVBOX
if not flags:
raise ValueError("no flags set")
lib.dc_empty_server(self._dc_context, flags)
def get_latest_backupfile(self, backupdir):
""" return the latest backup file in a given directory.
"""
@@ -225,6 +230,19 @@ class Account(object):
:param name: (optional) display name for this contact
:returns: :class:`deltachat.contact.Contact` instance.
"""
(name, addr) = self.get_contact_addr_and_name(obj, name)
name = as_dc_charpointer(name)
addr = as_dc_charpointer(addr)
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
return Contact(self, contact_id)
def get_contact(self, obj):
if isinstance(obj, Contact):
return obj
(_, addr) = self.get_contact_addr_and_name(obj)
return self.get_contact_by_addr(addr)
def get_contact_addr_and_name(self, obj, name=None):
if isinstance(obj, Account):
if not obj.is_configured():
raise ValueError("can only add addresses from configured accounts")
@@ -240,13 +258,7 @@ class Account(object):
if name is None and displayname:
name = displayname
return self._create_contact(addr, name)
def _create_contact(self, addr, name):
addr = as_dc_charpointer(addr)
name = as_dc_charpointer(name)
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
return Contact(self, contact_id)
return (name, addr)
def delete_contact(self, contact):
""" delete a Contact.
@@ -274,6 +286,17 @@ class Account(object):
"""
return Contact(self, contact_id)
def get_blocked_contacts(self):
""" return a list of all blocked contacts.
:returns: list of :class:`deltachat.contact.Contact` objects.
"""
dc_array = ffi.gc(
lib.dc_get_blocked_contacts(self._dc_context),
lib.dc_array_unref
)
return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_contacts(self, query=None, with_self=False, only_verified=False):
""" get a (filtered) list of contacts.
@@ -347,6 +370,9 @@ class Account(object):
def get_deaddrop_chat(self):
return Chat(self, const.DC_CHAT_ID_DEADDROP)
def get_device_chat(self):
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
def get_message_by_id(self, msg_id):
""" return Message instance.
:param msg_id: integer id of this message.
@@ -402,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):
@@ -428,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).
@@ -436,21 +462,22 @@ 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").
If sending out was unsuccessful, a RuntimeError is raised.
"""
self.check_is_configured()
if not self.is_started():
raise RuntimeError("IO not running, can not send out")
res = lib.dc_initiate_key_transfer(self._dc_context)
if res == ffi.NULL:
raise RuntimeError("could not send out autocrypt setup message")
@@ -557,6 +584,9 @@ class Account(object):
You may call `stop_scheduler`, `wait_shutdown` or `shutdown` after the
account is started.
If you are using this from a test, you may want to call
wait_all_initial_fetches() afterwards.
:raises MissingCredentials: if `addr` and `mail_pw` values are not set.
:raises ConfigureFailed: if the account could not be configured.
@@ -566,12 +596,34 @@ class Account(object):
raise ValueError("account not configured, cannot start io")
lib.dc_start_io(self._dc_context)
def configure(self):
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
value for the configuration process.
"""
assert not self.is_configured()
assert self.is_configured() == reconfigure
if not self.get_config("addr") or not self.get_config("mail_pw"):
raise MissingCredentials("addr or mail_pwd not set in config")
configtracker = ConfigureTracker(self)
@@ -579,9 +631,6 @@ class Account(object):
lib.dc_configure(self._dc_context)
return configtracker
def is_started(self):
return self._event_thread.is_alive() and bool(lib.dc_is_io_running(self._dc_context))
def wait_shutdown(self):
""" wait until shutdown of this account has completed. """
self._shutdown_event.wait()
@@ -591,11 +640,8 @@ class Account(object):
self.log("stop_ongoing")
self.stop_ongoing()
if bool(lib.dc_is_io_running(self._dc_context)):
self.log("dc_stop_io (stop core IO scheduler)")
lib.dc_stop_io(self._dc_context)
else:
self.log("stop_scheduler called on non-running context")
self.log("dc_stop_io (stop core IO scheduler)")
lib.dc_stop_io(self._dc_context)
def shutdown(self):
""" shutdown and destroy account (stop callback thread, close and remove

View File

@@ -57,10 +57,7 @@ class Chat(object):
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat.
"""
return lib.dc_chat_get_type(self._dc_chat) in (
const.DC_CHAT_TYPE_GROUP,
const.DC_CHAT_TYPE_VERIFIED_GROUP
)
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
def is_deaddrop(self):
""" return true if this chat is a deaddrop chat.
@@ -85,12 +82,20 @@ class Chat(object):
"""
return not lib.dc_chat_is_unpromoted(self._dc_chat)
def is_verified(self):
""" return True if this chat is a verified group.
def can_send(self):
"""Check if messages can be sent to a give chat.
This is not true eg. for the deaddrop or for the device-talk
:returns: True if chat is verified, False otherwise.
:returns: True if the chat is writable, False otherwise
"""
return lib.dc_chat_is_verified(self._dc_chat)
return lib.dc_chat_can_send(self._dc_chat)
def is_protected(self):
""" return True if this chat is a protected chat.
:returns: True if chat is protected, False otherwise.
"""
return lib.dc_chat_is_protected(self._dc_chat)
def get_name(self):
""" return name of this chat.
@@ -162,6 +167,13 @@ class Chat(object):
"""
return lib.dc_chat_get_type(self._dc_chat)
def get_encryption_info(self):
"""Return encryption info for this chat.
:returns: a string with encryption preferences of all chat members"""
res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id)
return from_dc_charpointer(res)
def get_join_qr(self):
""" get/create Join-Group QR Code as ascii-string.
@@ -242,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.
@@ -366,7 +380,7 @@ class Chat(object):
:raises ValueError: if contact could not be removed
:returns: None
"""
contact = self.account.create_contact(obj)
contact = self.account.get_contact(obj)
ret = lib.dc_remove_contact_from_chat(self.account._dc_context, self.id, contact.id)
if ret != 1:
raise ValueError("could not remove contact {!r} from chat".format(contact))
@@ -490,18 +504,23 @@ class Chat(object):
latitude=lib.dc_array_get_latitude(dc_array, i),
longitude=lib.dc_array_get_longitude(dc_array, i),
accuracy=lib.dc_array_get_accuracy(dc_array, i),
timestamp=datetime.utcfromtimestamp(lib.dc_array_get_timestamp(dc_array, i)))
timestamp=datetime.utcfromtimestamp(
lib.dc_array_get_timestamp(dc_array, i)
),
marker=from_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
)
for i in range(lib.dc_array_get_cnt(dc_array))
]
class Location:
def __init__(self, latitude, longitude, accuracy, timestamp):
def __init__(self, latitude, longitude, accuracy, timestamp, marker):
assert isinstance(timestamp, datetime)
self.latitude = latitude
self.longitude = longitude
self.accuracy = accuracy
self.timestamp = timestamp
self.marker = marker
def __eq__(self, other):
return self.__dict__ == other.__dict__

View File

@@ -52,9 +52,17 @@ class Contact(object):
return lib.dc_contact_is_blocked(self._dc_contact)
def set_blocked(self, block=True):
""" Block or unblock a contact. """
""" [Deprecated, use block/unblock methods] Block or unblock a contact. """
return lib.dc_block_contact(self.account._dc_context, self.id, block)
def block(self):
""" Block this contact. Message will not be seen/retrieved from this contact. """
return lib.dc_block_contact(self.account._dc_context, self.id, True)
def unblock(self):
""" Unblock this contact. Messages from this contact will be retrieved (again)."""
return lib.dc_block_contact(self.account._dc_context, self.id, False)
def is_verified(self):
""" Return True if the contact is verified. """
return lib.dc_contact_is_verified(self._dc_contact)
@@ -69,6 +77,14 @@ class Contact(object):
return None
return from_dc_charpointer(dc_res)
@property
def status(self):
"""Get contact status.
:returns: contact status, empty string if it doesn't exist.
"""
return from_dc_charpointer(lib.dc_contact_get_status(self._dc_contact))
def create_chat(self):
""" create or get an existing 1:1 chat object for the specified contact or contact id.

View File

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

View File

@@ -9,7 +9,9 @@ import ssl
import pathlib
from imapclient import IMAPClient
from imapclient.exceptions import IMAPClientError
import imaplib
import deltachat
from deltachat import const
SEEN = b'\\Seen'
@@ -24,12 +26,30 @@ def dc_account_extra_configure(account):
""" Reset the account (we reuse accounts across tests)
and make 'account.direct_imap' available for direct IMAP ops.
"""
imap = DirectImap(account)
if imap.select_config_folder("mvbox"):
imap.delete(ALL, expunge=True)
assert imap.select_config_folder("inbox")
imap.delete(ALL, expunge=True)
setattr(account, "direct_imap", imap)
try:
if not hasattr(account, "direct_imap"):
imap = DirectImap(account)
for folder in imap.list_folders():
if folder.lower() == "inbox" or folder.lower() == "deltachat":
assert imap.select_folder(folder)
imap.delete(ALL, expunge=True)
else:
imap.conn.delete_folder(folder)
# We just deleted the folder, so we have to make DC forget about it, too
if account.get_config("configured_sentbox_folder") == folder:
account.set_config("configured_sentbox_folder", None)
if account.get_config("configured_spam_folder") == folder:
account.set_config("configured_spam_folder", None)
setattr(account, "direct_imap", imap)
except Exception as e:
# Uncaught exceptions here would lead to a timeout without any note written to the log
# 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
@@ -49,18 +69,31 @@ class DirectImap:
self.connect()
def connect(self):
ssl_context = ssl.create_default_context()
# don't check if certificate hostname doesn't match target hostname
ssl_context.check_hostname = False
# don't check if the certificate is trusted by a certificate authority
ssl_context.verify_mode = ssl.CERT_NONE
host = self.account.get_config("configured_mail_server")
port = int(self.account.get_config("configured_mail_port"))
security = int(self.account.get_config("configured_mail_security"))
user = self.account.get_config("addr")
pw = self.account.get_config("mail_pw")
self.conn = IMAPClient(host, ssl_context=ssl_context)
if security == const.DC_SOCKET_PLAIN:
ssl_context = None
else:
ssl_context = ssl.create_default_context()
# don't check if certificate hostname doesn't match target hostname
ssl_context.check_hostname = False
# don't check if the certificate is trusted by a certificate authority
ssl_context.verify_mode = ssl.CERT_NONE
if security == const.DC_SOCKET_STARTTLS:
self.conn = IMAPClient(host, port, ssl=False)
self.conn.starttls(ssl_context)
elif security == const.DC_SOCKET_PLAIN:
self.conn = IMAPClient(host, port, ssl=False)
elif security == const.DC_SOCKET_SSL:
self.conn = IMAPClient(host, port, ssl_context=ssl_context)
self.conn.login(user, pw)
self.select_folder("INBOX")
@@ -75,6 +108,12 @@ class DirectImap:
except (OSError, IMAPClientError):
print("Could not logout direct_imap conn")
def create_folder(self, foldername):
try:
self.conn.create_folder(foldername)
except imaplib.IMAP4.error as e:
print("Can't create", foldername, "probably it already exists:", str(e))
def select_folder(self, foldername):
assert not self._idling
return self.conn.select_folder(foldername)
@@ -134,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()
@@ -225,3 +249,9 @@ class DirectImap:
res = self.conn.idle_done()
self._idling = False
return res
def append(self, folder, msg):
if msg.startswith("\n"):
msg = msg[1:]
msg = '\n'.join([s.lstrip() for s in msg.splitlines()])
self.conn.append(folder, msg)

View File

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

View File

@@ -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. """
@@ -175,6 +201,27 @@ class Message(object):
if ts:
return datetime.utcfromtimestamp(ts)
@property
def quoted_text(self):
"""Text inside the quote
:returns: Quoted text"""
return from_dc_charpointer(lib.dc_msg_get_quoted_text(self._dc_msg))
@property
def quote(self):
"""Quote getter
:returns: Quoted message, if found in the database"""
msg = lib.dc_msg_get_quoted_msg(self._dc_msg)
if msg:
return Message(self.account, ffi.gc(msg, lib.dc_msg_unref))
@quote.setter
def quote(self, quoted_message):
"""Quote setter"""
lib.dc_msg_set_quote(self._dc_msg, quoted_message._dc_msg)
def get_mime_headers(self):
""" return mime-header object for an incoming message.
@@ -191,6 +238,11 @@ class Message(object):
return email.message_from_bytes(s)
return email.message_from_string(s)
@property
def error(self):
"""Error message"""
return from_dc_charpointer(lib.dc_msg_get_error(self._dc_msg))
@property
def chat(self):
"""chat this message was posted in.
@@ -201,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.
@@ -313,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
@@ -333,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

@@ -312,24 +312,31 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False):
ac1 = self.get_online_configuring_account(
pre_generated_key=pre_generated_key, mvbox=mvbox, move=move)
self.wait_configure_and_start_io()
self.wait_configure_and_start_io([ac1])
return ac1
def get_two_online_accounts(self, move=False, quiet=False):
ac1 = self.get_online_configuring_account(move=move, quiet=quiet)
ac2 = self.get_online_configuring_account(quiet=quiet)
self.wait_configure_and_start_io()
self.wait_configure_and_start_io([ac1, ac2])
return ac1, ac2
def get_many_online_accounts(self, num, move=True):
accounts = [self.get_online_configuring_account(move=move, quiet=True)
for i in range(num)]
self.wait_configure_and_start_io()
self.wait_configure_and_start_io(accounts)
for acc in accounts:
acc.add_account_plugin(FFIEventLogger(acc))
return accounts
def clone_online_account(self, account, pre_generated_key=True):
""" Clones addr, mail_pw, mvbox_watch, mvbox_move, sentbox_watch and the
direct_imap object of an online account. This simulates the user setting
up a new device without importing a backup.
`pre_generated_key` only means that a key from python/tests/data/key is
used in order to speed things up.
"""
self.live_count += 1
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
@@ -342,19 +349,35 @@ def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
mvbox_move=account.get_config("mvbox_move"),
sentbox_watch=account.get_config("sentbox_watch"),
))
if hasattr(account, "direct_imap"):
# Attach the existing direct_imap. If we did not do this, a new one would be created and
# delete existing messages (see dc_account_extra_configure(configure))
ac.direct_imap = account.direct_imap
ac._configtracker = ac.configure()
return ac
def wait_configure_and_start_io(self):
for acc in self._accounts:
if hasattr(acc, "_configtracker"):
acc._configtracker.wait_finish()
del acc._configtracker
acc.set_config("bcc_self", "0")
if acc.is_configured() and not acc.is_started():
acc.start_io()
print("{}: {} account was successfully setup".format(
acc.get_config("displayname"), acc.get_config("addr")))
def wait_configure_and_start_io(self, accounts=None):
if accounts is None:
accounts = self._accounts[:]
started_accounts = []
for acc in accounts:
if acc not in started_accounts:
self.wait_configure(acc)
acc.set_config("bcc_self", "0")
if acc.is_configured():
acc.start_io()
started_accounts.append(acc)
print("{}: {} account was started".format(
acc.get_config("displayname"), acc.get_config("addr")))
for acc in started_accounts:
acc._evtracker.wait_all_initial_fetches()
def wait_configure(self, acc):
if hasattr(acc, "_configtracker"):
acc._configtracker.wait_finish()
acc._evtracker.consume_events()
acc.get_device_chat().mark_noticed()
del acc._configtracker
def run_bot_process(self, module, ffi=True):
fn = module.__file__
@@ -391,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):
@@ -493,6 +516,9 @@ def lp():
def step(self, msg):
print("-" * 5, "step " + msg, "-" * 5)
def indent(self, msg):
print(" " + msg)
return Printer()

View File

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

File diff suppressed because it is too large Load Diff

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.45.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.45.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.45.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

@@ -1,11 +1,9 @@
#!/bin/bash
export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export BRANCH=${CIRCLE_BRANCH:-master}
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
@@ -32,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

@@ -1,11 +1,9 @@
#!/bin/bash
export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export BRANCH=${CIRCLE_BRANCH:-master}
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
@@ -30,11 +28,7 @@ ssh $SSHTARGET <<_HERE
export RUSTC_WRAPPER=\`which sccache\`
cd $BUILDDIR
# let's share the target dir with our last run on this branch/job-type
# cargo will make sure to block/unblock us properly
export CARGO_TARGET_DIR=\`pwd\`/../target
export TARGET=release
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
#we rely on tox/virtualenv being available in the host
@@ -48,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

@@ -1,11 +1,9 @@
#!/bin/bash
export BRANCH=${CIRCLE_BRANCH:?branch to build}
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
export BRANCH=${CIRCLE_BRANCH:-master}
export REPONAME=${CIRCLE_PROJECT_REPONAME:-deltachat-core-rust}
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
# we construct the BUILDDIR such that we can easily share the
# CARGO_TARGET_DIR between runs ("..")
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
set -e
@@ -24,12 +22,9 @@ ssh $SSHTARGET <<_HERE
shopt -s huponexit
export RUSTC_WRAPPER=\`which sccache\`
cd $BUILDDIR
# let's share the target dir with our last run on this branch/job-type
# cargo will make sure to block/unblock us properly
export CARGO_TARGET_DIR=\`pwd\`/../target
export TARGET=x86_64-unknown-linux-gnu
export RUSTC_WRAPPER=sccache
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

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

View File

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

View File

@@ -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

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

@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
import os
import sys
@@ -63,13 +63,14 @@ def main():
ffi_toml = read_toml_version("deltachat-ffi/Cargo.toml")
assert core_toml == ffi_toml, (core_toml, ffi_toml)
for line in open("CHANGELOG.md"):
## 1.25.0
if line.startswith("## "):
if line[2:].strip().startswith(newversion):
break
else:
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
if "alpha" not in newversion:
for line in open("CHANGELOG.md"):
## 1.25.0
if line.startswith("## "):
if line[2:].strip().startswith(newversion):
break
else:
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
replace_toml_version("Cargo.toml", newversion)
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)

View File

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

View File

@@ -1,18 +1,15 @@
use std::collections::HashMap;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::task::{Context as TaskContext, Poll};
use std::collections::BTreeMap;
use async_std::fs;
use async_std::path::PathBuf;
use async_std::prelude::*;
use async_std::sync::{Arc, RwLock};
use uuid::Uuid;
use anyhow::{ensure, Context as _};
use anyhow::{ensure, Context as _, Result};
use serde::{Deserialize, Serialize};
use crate::context::Context;
use crate::error::Result;
use crate::events::Event;
/// Account manager, that can handle multiple accounts in a single place.
@@ -20,7 +17,7 @@ use crate::events::Event;
pub struct Accounts {
dir: PathBuf,
config: Config,
accounts: Arc<RwLock<HashMap<u32, Context>>>,
accounts: Arc<RwLock<BTreeMap<u32, Context>>>,
}
impl Accounts {
@@ -138,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(())
};
@@ -190,7 +197,7 @@ impl Accounts {
let id = self.add_account().await?;
let ctx = self.get_account(id).await.expect("just added");
match crate::imex::imex(&ctx, crate::imex::ImexMode::ImportBackup, Some(file)).await {
match crate::imex::imex(&ctx, crate::imex::ImexMode::ImportBackup, &file).await {
Ok(_) => Ok(id),
Err(err) => {
// remove temp account
@@ -225,61 +232,42 @@ impl Accounts {
/// Unified event emitter.
pub async fn get_event_emitter(&self) -> EventEmitter {
let emitters = self
let emitters: Vec<_> = self
.accounts
.read()
.await
.iter()
.map(|(id, a)| EmitterWrapper {
id: *id,
emitter: a.get_event_emitter(),
done: AtomicBool::new(false),
})
.map(|(_id, a)| a.get_event_emitter())
.collect();
EventEmitter(emitters)
EventEmitter(futures::stream::select_all(emitters))
}
}
#[derive(Debug)]
pub struct EventEmitter(futures::stream::SelectAll<crate::events::EventEmitter>);
impl EventEmitter {
/// Blocking recv of an event. Return `None` if the `Sender` has been droped.
pub fn recv_sync(&self) -> Option<Event> {
/// Blocking recv of an event. Return `None` if all `Sender`s have been droped.
pub fn recv_sync(&mut self) -> Option<Event> {
async_std::task::block_on(self.recv())
}
/// Async recv of an event. Return `None` if the `Sender` has been droped.
pub async fn recv(&self) -> Option<Event> {
futures::future::poll_fn(|cx| Pin::new(self).recv_poll(cx)).await
}
fn recv_poll(self: Pin<&Self>, _cx: &mut TaskContext<'_>) -> Poll<Option<Event>> {
for e in &*self.0 {
if e.done.load(Ordering::Acquire) {
// skip emitters that are already done
continue;
}
match e.emitter.try_recv() {
Ok(event) => return Poll::Ready(Some(event)),
Err(async_std::sync::TryRecvError::Disconnected) => {
e.done.store(true, Ordering::Release);
}
Err(async_std::sync::TryRecvError::Empty) => {}
}
}
Poll::Pending
/// Async recv of an event. Return `None` if all `Sender`s have been droped.
pub async fn recv(&mut self) -> Option<Event> {
self.0.next().await
}
}
#[derive(Debug)]
pub struct EventEmitter(Vec<EmitterWrapper>);
impl async_std::stream::Stream for EventEmitter {
type Item = Event;
#[derive(Debug)]
struct EmitterWrapper {
id: u32,
emitter: crate::events::EventEmitter,
done: AtomicBool,
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
std::pin::Pin::new(&mut self.0).poll_next(cx)
}
}
pub const CONFIG_NAME: &str = "accounts.toml";
@@ -342,9 +330,9 @@ impl Config {
})
}
pub async fn load_accounts(&self) -> Result<HashMap<u32, Context>> {
pub async fn load_accounts(&self) -> Result<BTreeMap<u32, Context>> {
let cfg = &*self.inner.read().await;
let mut accounts = HashMap::with_capacity(cfg.accounts.len());
let mut accounts = BTreeMap::new();
for account_config in &cfg.accounts {
let ctx = Context::new(
cfg.os_name.clone(),
@@ -368,7 +356,6 @@ impl Config {
inner.accounts.push(AccountConfig {
id,
name: String::new(),
dir: target_dir.into(),
uuid,
});
@@ -435,8 +422,6 @@ impl Config {
pub struct AccountConfig {
/// Unique id.
pub id: u32,
/// Display name
pub name: String,
/// Root directory for all data for this account.
pub dir: std::path::PathBuf,
pub uuid: Uuid,
@@ -527,7 +512,29 @@ 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()
);
}
/// Tests that accounts are sorted by ID.
#[async_std::test]
async fn test_accounts_sorted() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
for expected_id in 2..10 {
let id = accounts.add_account().await.unwrap();
assert_eq!(id, expected_id);
}
let ids = accounts.get_all().await;
for (i, expected_id) in (1..10).enumerate() {
assert_eq!(ids.get(i), Some(&expected_id));
}
}
}

View File

@@ -6,7 +6,7 @@ use std::collections::BTreeMap;
use std::str::FromStr;
use std::{fmt, str};
use crate::contact::*;
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, SignedPublicKey};

View File

@@ -7,14 +7,17 @@ use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use async_std::{fs, io};
use anyhow::Error;
use image::GenericImageView;
use num_traits::FromPrimitive;
use thiserror::Error;
use crate::config::Config;
use crate::constants::*;
use crate::constants::{
MediaQuality, Viewtype, BALANCED_AVATAR_SIZE, BALANCED_IMAGE_SIZE, WORSE_AVATAR_SIZE,
WORSE_IMAGE_SIZE,
};
use crate::context::Context;
use crate::error::Error;
use crate::events::EventType;
use crate::message;
@@ -55,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 {
@@ -63,6 +66,12 @@ impl<'a> BlobObject<'a> {
blobname: name.clone(),
cause: err.into(),
})?;
// workaround a bug in async-std
// (the executor does not handle blocking operation in Drop correctly,
// see https://github.com/async-rs/async-std/issues/900 )
let _ = file.flush().await;
let blob = BlobObject {
blobdir,
name: format!("$BLOBDIR/{}", name),
@@ -151,6 +160,10 @@ impl<'a> BlobObject<'a> {
cause: err,
});
}
// workaround, see create() for details
let _ = dst_file.flush().await;
let blob = BlobObject {
blobdir: context.get_blobdir(),
name: format!("$BLOBDIR/{}", name),
@@ -367,27 +380,18 @@ impl<'a> BlobObject<'a> {
true
}
pub fn recode_to_avatar_size(&self, context: &Context) -> Result<(), BlobError> {
pub async fn recode_to_avatar_size(&self, context: &Context) -> Result<(), BlobError> {
let blob_abs = self.to_abs_path();
let img = image::open(&blob_abs).map_err(|err| BlobError::RecodeFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
})?;
if img.width() <= AVATAR_SIZE && img.height() <= AVATAR_SIZE {
return Ok(());
}
let img_wh =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
MediaQuality::Balanced => BALANCED_AVATAR_SIZE,
MediaQuality::Worse => WORSE_AVATAR_SIZE,
};
let img = img.thumbnail(AVATAR_SIZE, AVATAR_SIZE);
img.save(&blob_abs).map_err(|err| BlobError::WriteFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err.into(),
})?;
Ok(())
self.recode_to_size(context, blob_abs, img_wh).await
}
pub async fn recode_to_image_size(&self, context: &Context) -> Result<(), BlobError> {
@@ -398,39 +402,54 @@ impl<'a> BlobObject<'a> {
return Ok(());
}
let img = image::open(&blob_abs).map_err(|err| BlobError::RecodeFailure {
let img_wh =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
MediaQuality::Balanced => BALANCED_IMAGE_SIZE,
MediaQuality::Worse => WORSE_IMAGE_SIZE,
};
self.recode_to_size(context, blob_abs, img_wh).await
}
async fn recode_to_size(
&self,
context: &Context,
blob_abs: PathBuf,
img_wh: u32,
) -> Result<(), BlobError> {
let mut img = image::open(&blob_abs).map_err(|err| BlobError::RecodeFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
})?;
let orientation = self.get_exif_orientation(context);
let img_wh = if MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await)
.unwrap_or_default()
== MediaQuality::Balanced
{
BALANCED_IMAGE_SIZE
} else {
WORSE_IMAGE_SIZE
};
let do_scale = img.width() > img_wh || img.height() > img_wh;
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
if img.width() <= img_wh && img.height() <= img_wh {
return Ok(());
if do_scale || do_rotate {
if do_scale {
img = img.thumbnail(img_wh, img_wh);
}
if do_rotate {
img = match orientation {
Ok(90) => img.rotate90(),
Ok(180) => img.rotate180(),
Ok(270) => img.rotate270(),
_ => img,
}
}
img.save(&blob_abs).map_err(|err| BlobError::WriteFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err.into(),
})?;
}
let mut img = img.thumbnail(img_wh, img_wh);
match self.get_exif_orientation(context) {
Ok(90) => img = img.rotate90(),
Ok(180) => img = img.rotate180(),
Ok(270) => img = img.rotate270(),
_ => {}
}
img.save(&blob_abs).map_err(|err| BlobError::WriteFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err.into(),
})?;
Ok(())
}
@@ -495,75 +514,67 @@ 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)]
mod tests {
use super::*;
use crate::test_utils::*;
use crate::test_utils::TestContext;
#[async_std::test]
async fn test_create() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t.ctx, "foo", b"hello").await.unwrap();
let fname = t.ctx.get_blobdir().join("foo");
let blob = BlobObject::create(&t, "foo", b"hello").await.unwrap();
let fname = t.get_blobdir().join("foo");
let data = fs::read(fname).await.unwrap();
assert_eq!(data, b"hello");
assert_eq!(blob.as_name(), "$BLOBDIR/foo");
assert_eq!(blob.to_abs_path(), t.ctx.get_blobdir().join("foo"));
assert_eq!(blob.to_abs_path(), t.get_blobdir().join("foo"));
}
#[async_std::test]
async fn test_lowercase_ext() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello")
.await
.unwrap();
let blob = BlobObject::create(&t, "foo.TXT", b"hello").await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/foo.txt");
}
#[async_std::test]
async fn test_as_file_name() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
assert_eq!(blob.as_file_name(), "foo.txt");
}
#[async_std::test]
async fn test_as_rel_path() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
assert_eq!(blob.as_rel_path(), Path::new("foo.txt"));
}
#[async_std::test]
async fn test_suffix() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
assert_eq!(blob.suffix(), Some("txt"));
let blob = BlobObject::create(&t.ctx, "bar", b"world").await.unwrap();
let blob = BlobObject::create(&t, "bar", b"world").await.unwrap();
assert_eq!(blob.suffix(), None);
}
#[async_std::test]
async fn test_create_dup() {
let t = TestContext::new().await;
BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
let foo_path = t.ctx.get_blobdir().join("foo.txt");
BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
let foo_path = t.get_blobdir().join("foo.txt");
assert!(foo_path.exists().await);
BlobObject::create(&t.ctx, "foo.txt", b"world")
.await
.unwrap();
let mut dir = fs::read_dir(t.ctx.get_blobdir()).await.unwrap();
BlobObject::create(&t, "foo.txt", b"world").await.unwrap();
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
while let Some(dirent) = dir.next().await {
let fname = dirent.unwrap().file_name();
if fname == foo_path.file_name().unwrap() {
@@ -579,15 +590,15 @@ mod tests {
#[async_std::test]
async fn test_double_ext_preserved() {
let t = TestContext::new().await;
BlobObject::create(&t.ctx, "foo.tar.gz", b"hello")
BlobObject::create(&t, "foo.tar.gz", b"hello")
.await
.unwrap();
let foo_path = t.ctx.get_blobdir().join("foo.tar.gz");
let foo_path = t.get_blobdir().join("foo.tar.gz");
assert!(foo_path.exists().await);
BlobObject::create(&t.ctx, "foo.tar.gz", b"world")
BlobObject::create(&t, "foo.tar.gz", b"world")
.await
.unwrap();
let mut dir = fs::read_dir(t.ctx.get_blobdir()).await.unwrap();
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
while let Some(dirent) = dir.next().await {
let fname = dirent.unwrap().file_name();
if fname == foo_path.file_name().unwrap() {
@@ -605,7 +616,7 @@ mod tests {
async fn test_create_long_names() {
let t = TestContext::new().await;
let s = "1".repeat(150);
let blob = BlobObject::create(&t.ctx, &s, b"data").await.unwrap();
let blob = BlobObject::create(&t, &s, b"data").await.unwrap();
let blobname = blob.as_name().split('/').last().unwrap();
assert!(blobname.len() < 128);
}
@@ -615,14 +626,14 @@ mod tests {
let t = TestContext::new().await;
let src = t.dir.path().join("src");
fs::write(&src, b"boo").await.unwrap();
let blob = BlobObject::create_and_copy(&t.ctx, &src).await.unwrap();
let blob = BlobObject::create_and_copy(&t, &src).await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/src");
let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo");
let whoops = t.dir.path().join("whoops");
assert!(BlobObject::create_and_copy(&t.ctx, &whoops).await.is_err());
let whoops = t.ctx.get_blobdir().join("whoops");
assert!(BlobObject::create_and_copy(&t, &whoops).await.is_err());
let whoops = t.get_blobdir().join("whoops");
assert!(!whoops.exists().await);
}
@@ -632,14 +643,14 @@ mod tests {
let src_ext = t.dir.path().join("external");
fs::write(&src_ext, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap();
let blob = BlobObject::new_from_path(&t, &src_ext).await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/external");
let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo");
let src_int = t.ctx.get_blobdir().join("internal");
let src_int = t.get_blobdir().join("internal");
fs::write(&src_int, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_int).await.unwrap();
let blob = BlobObject::new_from_path(&t, &src_int).await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo");
@@ -649,7 +660,7 @@ mod tests {
let t = TestContext::new().await;
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
fs::write(&src_ext, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap();
let blob = BlobObject::new_from_path(&t, &src_ext).await.unwrap();
assert_eq!(
blob.as_name(),
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
@@ -685,7 +696,7 @@ mod tests {
let (stem, ext) = BlobObject::sanitise_name("foo?.bar");
assert!(stem.contains("foo"));
assert!(!stem.contains("?"));
assert!(!stem.contains('?'));
assert_eq!(ext, ".bar");
let (stem, ext) = BlobObject::sanitise_name("no-extension");
@@ -698,10 +709,10 @@ mod tests {
assert!(!stem.contains("ignored"));
assert!(stem.contains("this"));
assert!(stem.contains("forbidden"));
assert!(!stem.contains("/"));
assert!(!stem.contains("\\"));
assert!(!stem.contains(":"));
assert!(!stem.contains("*"));
assert!(!stem.contains("?"));
assert!(!stem.contains('/'));
assert!(!stem.contains('\\'));
assert!(!stem.contains(':'));
assert!(!stem.contains('*'));
assert!(!stem.contains('?'));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,22 @@
//! # Chat list module
use anyhow::{bail, ensure, Result};
use async_std::prelude::*;
use sqlx::Row;
use crate::chat;
use crate::chat::*;
use crate::constants::*;
use crate::contact::*;
use crate::context::*;
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{
Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_DEADDROP,
DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF, DC_CONTACT_ID_UNDEFINED, DC_GCL_ADD_ALLDONE_HINT,
DC_GCL_ARCHIVED_ONLY, DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
};
use crate::contact::Contact;
use crate::context::Context;
use crate::ephemeral::delete_expired_messages;
use crate::error::{bail, ensure, Result};
use crate::lot::Lot;
use crate::message::{Message, MessageState, MsgId};
use crate::stock::StockMessage;
use crate::stock_str;
/// An object representing a single chatlist in memory.
///
@@ -58,9 +65,8 @@ impl Chatlist {
/// messages from addresses that have no relationship to the configured account.
/// The last of these messages is represented by DC_CHAT_ID_DEADDROP and you can retrieve details
/// about it with chatlist.get_msg_id(). Typically, the UI asks the user "Do you want to chat with NAME?"
/// and offers the options "Yes" (call dc_create_chat_by_msg_id()), "Never" (call dc_block_contact())
/// or "Not now".
/// The UI can also offer a "Close" button that calls dc_marknoticed_contact() then.
/// and offers the options "Start chat", "Block" and "Not now";
/// The decision should be passed to dc_decide_on_contact_request().
/// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
/// archived *any* chat using dc_set_chat_visibility(). The UI should show a link as
/// "Show archived chats", if the user clicks this item, the UI should show a
@@ -106,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
@@ -126,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`
@@ -141,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
@@ -158,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,
@@ -170,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
@@ -186,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");
@@ -204,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
@@ -220,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 {
@@ -235,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
@@ -250,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;
@@ -270,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 })
@@ -359,30 +370,29 @@ impl Chatlist {
return ret;
};
let mut lastcontact = None;
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id).await {
if lastmsg.from_id != DC_CONTACT_ID_SELF
&& (chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup)
{
lastcontact = Contact::load_from_db(context, lastmsg.from_id).await.ok();
}
Some(lastmsg)
} else {
None
};
let (lastmsg, lastcontact) =
if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id).await {
if lastmsg.from_id == DC_CONTACT_ID_SELF {
(Some(lastmsg), None)
} else {
match chat.typ {
Chattype::Group | Chattype::Mailinglist => {
let lastcontact =
Contact::load_from_db(context, lastmsg.from_id).await.ok();
(Some(lastmsg), lastcontact)
}
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
}
}
} else {
(None, None)
};
if chat.id.is_archived_link() {
ret.text2 = None;
} else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED
{
ret.text2 = Some(
context
.stock_str(StockMessage::NoMessages)
.await
.to_string(),
);
ret.text2 = Some(stock_str::no_messages(context).await);
} else {
ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context)
.await;
@@ -397,61 +407,62 @@ 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::test_utils::*;
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;
#[async_std::test]
async fn test_try_load() {
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat")
let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "b chat")
.await
.unwrap();
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat")
let chat_id3 = create_group_chat(&t, ProtectionStatus::Unprotected, "c chat")
.await
.unwrap();
// check that the chatlist starts with the most recent message
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 3);
assert_eq!(chats.get_chat_id(0), chat_id3);
assert_eq!(chats.get_chat_id(1), chat_id2);
@@ -460,26 +471,24 @@ 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.ctx, Some(&mut msg)).await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
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);
// check chatlist query and archive functionality
let chats = Chatlist::try_load(&t.ctx, 0, Some("b"), None)
.await
.unwrap();
let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
assert_eq!(chats.len(), 1);
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None)
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
.await
.unwrap();
assert_eq!(chats.len(), 0);
chat_id1
.set_visibility(&t.ctx, ChatVisibility::Archived)
.set_visibility(&t, ChatVisibility::Archived)
.await
.ok();
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None)
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
.await
.unwrap();
assert_eq!(chats.len(), 1);
@@ -488,23 +497,23 @@ mod tests {
#[async_std::test]
async fn test_sort_self_talk_up_on_forward() {
let t = TestContext::new().await;
t.ctx.update_device_chats().await.unwrap();
create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
t.update_device_chats().await.unwrap();
create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert!(chats.len() == 3);
assert!(!Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
assert!(!Chat::load_from_db(&t, chats.get_chat_id(0))
.await
.unwrap()
.is_self_talk());
let chats = Chatlist::try_load(&t.ctx, DC_GCL_FOR_FORWARDING, None, None)
let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
.await
.unwrap();
assert!(chats.len() == 2); // device chat cannot be written and is skipped on forwarding
assert!(Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
assert!(Chat::load_from_db(&t, chats.get_chat_id(0))
.await
.unwrap()
.is_self_talk());
@@ -513,49 +522,177 @@ mod tests {
#[async_std::test]
async fn test_search_special_chat_names() {
let t = TestContext::new().await;
t.ctx.update_device_chats().await.unwrap();
t.update_device_chats().await.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None)
let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)
.await
.unwrap();
assert_eq!(chats.len(), 0);
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None)
let chats = Chatlist::try_load(&t, 0, Some("t-5678-b"), None)
.await
.unwrap();
assert_eq!(chats.len(), 0);
t.ctx
.set_stock_translation(StockMessage::SavedMessages, "test-1234-save".to_string())
t.set_stock_translation(StockMessage::SavedMessages, "test-1234-save".to_string())
.await
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None)
let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)
.await
.unwrap();
assert_eq!(chats.len(), 1);
t.ctx
.set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string())
t.set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string())
.await
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None)
let chats = Chatlist::try_load(&t, 0, Some("t-5678-b"), None)
.await
.unwrap();
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;
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("foo:\nbar \r\n test".to_string()));
chat_id1.set_draft(&t.ctx, Some(&mut msg)).await;
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
let summary = chats.get_summary(&t.ctx, 0, None).await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
let summary = chats.get_summary(&t, 0, None).await;
assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary
}
}

62
src/color.rs Normal file
View File

@@ -0,0 +1,62 @@
//! Implementation of Consistent Color Generation
//!
//! Consistent Color Generation is defined in XEP-0392.
//!
//! Color Vision Deficiency correction is not implemented as Delta Chat does not offer
//! corresponding settings.
use hsluv::hsluv_to_rgb;
use sha1::{Digest, Sha1};
/// Converts an identifier to Hue angle.
fn str_to_angle(s: impl AsRef<str>) -> f64 {
let bytes = s.as_ref().as_bytes();
let result = Sha1::digest(bytes);
let checksum: u16 = result.get(0).map_or(0, |&x| u16::from(x))
+ 256 * result.get(1).map_or(0, |&x| u16::from(x));
f64::from(checksum) / 65536.0 * 360.0
}
/// 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 {
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_str_to_angle() {
// Test against test vectors from
// https://xmpp.org/extensions/xep-0392.html#testvectors-fullrange-no-cvd
assert!((str_to_angle("Romeo") - 327.255249).abs() < 1e-6);
assert!((str_to_angle("juliet@capulet.lit") - 209.410400).abs() < 1e-6);
assert!((str_to_angle("😺") - 331.199341).abs() < 1e-6);
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};
@@ -7,12 +8,13 @@ use crate::blob::BlobObject;
use crate::chat::ChatId;
use crate::constants::DC_VERSION_STR;
use crate::context::Context;
use crate::dc_tools::*;
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input};
use crate::events::EventType;
use crate::job;
use crate::message::MsgId;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::stock::StockMessage;
use crate::provider::{get_provider_by_id, Provider};
use crate::stock_str;
/// The available configuration keys.
#[derive(
@@ -35,9 +37,6 @@ pub enum Config {
SmtpCertificateChecks,
ServerFlags,
#[strum(props(default = "INBOX"))]
ImapFolder,
Displayname,
Selfstatus,
Selfavatar,
@@ -63,12 +62,21 @@ pub enum Config {
#[strum(props(default = "1"))]
MvboxMove,
#[strum(props(default = "0"))]
SentboxMove, // If `MvboxMove` is true, this config is ignored. Currently only used in tests.
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
ShowEmails,
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
MediaQuality,
/// If set to "1", on the first time `start_io()` is called after configuring,
/// the newest existing messages are fetched.
/// Existing recipients are added to the contact database regardless of this setting.
#[strum(props(default = "1"))]
FetchExistingMsgs,
#[strum(props(default = "0"))]
KeyGenType,
@@ -110,6 +118,9 @@ pub enum Config {
ConfiguredInboxFolder,
ConfiguredMvboxFolder,
ConfiguredSentboxFolder,
ConfiguredSpamFolder,
ConfiguredTimestamp,
ConfiguredProvider,
Configured,
#[strum(serialize = "sys.version")]
@@ -121,113 +132,151 @@ pub enum Config {
#[strum(serialize = "sys.config_keys")]
SysConfigKeys,
#[strum(props(default = "0"))]
Bot,
/// Whether we send a warning if the password is wrong (set to false when we send a warning
/// because we do not want to send a second warning)
#[strum(props(default = "0"))]
NotifyAboutWrongPw,
/// address to webrtc instance to use for videochats
WebrtcInstance,
/// Timestamp of the last time housekeeping was run
LastHousekeeping,
/// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely.
#[strum(props(default = "60"))]
ScanAllFoldersDebounceSecs,
}
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(self.stock_str(StockMessage::StatusLine).await.into_owned()),
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_bool(&self, key: Config) -> bool {
self.get_config_int(key).await != 0
pub async fn get_config_i64(&self, key: Config) -> Result<i64> {
self.get_config(key)
.await
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
}
pub async fn get_config_u64(&self, key: Config) -> Result<u64> {
self.get_config(key)
.await
.map(|s: Option<String>| s.and_then(|s| s.parse().ok()).unwrap_or_default())
}
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)),
}
}
/// Gets the configured provider, as saved in the `configured_provider` value.
///
/// 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) -> 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?;
blob.recode_to_avatar_size(self)?;
self.sql
.set_raw_config(self, key, Some(blob.as_name()))
.await
let blob = BlobObject::new_from_path(self, value).await?;
blob.recode_to_avatar_size(self).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 => {
let def = self.stock_str(StockMessage::StatusLine).await;
let def = stock_str::status_line(self).await;
let val = if value.is_none() || value.unwrap() == def {
None
} else {
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),
@@ -237,16 +286,30 @@ 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?;
Ok(())
}
}
/// Returns all available configuration keys concated together.
@@ -268,8 +331,8 @@ mod tests {
use std::string::ToString;
use crate::constants;
use crate::constants::AVATAR_SIZE;
use crate::test_utils::*;
use crate::constants::BALANCED_AVATAR_SIZE;
use crate::test_utils::TestContext;
use image::GenericImageView;
use num_traits::FromPrimitive;
use std::fs::File;
@@ -287,11 +350,6 @@ mod tests {
);
}
#[test]
fn test_default_prop() {
assert_eq!(Config::ImapFolder.get_str("default"), Some("INBOX"));
}
#[async_std::test]
async fn test_selfavatar_outside_blobdir() {
let t = TestContext::new().await;
@@ -301,15 +359,14 @@ mod tests {
.unwrap()
.write_all(avatar_bytes)
.unwrap();
let avatar_blob = t.ctx.get_blobdir().join("avatar.jpg");
let avatar_blob = t.get_blobdir().join("avatar.jpg");
assert!(!avatar_blob.exists().await);
t.ctx
.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.ctx.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();
@@ -317,14 +374,14 @@ mod tests {
assert_eq!(img.height(), 1000);
let img = image::open(avatar_blob).unwrap();
assert_eq!(img.width(), AVATAR_SIZE);
assert_eq!(img.height(), AVATAR_SIZE);
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
}
#[async_std::test]
async fn test_selfavatar_in_blobdir() {
let t = TestContext::new().await;
let avatar_src = t.ctx.get_blobdir().join("avatar.png");
let avatar_src = t.get_blobdir().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
File::create(&avatar_src)
.unwrap()
@@ -335,16 +392,15 @@ mod tests {
assert_eq!(img.width(), 900);
assert_eq!(img.height(), 900);
t.ctx
.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.ctx.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();
assert_eq!(img.width(), AVATAR_SIZE);
assert_eq!(img.height(), AVATAR_SIZE);
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
}
#[async_std::test]
@@ -356,10 +412,9 @@ mod tests {
.unwrap()
.write_all(avatar_bytes)
.unwrap();
let avatar_blob = t.ctx.get_blobdir().join("avatar.png");
let avatar_blob = t.get_blobdir().join("avatar.png");
assert!(!avatar_blob.exists().await);
t.ctx
.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);
@@ -367,24 +422,21 @@ mod tests {
std::fs::metadata(&avatar_blob).unwrap().len(),
avatar_bytes.len() as u64
);
let avatar_cfg = t.ctx.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.ctx.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.ctx
.set_config(Config::MediaQuality, Some("1"))
.await
.unwrap();
t.set_config(Config::MediaQuality, Some("1")).await.unwrap();
let media_quality = t.ctx.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 {
@@ -251,10 +251,10 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerPar
pub(crate) async fn moz_autoconfigure(
context: &Context,
url: &str,
url: impl AsRef<str>,
param_in: &LoginParam,
) -> Result<Vec<ServerParams>, Error> {
let xml_raw = read_url(context, url).await?;
let xml_raw = read_url(context, url.as_ref()).await?;
let res = parse_serverparams(&param_in.addr, &xml_raw);
if let Err(err) = &res {
@@ -268,16 +268,18 @@ pub(crate) async fn moz_autoconfigure(
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
#[test]
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);
}
@@ -293,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);
@@ -319,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 {
@@ -187,9 +187,8 @@ fn protocols_to_serverparams(protocols: Vec<ProtocolTag>) -> Vec<ServerParams> {
pub(crate) async fn outlk_autodiscover(
context: &Context,
url: &str,
mut url: String,
) -> Result<Vec<ServerParams>, Error> {
let mut url = url.to_string();
/* Follow up to 10 xml-redirects (http-redirects are followed in read_url() */
for _i in 0..10 {
let xml_raw = read_url(context, &url).await?;
@@ -209,6 +208,8 @@ pub(crate) async fn outlk_autodiscover(
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
#[test]

View File

@@ -7,38 +7,54 @@ mod server_params;
use anyhow::{bail, ensure, Context as _, Result};
use async_std::prelude::*;
use async_std::task;
use itertools::Itertools;
use job::Action;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::dc_tools::EmailAddress;
use crate::imap::Imap;
use crate::login_param::{LoginParam, ServerLoginParam};
use crate::message::Message;
use crate::oauth2::*;
use crate::oauth2::dc_get_oauth2_addr;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::smtp::Smtp;
use crate::stock_str;
use crate::{chat, e2ee, provider};
use crate::{config::Config, dc_tools::time};
use crate::{
constants::{Viewtype, DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2},
job,
};
use crate::{context::Context, param::Params};
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
use server_params::ServerParams;
use server_params::{expand_param_vector, ServerParams};
macro_rules! progress {
($context:tt, $progress:expr) => {
($context:tt, $progress:expr, $comment:expr) => {
assert!(
$progress <= 1000,
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
);
$context.emit_event($crate::events::EventType::ConfigureProgress($progress));
$context.emit_event($crate::events::EventType::ConfigureProgress {
progress: $progress,
comment: $comment,
});
};
($context:tt, $progress:expr) => {
progress!($context, $progress, None);
};
}
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.
@@ -71,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) = provider::get_provider_info(&param.addr) {
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 {
@@ -110,7 +126,19 @@ impl Context {
Ok(())
}
Err(err) => {
progress!(self, 0);
progress!(
self,
0,
Some(
stock_str::configuration_failed(
self,
// We are using Anyhow's .context() and to show the
// inner error, too, we need the {:#}:
format!("{:#}", err),
)
.await
)
);
Err(err)
}
}
@@ -142,6 +170,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
DC_LP_AUTH_NORMAL as i32
};
let ctx2 = ctx.clone();
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
// Step 1: Load the parameters and check email-address and password
if oauth2 {
@@ -149,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);
@@ -181,9 +212,51 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
{
// no advanced parameters entered by the user: query provider-database or do Autoconfig
if let Some(servers) = get_offline_autoconfig(ctx, &param.addr) {
param_autoconfig = Some(servers);
info!(
ctx,
"checking internal provider-info for offline autoconfig"
);
if let Some(provider) = provider::get_provider_info(&param_domain).await {
param.provider = Some(provider);
match provider.status {
provider::Status::Ok | provider::Status::Preparation => {
if provider.server.is_empty() {
info!(ctx, "offline autoconfig found, but no servers defined");
param_autoconfig = None;
} else {
info!(ctx, "offline autoconfig found");
let servers = provider
.server
.iter()
.map(|s| ServerParams {
protocol: s.protocol,
socket: s.socket,
hostname: s.hostname.to_string(),
port: s.port,
username: match s.username_pattern {
UsernamePattern::Email => param.addr.to_string(),
UsernamePattern::Emaillocalpart => {
if let Some(at) = param.addr.find('@') {
param.addr.split_at(at).0.to_string()
} else {
param.addr.to_string()
}
}
},
})
.collect();
param_autoconfig = Some(servers)
}
}
provider::Status::Broken => {
info!(ctx, "offline autoconfig found, provider is broken");
param_autoconfig = None;
}
}
} else {
info!(ctx, "no offline autoconfig found");
param_autoconfig =
get_autoconfig(ctx, param, &param_domain, &param_addr_urlencoded).await;
}
@@ -193,86 +266,142 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 500);
let servers: Vec<ServerParams> = param_autoconfig
.unwrap_or_else(|| {
vec![
ServerParams {
protocol: Protocol::IMAP,
hostname: param.imap.server.clone(),
port: param.imap.port,
socket: param.imap.security,
username: param.imap.user.clone(),
},
ServerParams {
protocol: Protocol::SMTP,
hostname: param.smtp.server.clone(),
port: param.smtp.port,
socket: param.smtp.security,
username: param.smtp.user.clone(),
},
]
let mut servers = param_autoconfig.unwrap_or_default();
if !servers
.iter()
.any(|server| server.protocol == Protocol::Imap)
{
servers.push(ServerParams {
protocol: Protocol::Imap,
hostname: param.imap.server.clone(),
port: param.imap.port,
socket: param.imap.security,
username: param.imap.user.clone(),
})
.into_iter()
// The order of expansion is important: ports are expanded the
// last, so they are changed the first. Username is only
// changed if default value (address with domain) didn't work
// for all available hosts and ports.
.flat_map(|params| params.expand_usernames(&param.addr).into_iter())
.flat_map(|params| params.expand_hostnames(&param_domain).into_iter())
.flat_map(|params| params.expand_ports().into_iter())
}
if !servers
.iter()
.any(|server| server.protocol == Protocol::Smtp)
{
servers.push(ServerParams {
protocol: Protocol::Smtp,
hostname: param.smtp.server.clone(),
port: param.smtp.port,
socket: param.smtp.security,
username: param.smtp.user.clone(),
})
}
let servers = expand_param_vector(servers, &param.addr, &param_domain);
progress!(ctx, 550);
// Spawn SMTP configuration task
let mut smtp = Smtp::new();
let context_smtp = ctx.clone();
let mut smtp_param = param.smtp.clone();
let smtp_addr = param.addr.clone();
let smtp_servers: Vec<ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::Smtp)
.cloned()
.collect();
let provider_strict_tls = param.provider.map_or(false, |provider| provider.strict_tls);
let smtp_config_task = task::spawn(async move {
let mut smtp_configured = false;
let mut errors = Vec::new();
for smtp_server in smtp_servers {
smtp_param.user = smtp_server.username.clone();
smtp_param.server = smtp_server.hostname.clone();
smtp_param.port = smtp_server.port;
smtp_param.security = smtp_server.socket;
match try_smtp_one_param(
&context_smtp,
&smtp_param,
&smtp_addr,
oauth2,
provider_strict_tls,
&mut smtp,
)
.await
{
Ok(_) => {
smtp_configured = true;
break;
}
Err(e) => errors.push(e),
}
}
if smtp_configured {
Ok(smtp_param)
} else {
Err(errors)
}
});
progress!(ctx, 600);
// Configure IMAP
progress!(ctx, 600);
let (_s, r) = async_std::sync::channel(1);
let (_s, r) = async_std::channel::bounded(1);
let mut imap = Imap::new(r);
let mut imap_configured = false;
for imap_server in servers
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();
for (imap_server_index, imap_server) in imap_servers.into_iter().enumerate() {
param.imap.user = imap_server.username.clone();
param.imap.server = imap_server.hostname.clone();
param.imap.port = imap_server.port;
param.imap.security = imap_server.socket;
if try_imap_one_param(ctx, &param.imap, &param.addr, oauth2, &mut imap).await {
imap_configured = true;
break;
match try_imap_one_param(
ctx,
&param.imap,
&param.addr,
oauth2,
provider_strict_tls,
&mut imap,
)
.await
{
Ok(_) => {
imap_configured = true;
break;
}
Err(e) => errors.push(e),
}
progress!(
ctx,
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
);
}
if !imap_configured {
bail!("IMAP autoconfig did not succeed");
bail!(nicer_configuration_error(ctx, errors).await);
}
// Configure SMTP
progress!(ctx, 750);
let mut smtp = Smtp::new();
progress!(ctx, 850);
let mut smtp_configured = false;
for smtp_server in servers
.iter()
.filter(|params| params.protocol == Protocol::SMTP)
{
param.smtp.user = smtp_server.username.clone();
param.smtp.server = smtp_server.hostname.clone();
param.smtp.port = smtp_server.port;
param.smtp.security = smtp_server.socket;
if try_smtp_one_param(ctx, &param.smtp, &param.addr, oauth2, &mut smtp).await {
smtp_configured = true;
break;
// Wait for SMTP configuration
match smtp_config_task.await {
Ok(smtp_param) => {
param.smtp = smtp_param;
}
Err(errors) => {
bail!(nicer_configuration_error(ctx, errors).await);
}
}
if !smtp_configured {
bail!("SMTP autoconfig did not succeed");
}
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?;
@@ -287,78 +416,27 @@ 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?;
progress!(ctx, 920);
e2ee::ensure_secret_key_exists(ctx).await?;
info!(ctx, "key generation completed");
job::add(
ctx,
job::Job::new(Action::FetchExistingMsgs, 0, Params::new(), 0),
)
.await;
progress!(ctx, 940);
update_device_chats_handle.await?;
Ok(())
}
#[derive(Debug, PartialEq, Eq)]
enum AutoconfigProvider {
Mozilla,
Outlook,
}
#[derive(Debug, PartialEq, Eq)]
struct AutoconfigSource {
provider: AutoconfigProvider,
url: String,
}
impl AutoconfigSource {
fn all(domain: &str, addr: &str) -> [Self; 5] {
[
AutoconfigSource {
provider: AutoconfigProvider::Mozilla,
url: format!(
"https://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}",
domain, addr,
),
},
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see https://releases.mozilla.org/pub/thunderbird/ , which makes some sense
AutoconfigSource {
provider: AutoconfigProvider::Mozilla,
url: format!(
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
domain, addr
),
},
AutoconfigSource {
provider: AutoconfigProvider::Outlook,
url: format!("https://{}/autodiscover/autodiscover.xml", domain),
},
// Outlook uses always SSL but different domains (this comment describes the next two steps)
AutoconfigSource {
provider: AutoconfigProvider::Outlook,
url: format!(
"https://autodiscover.{}/autodiscover/autodiscover.xml",
domain
),
},
// always SSL for Thunderbird's database
AutoconfigSource {
provider: AutoconfigProvider::Mozilla,
url: format!("https://autoconfig.thunderbird.net/v1.1/{}", domain),
},
]
}
async fn fetch(&self, ctx: &Context, param: &LoginParam) -> Result<Vec<ServerParams>> {
let params = match self.provider {
AutoconfigProvider::Mozilla => moz_autoconfigure(ctx, &self.url, &param).await?,
AutoconfigProvider::Outlook => outlk_autodiscover(ctx, &self.url).await?,
};
Ok(params)
}
}
/// Retrieve available autoconfigurations.
///
/// A Search configurations from the domain used in the email-address, prefer encrypted
@@ -369,88 +447,99 @@ async fn get_autoconfig(
param_domain: &str,
param_addr_urlencoded: &str,
) -> Option<Vec<ServerParams>> {
let sources = AutoconfigSource::all(param_domain, param_addr_urlencoded);
if let Ok(res) = moz_autoconfigure(
ctx,
format!(
"https://autoconfig.{}/mail/config-v1.1.xml?emailaddress={}",
param_domain, param_addr_urlencoded
),
param,
)
.await
{
return Some(res);
}
progress!(ctx, 300);
let mut progress = 300;
for source in &sources {
let res = source.fetch(ctx, param).await;
progress!(ctx, progress);
progress += 10;
if let Ok(res) = res {
return Some(res);
}
if let Ok(res) = moz_autoconfigure(
ctx,
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see https://releases.mozilla.org/pub/thunderbird/ , which makes some sense
format!(
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
&param_domain, &param_addr_urlencoded
),
param,
)
.await
{
return Some(res);
}
progress!(ctx, 310);
// Outlook uses always SSL but different domains (this comment describes the next two steps)
if let Ok(res) = outlk_autodiscover(
ctx,
format!("https://{}/autodiscover/autodiscover.xml", &param_domain),
)
.await
{
return Some(res);
}
progress!(ctx, 320);
if let Ok(res) = outlk_autodiscover(
ctx,
format!(
"https://autodiscover.{}/autodiscover/autodiscover.xml",
&param_domain
),
)
.await
{
return Some(res);
}
progress!(ctx, 330);
// always SSL for Thunderbird's database
if let Ok(res) = moz_autoconfigure(
ctx,
format!("https://autoconfig.thunderbird.net/v1.1/{}", &param_domain),
param,
)
.await
{
return Some(res);
}
None
}
fn get_offline_autoconfig(context: &Context, addr: &str) -> Option<Vec<ServerParams>> {
info!(
context,
"checking internal provider-info for offline autoconfig"
);
if let Some(provider) = provider::get_provider_info(&addr) {
match provider.status {
provider::Status::OK | provider::Status::PREPARATION => {
if provider.server.is_empty() {
info!(context, "offline autoconfig found, but no servers defined");
None
} else {
info!(context, "offline autoconfig found");
let servers = provider
.server
.iter()
.map(|s| ServerParams {
protocol: s.protocol,
socket: s.socket,
hostname: s.hostname.to_string(),
port: s.port,
username: match s.username_pattern {
UsernamePattern::EMAIL => addr.to_string(),
UsernamePattern::EMAILLOCALPART => {
if let Some(at) = addr.find('@') {
addr.split_at(at).0.to_string()
} else {
addr.to_string()
}
}
},
})
.collect();
Some(servers)
}
}
provider::Status::BROKEN => {
info!(context, "offline autoconfig found, provider is broken");
None
}
}
} else {
info!(context, "no offline autoconfig found");
None
}
}
async fn try_imap_one_param(
context: &Context,
param: &ServerLoginParam,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
imap: &mut Imap,
) -> bool {
) -> Result<(), ConfigurationError> {
let inf = format!(
"imap: {}@{}:{} security={} certificate_checks={} oauth2={}",
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
);
info!(context, "Trying: {}", inf);
if let Err(err) = imap.connect(context, param, addr, oauth2).await {
if let Err(err) = imap
.connect(context, param, addr, oauth2, provider_strict_tls)
.await
{
info!(context, "failure: {}", err);
false
Err(ConfigurationError {
config: inf,
msg: err.to_string(),
})
} else {
info!(context, "success: {}", inf);
true
Ok(())
}
}
@@ -459,37 +548,74 @@ async fn try_smtp_one_param(
param: &ServerLoginParam,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
smtp: &mut Smtp,
) -> bool {
) -> Result<(), ConfigurationError> {
let inf = format!(
"smtp: {}@{}:{} security={} certificate_checks={} oauth2={}",
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
);
info!(context, "Trying: {}", inf);
if let Err(err) = smtp.connect(context, param, addr, oauth2).await {
if let Err(err) = smtp
.connect(context, param, addr, oauth2, provider_strict_tls)
.await
{
info!(context, "failure: {}", err);
false
Err(ConfigurationError {
config: inf,
msg: err.to_string(),
})
} else {
info!(context, "success: {}", inf);
smtp.disconnect().await;
true
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
#[error("Trying {config}…\nError: {msg}")]
pub struct ConfigurationError {
config: String,
msg: String,
}
async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationError>) -> String {
let first_err = if let Some(f) = errors.first() {
f
} else {
// This means configuration failed but no errors have been captured. This should never
// happen, but if it does, the user will see classic "Error: no error".
return "no error".to_string();
};
if errors
.iter()
.all(|e| e.msg.to_lowercase().contains("could not resolve"))
{
return stock_str::error_no_network(context).await;
}
if errors.iter().all(|e| e.msg == first_err.msg) {
return first_err.msg.to_string();
}
errors.iter().map(|e| e.to_string()).join("\n\n")
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Invalid email address: {0:?}")]
InvalidEmailAddress(String),
#[error("XML error at position {position}")]
#[error("XML error at position {position}: {error}")]
InvalidXml {
position: usize,
#[source]
error: quick_xml::Error,
},
#[error("Failed to get URL")]
#[error("Failed to get URL: {0}")]
ReadUrlError(#[from] self::read_url::Error),
#[error("Number of redirection is exceeded")]
@@ -498,38 +624,18 @@ pub enum Error {
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::config::*;
use crate::test_utils::*;
use crate::config::Config;
use crate::test_utils::TestContext;
#[async_std::test]
async fn test_no_panic_on_bad_credentials() {
let t = TestContext::new().await;
t.ctx
.set_config(Config::Addr, Some("probably@unexistant.addr"))
t.set_config(Config::Addr, Some("probably@unexistant.addr"))
.await
.unwrap();
t.ctx
.set_config(Config::MailPw, Some("123456"))
.await
.unwrap();
assert!(t.ctx.configure().await.is_err());
}
#[async_std::test]
async fn test_get_offline_autoconfig() {
let context = TestContext::new().await.ctx;
let addr = "someone123@example.org";
assert!(get_offline_autoconfig(&context, addr).is_none());
let addr = "someone123@nauta.cu";
let found_params = get_offline_autoconfig(&context, addr).unwrap();
assert_eq!(found_params.len(), 2);
assert_eq!(found_params[0].protocol, Protocol::IMAP);
assert_eq!(found_params[0].hostname, "imap.nauta.cu".to_string());
assert_eq!(found_params[1].protocol, Protocol::SMTP);
assert_eq!(found_params[1].hostname, "smtp.nauta.cu".to_string());
t.set_config(Config::MailPw, Some("123456")).await.unwrap();
assert!(t.configure().await.is_err());
}
}

View File

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

View File

@@ -6,7 +6,7 @@ use crate::provider::{Protocol, Socket};
///
/// Can be loaded from offline provider database, online configuraiton
/// or derived from user entered parameters.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ServerParams {
/// Protocol, such as IMAP or SMTP.
pub protocol: Protocol,
@@ -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);
@@ -113,3 +113,84 @@ impl ServerParams {
res
}
}
/// Expands vector of `ServerParams`, replacing placeholders with
/// variants to try.
pub(crate) fn expand_param_vector(
v: Vec<ServerParams>,
addr: &str,
domain: &str,
) -> Vec<ServerParams> {
v.into_iter()
// The order of expansion is important: ports are expanded the
// last, so they are changed the first. Username is only
// changed if default value (address with domain) didn't work
// for all available hosts and ports.
.flat_map(|params| params.expand_usernames(addr).into_iter())
.flat_map(|params| params.expand_hostnames(domain).into_iter())
.flat_map(|params| params.expand_ports().into_iter())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_expand_param_vector() {
let v = expand_param_vector(
vec![ServerParams {
protocol: Protocol::Imap,
hostname: "example.net".to_string(),
port: 0,
socket: Socket::Ssl,
username: "foobar".to_string(),
}],
"foobar@example.net",
"example.net",
);
assert_eq!(
v,
vec![ServerParams {
protocol: Protocol::Imap,
hostname: "example.net".to_string(),
port: 993,
socket: Socket::Ssl,
username: "foobar".to_string(),
}],
);
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,20 +1,10 @@
//! # Constants
#![allow(dead_code)]
use deltachat_derive::*;
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
lazy_static! {
pub static ref DC_VERSION_STR: String = env!("CARGO_PKG_VERSION").to_string();
}
use crate::chat::ChatId;
// some defaults
const DC_E2EE_DEFAULT_ENABLED: i32 = 1;
const DC_INBOX_WATCH_DEFAULT: i32 = 1;
const DC_SENTBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_WATCH_DEFAULT: i32 = 1;
const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
#[derive(
Debug,
@@ -25,12 +15,11 @@ const DC_MVBOX_MOVE_DEFAULT: i32 = 1;
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
sqlx::Type,
)]
#[repr(u8)]
#[repr(i8)]
pub enum Blocked {
Not = 0,
Manually = 1,
@@ -43,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,
@@ -57,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,
@@ -70,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,
@@ -84,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,
@@ -110,27 +99,30 @@ pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
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;
// warn about an outdated app after a given number of days.
// as we use the "provider-db generation date" as reference (that might not be updated very often)
// and as not all system get speedy updates,
// do not use too small value that will annoy users checking for nonexistant updates.
pub const DC_OUTDATED_WARNING_DAYS: i64 = 365;
/// virtual chat showing all messages belonging to chats flagged with chats.blocked=2
pub(crate) const DC_CHAT_ID_DEADDROP: u32 = 1;
pub const DC_CHAT_ID_DEADDROP: 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;
/// a message is just in creation but not yet assigned to a chat (eg. we may need the message ID to set up blobs; this avoids unready message to be sent and shown)
const DC_CHAT_ID_MSGS_IN_CREATION: u32 = 4;
/// virtual chat showing all messages flagged with msgs.starred=2
pub const DC_CHAT_ID_STARRED: u32 = 5;
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,
@@ -141,18 +133,17 @@ pub const DC_CHAT_ID_LAST_SPECIAL: u32 = 9;
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
IntoStaticStr,
Serialize,
Deserialize,
sqlx::Type,
)]
#[repr(u32)]
pub enum Chattype {
Undefined = 0,
Single = 100,
Group = 120,
VerifiedGroup = 130,
Mailinglist = 140,
}
impl Default for Chattype {
@@ -165,10 +156,36 @@ 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()
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()
const DC_MAX_GET_INFO_LEN: usize = 100_000;
pub const DC_MAX_GET_INFO_LEN: usize = 100_000;
pub const DC_CONTACT_ID_UNDEFINED: u32 = 0;
pub const DC_CONTACT_ID_SELF: u32 = 1;
@@ -202,8 +219,12 @@ pub const DC_LP_AUTH_NORMAL: i32 = 0x4;
/// if none of these flags are set, the default is chosen
pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
/// How many existing messages shall be fetched after configuration.
pub const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
// max. width/height of an avatar
pub const AVATAR_SIZE: u32 = 192;
pub const BALANCED_AVATAR_SIZE: u32 = 256;
pub const WORSE_AVATAR_SIZE: u32 = 128;
// max. width/height of images
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
@@ -212,6 +233,11 @@ pub const WORSE_IMAGE_SIZE: u32 = 640;
// this value can be increased if the folder configuration is changed and must be redone on next program start
pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
// if more recipients are needed in SMTP's `RCPT TO:` header, recipient-list is splitted to chunks.
// this does not affect MIME'e `To:` header.
// can be overwritten by the setting `max_smtp_rcpt_to` in provider-db.
pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
#[derive(
Debug,
Display,
@@ -221,12 +247,11 @@ pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3;
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
sqlx::Type,
)]
#[repr(i32)]
#[repr(u32)]
pub enum Viewtype {
Unknown = 0,
@@ -285,26 +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");
}
}
// These constants are used as events
// reported to the callback given to dc_context_new().
// If you do not want to handle an event, it is always safe to return 0,
// so there is no need to add a "case" for every event.
const DC_EVENT_FILE_COPIED: usize = 2055; // deprecated;
const DC_EVENT_IS_OFFLINE: usize = 2081; // deprecated;
const DC_ERROR_SEE_STRING: usize = 0; // deprecated;
const DC_ERROR_SELF_NOT_IN_GROUP: usize = 1; // deprecated;
pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110;
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)]
@@ -313,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());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,25 +3,30 @@
use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::ops::Deref;
use std::time::{Instant, SystemTime};
use async_std::path::{Path, PathBuf};
use async_std::sync::{channel, Arc, Mutex, Receiver, RwLock, Sender};
use async_std::task;
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::*;
use crate::chat::{get_chat_cnt, ChatId};
use crate::config::Config;
use crate::constants::*;
use crate::contact::*;
use crate::dc_tools::duration_to_str;
use crate::error::*;
use crate::constants::DC_VERSION_STR;
use crate::contact::Contact;
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;
use std::time::SystemTime;
#[derive(Clone, Debug)]
pub struct Context {
@@ -44,7 +49,7 @@ pub struct InnerContext {
pub(crate) blobdir: PathBuf,
pub(crate) sql: Sql,
pub(crate) os_name: Option<String>,
pub(crate) bob: RwLock<Bob>,
pub(crate) bob: Bob,
pub(crate) last_smeared_timestamp: RwLock<i64>,
pub(crate) running_state: RwLock<RunningState>,
/// Mutex to avoid generating the key for the user more than once.
@@ -59,7 +64,12 @@ pub struct InnerContext {
pub(crate) scheduler: RwLock<Scheduler>,
pub(crate) ephemeral_task: RwLock<Option<task::JoinHandle<()>>>,
/// Id for this context on the current device.
pub(crate) last_full_folder_scan: Mutex<Option<Instant>>,
/// ID for this `Context` in the current process.
///
/// This allows for multiple `Context`s open in a single process where each context can
/// be identified by this ID.
pub(crate) id: u32,
creation_time: SystemTime,
@@ -81,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
}
@@ -121,7 +132,7 @@ impl Context {
os_name: Some(os_name),
running_state: RwLock::new(Default::default()),
sql: Sql::new(),
bob: RwLock::new(Default::default()),
bob: Default::default(),
last_smeared_timestamp: RwLock::new(0),
generating_key_mutex: Mutex::new(()),
oauth2_mutex: Mutex::new(()),
@@ -131,15 +142,13 @@ impl Context {
scheduler: RwLock::new(Scheduler::Stopped),
ephemeral_task: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
};
let ctx = Context {
inner: Arc::new(inner),
};
ensure!(
ctx.sql.open(&ctx, &ctx.dbfile, false).await,
"Failed opening sqlite database"
);
ctx.sql.open(&ctx, &ctx.dbfile, false).await?;
Ok(ctx)
}
@@ -147,7 +156,7 @@ impl Context {
/// Starts the IO scheduler.
pub async fn start_io(&self) {
info!(self, "starting IO");
if self.is_io_running().await {
if self.inner.is_io_running().await {
info!(self, "IO is already running");
return;
}
@@ -158,18 +167,9 @@ impl Context {
}
}
/// Returns if the IO scheduler is running.
pub async fn is_io_running(&self) -> bool {
self.inner.is_io_running().await
}
/// Stops the IO scheduler.
pub async fn stop_io(&self) {
info!(self, "stopping IO");
if !self.is_io_running().await {
info!(self, "IO is not running");
return;
}
self.inner.stop_io().await;
}
@@ -200,7 +200,10 @@ impl Context {
});
}
/// Get the next queued event.
/// Returns a receiver for emitted events.
///
/// Multiple emitters can be created, but note that in this case each emitted event will
/// only be received by one of the emitters, not by all of them.
pub fn get_event_emitter(&self) -> EventEmitter {
self.events.get_emitter()
}
@@ -222,7 +225,7 @@ impl Context {
s.ongoing_running = true;
s.shall_stop_ongoing = false;
let (sender, receiver) = channel(1);
let (sender, receiver) = channel::bounded(1);
s.cancel_sender = Some(sender);
Ok(receiver)
@@ -249,7 +252,9 @@ impl Context {
let s_a = &self.running_state;
let mut s = s_a.write().await;
if let Some(cancel) = s.cancel_sender.take() {
cancel.send(()).await;
if let Err(err) = cancel.send(()).await {
warn!(self, "could not cancel ongoing: {:?}", err);
}
}
if s.ongoing_running && !s.shall_stop_ongoing {
@@ -268,64 +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 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("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());
@@ -338,43 +347,92 @@ 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());
res.insert("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2.to_string());
res.insert(
"fetch_existing_msgs",
self.get_config_int(Config::FetchExistingMsgs)
.await?
.to_string(),
);
res.insert(
"show_emails",
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());
res.insert("mvbox_watch", mvbox_watch.to_string());
res.insert("mvbox_move", mvbox_move.to_string());
res.insert("sentbox_move", sentbox_move.to_string());
res.insert("folders_configured", folders_configured.to_string());
res.insert("configured_sentbox_folder", configured_sentbox_folder);
res.insert("configured_mvbox_folder", configured_mvbox_folder);
res.insert("mdns_enabled", mdns_enabled.to_string());
res.insert("e2ee_enabled", e2ee_enabled.to_string());
res.insert(
"key_gen_type",
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?
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert(
"media_quality",
self.get_config_int(Config::MediaQuality).await?.to_string(),
);
res.insert(
"delete_device_after",
self.get_config_int(Config::DeleteDeviceAfter)
.await?
.to_string(),
);
res.insert(
"delete_server_after",
self.get_config_int(Config::DeleteServerAfter)
.await?
.to_string(),
);
res.insert(
"last_housekeeping",
self.get_config_int(Config::LastHousekeeping)
.await?
.to_string(),
);
res.insert(
"scan_all_folders_debounce_secs",
self.get_config_int(Config::ScanAllFoldersDebounceSecs)
.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",
@@ -383,93 +441,126 @@ 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)
}
#[allow(non_snake_case)]
pub async fn search_msgs(&self, chat_id: ChatId, query: impl AsRef<str>) -> Vec<MsgId> {
/// 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>,
) -> Result<Vec<MsgId>> {
let real_query = query.as_ref().trim();
if real_query.is_empty() {
return Vec::new();
return Ok(Vec::new());
}
let strLikeInText = format!("%{}%", real_query);
let strLikeBeg = format!("{}%", real_query);
let str_like_in_text = format!("%{}%", real_query);
let query = if !chat_id.is_unset() {
concat!(
"SELECT m.id AS id, m.timestamp AS timestamp",
" FROM msgs m",
" LEFT JOIN contacts ct",
" ON m.from_id=ct.id",
" WHERE m.chat_id=?",
" AND m.hidden=0",
" AND ct.blocked=0",
" AND (txt LIKE ? OR ct.name LIKE ?)",
" ORDER BY m.timestamp,m.id;"
)
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 ?
ORDER BY m.timestamp,m.id;",
)
.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 {
concat!(
"SELECT m.id AS id, m.timestamp AS timestamp",
" FROM msgs m",
" LEFT JOIN contacts ct",
" ON m.from_id=ct.id",
" LEFT JOIN chats c",
" ON m.chat_id=c.id",
" WHERE m.chat_id>9",
" AND m.hidden=0",
" AND (c.blocked=0 OR c.blocked=?)",
" AND ct.blocked=0",
" AND (m.txt LIKE ? OR ct.name LIKE ?)",
" ORDER BY m.timestamp DESC,m.id DESC;"
)
// 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
LEFT JOIN chats c
ON m.chat_id=c.id
WHERE m.chat_id>9
AND m.hidden=0
AND c.blocked=0
AND ct.blocked=0
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?
};
self.sql
.query_map(
query,
paramsv![chat_id, strLikeInText, strLikeBeg],
|row| row.get::<_, MsgId>("id"),
|rows| {
let mut ret = Vec::new();
for id in rows {
ret.push(id?);
}
Ok(ret)
},
)
.await
.unwrap_or_default()
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>) -> 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 {
@@ -486,14 +577,19 @@ impl InnerContext {
}
async fn stop_io(&self) {
assert!(self.is_io_running().await, "context is already stopped");
let token = {
let lock = &*self.scheduler.read().await;
lock.pre_stop().await
};
{
let lock = &mut *self.scheduler.write().await;
lock.stop(token).await;
if self.is_io_running().await {
let token = {
let lock = &*self.scheduler.read().await;
lock.pre_stop().await
};
{
let lock = &mut *self.scheduler.write().await;
lock.stop(token).await;
}
}
if let Some(ephemeral_task) = self.ephemeral_task.write().await.take() {
ephemeral_task.cancel().await;
}
}
}
@@ -516,7 +612,17 @@ pub fn get_version_str() -> &'static str {
mod tests {
use super::*;
use crate::test_utils::*;
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]
async fn test_wrong_db() {
@@ -530,10 +636,144 @@ mod tests {
#[async_std::test]
async fn test_get_fresh_msgs() {
let t = TestContext::new().await;
let fresh = t.ctx.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();
@@ -573,7 +813,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into(), 1).await;
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir, 1).await;
assert!(res.is_err());
}
@@ -589,14 +829,14 @@ mod tests {
#[async_std::test]
async fn no_crashes_on_context_deref() {
let t = TestContext::new().await;
std::mem::drop(t.ctx);
std::mem::drop(t);
}
#[async_std::test]
async fn test_get_info() {
let t = TestContext::new().await;
let info = t.ctx.get_info().await;
let info = t.get_info().await.unwrap();
assert!(info.get("database_dir").is_some());
}
@@ -607,4 +847,137 @@ mod tests {
assert!(info.get("database_dir").is_none());
assert_eq!(info.get("level").unwrap(), "awesome");
}
#[async_std::test]
async fn test_get_info_completeness() {
// For easier debugging,
// get_info() shall return all important information configurable by the Config-values.
//
// There are exceptions for Config-values considered to be unimportant,
// too sensitive or summarized in another item.
let skip_from_get_info = vec![
"addr",
"displayname",
"imap_certificate_checks",
"mail_server",
"mail_user",
"mail_pw",
"mail_port",
"mail_security",
"notify_about_wrong_pw",
"save_mime_headers",
"selfstatus",
"send_server",
"send_user",
"send_pw",
"send_port",
"send_security",
"server_flags",
"smtp_certificate_checks",
];
let t = TestContext::new().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)
&& !key.starts_with("configured")
&& !key.starts_with("sys.")
{
assert!(
info.contains_key(&*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

@@ -12,21 +12,24 @@ use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use async_std::{fs, io};
use anyhow::{bail, Error};
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_ELLIPSE, DC_OUTDATED_WARNING_DAYS};
use crate::context::Context;
use crate::error::{bail, Error};
use crate::events::EventType;
use crate::message::Message;
use crate::provider::get_provider_update_timestamp;
use crate::stock_str;
/// Shortens a string to a specified length and adds "[...]" to the
/// 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)
@@ -34,40 +37,15 @@ 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)
}
}
/// the colors must fulfill some criterions as:
/// - contrast to black and to white
/// - work as a text-color
/// - being noticeable on a typical map
/// - harmonize together while being different enough
/// (therefore, we cannot just use random rgb colors :)
const COLORS: [u32; 16] = [
0xe5_65_55, 0xf2_8c_48, 0x8e_85_ee, 0x76_c8_4d, 0x5b_b6_cc, 0x54_9c_dd, 0xd2_5c_99, 0xb3_78_00,
0xf2_30_30, 0x39_b2_49, 0xbb_24_3b, 0x96_40_78, 0x66_87_4f, 0x30_8a_b9, 0x12_7e_d0, 0xbe_45_0c,
];
#[allow(clippy::indexing_slicing)]
pub(crate) fn dc_str_to_color(s: impl AsRef<str>) -> u32 {
let str_lower = s.as_ref().to_lowercase();
let mut checksum = 0;
let bytes = str_lower.as_bytes();
for (i, byte) in bytes.iter().enumerate() {
checksum += (i + 1) * *byte as usize;
checksum %= 0x00ff_ffff;
}
let color_index = checksum % COLORS.len();
COLORS[color_index]
}
/* ******************************************************************************
* date/time tools
******************************************************************************/
@@ -151,6 +129,67 @@ pub(crate) async fn dc_create_smeared_timestamps(context: &Context, count: usize
start
}
// if the system time is not plausible, once a day, add a device message.
// for testing we're using time() as that is also used for message timestamps.
// moreover, add a warning if the app is outdated.
pub(crate) async fn maybe_add_time_based_warnings(context: &Context) {
if !maybe_warn_on_bad_time(context, time(), get_provider_update_timestamp()).await {
maybe_warn_on_outdated(context, time(), get_provider_update_timestamp()).await;
}
}
async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestamp: i64) -> bool {
if now < known_past_timestamp {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(
stock_str::bad_time_msg_body(
context,
Local
.timestamp(now, 0)
.format("%Y-%m-%d %H:%M:%S")
.to_string(),
)
.await,
);
add_device_msg_with_importance(
context,
Some(
format!(
"bad-time-warning-{}",
chrono::NaiveDateTime::from_timestamp(now, 0).format("%Y-%m-%d") // repeat every day
)
.as_str(),
),
Some(&mut msg),
true,
)
.await
.ok();
return true;
}
false
}
async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time: i64) {
if now > approx_compile_time + DC_OUTDATED_WARNING_DAYS * 24 * 60 * 60 {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_str::update_reminder_msg_body(context).await);
add_device_msg(
context,
Some(
format!(
"outdated-warning-{}",
chrono::NaiveDateTime::from_timestamp(now, 0).format("%Y-%m") // repeat every month
)
.as_str(),
),
Some(&mut msg),
)
.await
.ok();
}
}
/* Message-ID tools */
pub(crate) fn dc_create_id() -> String {
/* generate an id. the generated ID should be as short and as unique as possible:
@@ -465,34 +504,15 @@ pub fn dc_open_file_std<P: AsRef<std::path::Path>>(
}
}
pub(crate) async fn get_next_backup_path_old(
folder: impl AsRef<Path>,
backup_time: i64,
) -> Result<PathBuf, Error> {
let folder = PathBuf::from(folder.as_ref());
let stem = chrono::NaiveDateTime::from_timestamp(backup_time, 0)
.format("delta-chat-%Y-%m-%d")
.to_string();
// 64 backup files per day should be enough for everyone
for i in 0..64 {
let mut path = folder.clone();
path.push(format!("{}-{}.bak", stem, i));
if !path.exists().await {
return Ok(path);
}
}
bail!("could not create backup file, disk full?");
}
/// Returns Ok((temp_path, dest_path)) on success. The backup can then be written to temp_path. If the backup succeeded,
/// it can be renamed to dest_path. This guarantees that the backup is complete.
pub(crate) async fn get_next_backup_path_new(
pub(crate) async fn get_next_backup_path(
folder: impl AsRef<Path>,
backup_time: i64,
) -> 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();
@@ -612,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
@@ -637,13 +642,45 @@ pub(crate) fn improve_single_line_input(input: impl AsRef<str>) -> String {
.to_string()
}
pub(crate) trait IsNoneOrEmpty<T> {
fn is_none_or_empty(&self) -> bool;
}
impl<T> IsNoneOrEmpty<T> for Option<T>
where
T: AsRef<str>,
{
fn is_none_or_empty(&self) -> bool {
!matches!(self, Some(s) if !s.as_ref().is_empty())
}
}
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 {
use super::*;
use std::convert::TryInto;
#![allow(clippy::indexing_slicing)]
use crate::constants::*;
use crate::test_utils::*;
use super::*;
use crate::test_utils::TestContext;
#[test]
fn test_rust_ftoa() {
@@ -798,6 +835,9 @@ mod tests {
assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
}
use crate::chat;
use crate::chatlist::Chatlist;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use proptest::prelude::*;
proptest! {
@@ -829,7 +869,7 @@ mod tests {
#[async_std::test]
async fn test_file_handling() {
let t = TestContext::new().await;
let context = &t.ctx;
let context = &t;
macro_rules! dc_file_exist {
($ctx:expr, $fname:expr) => {
$ctx.get_blobdir()
@@ -839,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());
@@ -891,29 +921,15 @@ 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;
assert_ne!(
dc_create_smeared_timestamp(&t.ctx).await,
dc_create_smeared_timestamp(&t.ctx).await
dc_create_smeared_timestamp(&t).await,
dc_create_smeared_timestamp(&t).await
);
assert!(
dc_create_smeared_timestamp(&t.ctx).await
dc_create_smeared_timestamp(&t).await
>= SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
@@ -925,13 +941,13 @@ mod tests {
async fn test_create_smeared_timestamps() {
let t = TestContext::new().await;
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1;
let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
let next = dc_smeared_time(&t.ctx).await;
let start = dc_create_smeared_timestamps(&t, count as usize).await;
let next = dc_smeared_time(&t).await;
assert!((start + count - 1) < next);
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE + 30;
let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
let next = dc_smeared_time(&t.ctx).await;
let start = dc_create_smeared_timestamps(&t, count as usize).await;
let next = dc_smeared_time(&t).await;
assert!((start + count - 1) < next);
}
@@ -991,4 +1007,146 @@ mod tests {
assert_eq!(improve_single_line_input("Hi\naiae "), "Hi aiae");
assert_eq!(improve_single_line_input("\r\nahte\n\r"), "ahte");
}
#[async_std::test]
async fn test_maybe_warn_on_bad_time() {
let t = TestContext::new().await;
let timestamp_now = time();
let timestamp_future = timestamp_now + 60 * 60 * 24 * 7;
let timestamp_past = NaiveDateTime::new(
NaiveDate::from_ymd(2020, 9, 1),
NaiveTime::from_hms(0, 0, 0),
)
.timestamp_millis()
/ 1_000;
// a correct time must not add a device message
maybe_warn_on_bad_time(&t, timestamp_now, get_provider_update_timestamp()).await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
// we cannot find out if a date in the future is wrong - a device message is not added
maybe_warn_on_bad_time(&t, timestamp_future, get_provider_update_timestamp()).await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
// a date in the past must add a device message
maybe_warn_on_bad_time(&t, timestamp_past, get_provider_update_timestamp()).await;
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
.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
maybe_warn_on_bad_time(
&t,
timestamp_past + 60 * 60,
get_provider_update_timestamp(),
)
.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(
&t,
timestamp_past + 60 * 60 * 24 - 1,
get_provider_update_timestamp(),
)
.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
maybe_warn_on_bad_time(
&t,
timestamp_past + 60 * 60 * 24,
get_provider_update_timestamp(),
)
.await;
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
.unwrap();
assert_eq!(msgs.len(), 2);
}
#[async_std::test]
async fn test_maybe_warn_on_outdated() {
let t = TestContext::new().await;
let timestamp_now: i64 = time();
// in about 6 months, the app should not be outdated
// (if this fails, provider-db is not updated since 6 months)
maybe_warn_on_outdated(
&t,
timestamp_now + 180 * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
// in 1 year, the app should be considered as outdated
maybe_warn_on_outdated(
&t,
timestamp_now + 365 * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
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
.unwrap();
assert_eq!(msgs.len(), 1);
// do not repeat the warning every day ...
// (we test that for the 2 subsequent days, this may be the next month, so the result should be 1 or 2 device message)
maybe_warn_on_outdated(
&t,
timestamp_now + (365 + 1) * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
maybe_warn_on_outdated(
&t,
timestamp_now + (365 + 2) * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
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
.unwrap();
let test_len = msgs.len();
assert!(test_len == 1 || test_len == 2);
// ... but every month
// (forward generous 33 days to avoid being in the same month as in the previous check)
maybe_warn_on_outdated(
&t,
timestamp_now + (365 + 33) * 24 * 60 * 60,
get_provider_update_timestamp(),
)
.await;
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
.unwrap();
assert_eq!(msgs.len(), test_len + 1);
}
}

View File

@@ -2,20 +2,54 @@
//!
//! A module to remove HTML tags from the email text
use lazy_static::lazy_static;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use std::io::BufRead;
lazy_static! {
static ref LINE_RE: regex::Regex = regex::Regex::new(r"(\r?\n)+").unwrap();
}
use once_cell::sync::Lazy;
use quick_xml::{
events::{BytesEnd, BytesStart, BytesText},
Reader,
};
static LINE_RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
struct Dehtml {
strbuilder: String,
add_text: AddText,
last_href: Option<String>,
/// GMX wraps a quote in `<div name="quote">`. After a `<div name="quote">`, this count is
/// increased at each `<div>` and decreased at each `</div>`. This way we know when the quote ends.
/// If this is > `0`, then we are inside a `<div name="quote">`
divs_since_quote_div: u32,
/// 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,
}
#[derive(Debug, PartialEq)]
impl Dehtml {
fn line_prefix(&self) -> &str {
if self.divs_since_quoted_content_div > 0 || self.blockquotes_since_blockquote > 0 {
"> "
} else {
""
}
}
fn append_prefix(&self, line_end: impl AsRef<str>) -> String {
// line_end is e.g. "\n\n". We add "> " if necessary.
line_end.as_ref().to_owned() + self.line_prefix()
}
fn get_add_text(&self) -> AddText {
if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 {
AddText::No // Everything between <div name="quoted"> and <div name="quoted_content"> is metadata which we don't want
} else {
self.add_text
}
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
enum AddText {
No,
YesRemoveLineEnds,
@@ -24,25 +58,28 @@ enum AddText {
// dehtml() returns way too many newlines; however, an optimisation on this issue is not needed as
// the newlines are typically removed in further processing by the caller
pub fn dehtml(buf: &str) -> String {
pub fn dehtml(buf: &str) -> Option<String> {
let s = dehtml_quick_xml(buf);
if !s.trim().is_empty() {
return s;
return Some(s);
}
let s = dehtml_manually(buf);
if !s.trim().is_empty() {
return s;
return Some(s);
}
buf.to_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 {
strbuilder: String::with_capacity(buf.len()),
add_text: AddText::YesRemoveLineEnds,
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);
@@ -81,13 +118,16 @@ pub fn dehtml_quick_xml(buf: &str) -> String {
}
fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
if dehtml.add_text == AddText::YesPreserveLineEnds
|| dehtml.add_text == AddText::YesRemoveLineEnds
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
{
let last_added = escaper::decode_html_buf_sloppy(event.escaped()).unwrap_or_default();
if dehtml.add_text == AddText::YesRemoveLineEnds {
if dehtml.get_add_text() == AddText::YesRemoveLineEnds {
dehtml.strbuilder += LINE_RE.replace_all(&last_added, "\r").as_ref();
} else if !dehtml.line_prefix().is_empty() {
let l = dehtml.append_prefix("\n");
dehtml.strbuilder += LINE_RE.replace_all(&last_added, l.as_str()).as_ref();
} else {
dehtml.strbuilder += &last_added;
}
@@ -95,13 +135,16 @@ fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
}
fn dehtml_cdata_cb(event: &BytesText, dehtml: &mut Dehtml) {
if dehtml.add_text == AddText::YesPreserveLineEnds
|| dehtml.add_text == AddText::YesRemoveLineEnds
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
{
let last_added = escaper::decode_html_buf_sloppy(event.escaped()).unwrap_or_default();
if dehtml.add_text == AddText::YesRemoveLineEnds {
if dehtml.get_add_text() == AddText::YesRemoveLineEnds {
dehtml.strbuilder += LINE_RE.replace_all(&last_added, "\r").as_ref();
} else if !dehtml.line_prefix().is_empty() {
let l = dehtml.append_prefix("\n");
dehtml.strbuilder += LINE_RE.replace_all(&last_added, l.as_str()).as_ref();
} else {
dehtml.strbuilder += &last_added;
}
@@ -112,8 +155,15 @@ fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
match tag.as_str() {
"p" | "div" | "table" | "td" | "style" | "script" | "title" | "pre" => {
dehtml.strbuilder += "\n\n";
"p" | "table" | "td" | "style" | "script" | "title" | "pre" => {
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
dehtml.add_text = AddText::YesRemoveLineEnds;
}
"div" => {
pop_tag(&mut dehtml.divs_since_quote_div);
pop_tag(&mut dehtml.divs_since_quoted_content_div);
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
dehtml.add_text = AddText::YesRemoveLineEnds;
}
"a" => {
@@ -124,11 +174,16 @@ fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
}
}
"b" | "strong" => {
dehtml.strbuilder += "*";
if dehtml.get_add_text() != AddText::No {
dehtml.strbuilder += "*";
}
}
"i" | "em" => {
dehtml.strbuilder += "_";
if dehtml.get_add_text() != AddText::No {
dehtml.strbuilder += "_";
}
}
"blockquote" => pop_tag(&mut dehtml.blockquotes_since_blockquote),
_ => {}
}
}
@@ -141,19 +196,27 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase();
match tag.as_str() {
"p" | "div" | "table" | "td" => {
dehtml.strbuilder += "\n\n";
"p" | "table" | "td" => {
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
dehtml.add_text = AddText::YesRemoveLineEnds;
}
#[rustfmt::skip]
"div" => {
maybe_push_tag(event, reader, "quote", &mut dehtml.divs_since_quote_div);
maybe_push_tag(event, reader, "quoted-content", &mut dehtml.divs_since_quoted_content_div);
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
dehtml.add_text = AddText::YesRemoveLineEnds;
}
"br" => {
dehtml.strbuilder += "\n";
dehtml.strbuilder += &dehtml.append_prefix("\n");
dehtml.add_text = AddText::YesRemoveLineEnds;
}
"style" | "script" | "title" => {
dehtml.add_text = AddText::No;
}
"pre" => {
dehtml.strbuilder += "\n\n";
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
dehtml.add_text = AddText::YesPreserveLineEnds;
}
"a" => {
@@ -174,15 +237,52 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
}
}
"b" | "strong" => {
dehtml.strbuilder += "*";
if dehtml.get_add_text() != AddText::No {
dehtml.strbuilder += "*";
}
}
"i" | "em" => {
dehtml.strbuilder += "_";
if dehtml.get_add_text() != AddText::No {
dehtml.strbuilder += "_";
}
}
"blockquote" => dehtml.blockquotes_since_blockquote += 1,
_ => {}
}
}
/// In order to know when a specific tag is closed, we need to count the opening and closing tags.
/// The `counts`s are stored in the `Dehtml` struct.
fn pop_tag(count: &mut u32) {
if *count > 0 {
*count -= 1;
}
}
/// In order to know when a specific tag is closed, we need to count the opening and closing tags.
/// The `counts`s are stored in the `Dehtml` struct.
fn maybe_push_tag(
event: &BytesStart,
reader: &Reader<impl BufRead>,
tag_name: &str,
count: &mut u32,
) {
if *count > 0 || tag_contains_attr(event, reader, tag_name) {
*count += 1;
}
}
fn tag_contains_attr(event: &BytesStart, reader: &Reader<impl BufRead>, name: &str) -> bool {
event.attributes().any(|r| {
r.map(|a| {
a.unescape_and_decode_value(reader)
.map(|v| v == name)
.unwrap_or(false)
})
.unwrap_or(false)
})
}
pub fn dehtml_manually(buf: &str) -> String {
// Just strip out everything between "<" and ">"
let mut strbuilder = String::new();
@@ -214,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:
@@ -222,21 +323,23 @@ mod tests {
"<a href='https://get.delta.chat/'/>",
"[](https://get.delta.chat/)",
),
("", ""),
("<!doctype html>\n<b>fat text</b>", "*fat text*"),
// Invalid html (at least DC should show the text if the html is invalid):
("<!some invalid html code>\n<b>some text</b>", "some text"),
("<This text is in brackets>", "<This text is in brackets>"),
];
for (input, output) in cases {
assert_eq!(simplify(dehtml(input), true).0, output);
assert_eq!(simplify(dehtml(input).unwrap(), true).0, output);
}
let none_cases = vec!["<html> </html>", ""];
for input in none_cases {
assert_eq!(dehtml(input), None);
}
}
#[test]
fn test_dehtml_parse_br() {
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2<br/>line3\n\r";
let plain = dehtml(html);
let plain = dehtml(html).unwrap();
assert_eq!(plain, "line1\n\r\r\rline2\nline3");
}
@@ -244,7 +347,7 @@ mod tests {
#[test]
fn test_dehtml_parse_href() {
let html = "<a href=url>text</a";
let plain = dehtml(html);
let plain = dehtml(html).unwrap();
assert_eq!(plain, "[text](url)");
}
@@ -252,7 +355,7 @@ mod tests {
#[test]
fn test_dehtml_bold_text() {
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
let plain = dehtml(html);
let plain = dehtml(html).unwrap();
assert_eq!(plain, "text *bold*<>");
}
@@ -262,7 +365,7 @@ mod tests {
let html =
"&lt;&gt;&quot;&apos;&amp; &auml;&Auml;&ouml;&Ouml;&uuml;&Uuml;&szlig; foo&AElig;&ccedil;&Ccedil; &diams;&lrm;&rlm;&zwnj;&noent;&zwj;";
let plain = dehtml(html);
let plain = dehtml(html).unwrap();
assert_eq!(
plain,
@@ -285,7 +388,27 @@ mod tests {
</body>
</html>
"##;
let txt = dehtml(input);
let txt = dehtml(input).unwrap();
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");
let dehtml = dehtml(input).unwrap();
println!("{}", dehtml);
let (msg, forwarded, cut, top_quote, footer) = simplify(dehtml, false);
assert_eq!(msg, "Test");
assert_eq!(forwarded, false);
assert_eq!(cut, false);
assert_eq!(top_quote.as_deref(), Some("test"));
assert_eq!(footer, None);
}
}

View File

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

@@ -56,24 +56,30 @@
//! the database entries which are expired either according to their
//! ephemeral message timers or global `delete_server_after` setting.
use crate::chat::{lookup_by_contact_id, send_msg, ChatId};
use std::convert::{TryFrom, TryInto};
use std::num::ParseIntError;
use std::str::FromStr;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use anyhow::{ensure, Context as _, Error};
use async_std::task;
use serde::{Deserialize, Serialize};
use sqlx::Row;
use crate::constants::{
Viewtype, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF,
};
use crate::context::Context;
use crate::dc_tools::time;
use crate::error::{ensure, Error};
use crate::events::EventType;
use crate::message::{Message, MessageState, MsgId};
use crate::mimeparser::SystemMessage;
use crate::sql;
use crate::stock::StockMessage;
use async_std::task;
use serde::{Deserialize, Serialize};
use std::convert::{TryFrom, TryInto};
use std::num::ParseIntError;
use std::str::FromStr;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
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 {
@@ -118,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,
))))
}
}
}
@@ -148,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())
@@ -170,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?;
@@ -211,52 +232,87 @@ pub(crate) async fn stock_ephemeral_timer_changed(
timer: Timer,
from_id: u32,
) -> String {
let stock_message = match timer {
Timer::Disabled => StockMessage::MsgEphemeralTimerDisabled,
match timer {
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id as u32).await,
Timer::Enabled { duration } => match duration {
60 => StockMessage::MsgEphemeralTimerMinute,
3600 => StockMessage::MsgEphemeralTimerHour,
86400 => StockMessage::MsgEphemeralTimerDay,
604_800 => StockMessage::MsgEphemeralTimerWeek,
2_419_200 => StockMessage::MsgEphemeralTimerFourWeeks,
_ => StockMessage::MsgEphemeralTimerEnabled,
0..=59 => {
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 as u32).await,
61..=3599 => {
stock_str::msg_ephemeral_timer_minutes(
context,
format!("{}", (f64::from(duration) / 6.0).round() / 10.0),
from_id as u32,
)
.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 as u32,
)
.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 as u32,
)
.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 as u32,
)
.await
}
},
};
context
.stock_system_msg(stock_message, timer.to_string(), "", from_id)
.await
}
}
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;
@@ -277,18 +333,29 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result<bool, E
let mut updated = context
.sql
.execute(
"UPDATE msgs \
SET txt = 'DELETED', chat_id = ? \
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()
@@ -307,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;
}
@@ -343,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
{
@@ -406,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 < ?) \
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
@@ -440,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?;
@@ -459,12 +542,19 @@ 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::*;
use crate::test_utils::*;
use crate::test_utils::TestContext;
use crate::{
chat::{self, Chat, ChatItem},
dc_tools::IsNoneOrEmpty,
};
#[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,
@@ -472,24 +562,78 @@ mod tests {
);
assert_eq!(
stock_ephemeral_timer_changed(&context, Timer::Disabled, 0).await,
"Message deletion timer is disabled."
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 1 },
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1 s by me."
);
assert_eq!(
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 1 }, 0).await,
"Message deletion timer is set to 1 s."
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 30 },
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 30 s by me."
);
assert_eq!(
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 30 }, 0).await,
"Message deletion timer is set to 30 s."
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 60 },
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1 minute by me."
);
assert_eq!(
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 }, 0).await,
"Message deletion timer is set to 1 minute."
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 90 },
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1.5 minutes by me."
);
assert_eq!(
stock_ephemeral_timer_changed(&context, Timer::Enabled { duration: 60 * 60 }, 0).await,
"Message deletion timer is set to 1 hour."
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 30 * 60 },
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 30 minutes by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 60 * 60 },
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1 hour by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled { duration: 5400 },
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1.5 hours by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled {
duration: 2 * 60 * 60
},
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 2 hours by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
@@ -497,10 +641,21 @@ mod tests {
Timer::Enabled {
duration: 24 * 60 * 60
},
0
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1 day."
"Message deletion timer is set to 1 day by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
&context,
Timer::Enabled {
duration: 2 * 24 * 60 * 60
},
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 2 days by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
@@ -508,10 +663,10 @@ mod tests {
Timer::Enabled {
duration: 7 * 24 * 60 * 60
},
0
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 1 week."
"Message deletion timer is set to 1 week by me."
);
assert_eq!(
stock_ephemeral_timer_changed(
@@ -519,10 +674,144 @@ mod tests {
Timer::Enabled {
duration: 4 * 7 * 24 * 60 * 60
},
0
DC_CONTACT_ID_SELF
)
.await,
"Message deletion timer is set to 4 weeks."
"Message deletion timer is set to 4 weeks by me."
);
}
#[async_std::test]
async fn test_ephemeral_timer() -> anyhow::Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_alice = alice.create_chat(&bob).await.id;
let chat_bob = bob.create_chat(&alice).await.id;
// Alice sends message to Bob
let mut msg = Message::new(Viewtype::Text);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
bob.recv_msg(&sent).await;
// Alice sends second message to Bob, with no timer
let mut msg = Message::new(Viewtype::Text);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
assert_eq!(
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
Timer::Disabled
);
// Bob sets ephemeral timer and sends a message about timer change
chat_bob
.set_ephemeral_timer(&bob.ctx, Timer::Enabled { duration: 60 })
.await?;
let sent_timer_change = bob.pop_sent_msg().await;
assert_eq!(
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
Timer::Enabled { duration: 60 }
);
// Bob receives message from Alice.
// Alice message has no timer. However, Bob should not disable timer,
// because Alice replies to old message.
bob.recv_msg(&sent).await;
assert_eq!(
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
Timer::Disabled
);
assert_eq!(
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
Timer::Enabled { duration: 60 }
);
// Alice receives message from Bob
alice.recv_msg(&sent_timer_change).await;
assert_eq!(
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
Timer::Enabled { duration: 60 }
);
Ok(())
}
#[async_std::test]
async fn test_ephemeral_delete_msgs() {
let t = TestContext::new_alice().await;
let chat = t.get_self_chat().await;
t.send_text(chat.id, "Saved message, which we delete manually")
.await;
let msg = t.get_last_msg_in(chat.id).await;
msg.id.delete_from_db(&t).await.unwrap();
check_msg_was_deleted(&t, &chat, msg.id).await;
chat.id
.set_ephemeral_timer(&t, Timer::Enabled { duration: 1 })
.await
.unwrap();
let msg = t
.send_text(chat.id, "Saved message, disappearing after 1s")
.await;
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.unwrap();
// Check that the chat is empty except for possibly info messages:
for item in &chat_items {
if let ChatItem::Message { msg_id } = item {
let msg = Message::load_from_db(t, *msg_id).await.unwrap();
assert!(msg.is_info())
}
}
// 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 {
assert_eq!(msg.from_id, 0);
assert_eq!(msg.to_id, 0);
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
let rawtxt: Option<String> = t
.sql
.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,6 +1,8 @@
//! # Error handling
pub use anyhow::{bail, ensure, format_err, Error, Result};
#[derive(Debug, thiserror::Error)]
#[error("Out of Range")]
pub struct OutOfRangeError;
#[macro_export]
macro_rules! ensure_eq {

View File

@@ -2,8 +2,8 @@
use std::ops::Deref;
use async_std::channel::{self, Receiver, Sender, TrySendError};
use async_std::path::PathBuf;
use async_std::sync::{channel, Receiver, Sender, TrySendError};
use strum::EnumProperty;
use crate::chat::ChatId;
@@ -18,7 +18,7 @@ pub struct Events {
impl Default for Events {
fn default() -> Self {
let (sender, receiver) = channel(1_000);
let (sender, receiver) = channel::bounded(1_000);
Self { receiver, sender }
}
@@ -35,7 +35,7 @@ impl Events {
// try again
self.emit(event);
}
Err(TrySendError::Disconnected(_)) => {
Err(TrySendError::Closed(_)) => {
unreachable!("unable to emit event, channel disconnected");
}
}
@@ -47,6 +47,17 @@ impl Events {
}
}
/// A receiver of events from a [`Context`].
///
/// See [`Context::get_event_emitter`] to create an instance. If multiple instances are
/// created events emitted by the [`Context`] will only be delivered to one of the
/// `EventEmitter`s.
///
/// The `EventEmitter` is also a [`Stream`], so a typical usage is in a `while let` loop.
///
/// [`Context`]: crate::context::Context
/// [`Context::get_event_emitter`]: crate::context::Context::get_event_emitter
/// [`Stream`]: async_std::stream::Stream
#[derive(Debug, Clone)]
pub struct EventEmitter(Receiver<Event>);
@@ -58,18 +69,42 @@ impl EventEmitter {
/// Async recv of an event. Return `None` if the `Sender` has been droped.
pub async fn recv(&self) -> Option<Event> {
// TODO: change once we can use async channels internally.
self.0.recv().await.ok()
}
pub fn try_recv(&self) -> Result<Event, async_std::sync::TryRecvError> {
self.0.try_recv()
}
}
impl async_std::stream::Stream for EventEmitter {
type Item = Event;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
std::pin::Pin::new(&mut self.0).poll_next(cx)
}
}
/// The event emitted by a [`Context`] from an [`EventEmitter`].
///
/// Events are documented on the C/FFI API in `deltachat.h` as `DC_EVENT_*` contants. The
/// context emits them in relation to various operations happening, a lot of these are again
/// documented in `deltachat.h`.
///
/// This struct [`Deref`]s to the [`EventType`].
///
/// [`Context`]: crate::context::Context
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Event {
/// The ID of the [`Context`] which emitted this event.
///
/// This allows using multiple [`Context`]s in a single process as they are identified
/// by this ID.
///
/// [`Context`]: crate::context::Context
pub id: u32,
/// The event payload.
///
/// These are documented in `deltachat.h` as the `DC_EVENT_*` constants.
pub typ: EventType,
}
@@ -82,7 +117,9 @@ impl Deref for Event {
}
impl EventType {
/// Returns the corresponding Event id.
/// Returns the corresponding Event ID.
///
/// These are the IDs used in the `DC_EVENT_*` constants in `deltachat.h`.
pub fn as_id(&self) -> i32 {
self.get_str("id")
.expect("missing id")
@@ -94,8 +131,9 @@ impl EventType {
#[derive(Debug, Clone, PartialEq, Eq, EnumProperty)]
pub enum EventType {
/// The library-user may write an informational string to the log.
/// Passed to the callback given to dc_context_new().
/// This event should not be reported to the end-user using a popup or something like that.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
#[strum(props(id = "100"))]
Info(String),
@@ -119,10 +157,6 @@ pub enum EventType {
#[strum(props(id = "105"))]
ImapMessageMoved(String),
/// Emitted when an IMAP folder was emptied
#[strum(props(id = "106"))]
ImapFolderEmptied(String),
/// Emitted when an new file in the $BLOBDIR was created
#[strum(props(id = "150"))]
NewBlobFile(String),
@@ -132,14 +166,13 @@ pub enum EventType {
DeletedBlobFile(String),
/// The library-user should write a warning string to the log.
/// Passed to the callback given to dc_context_new().
///
/// This event should not be reported to the end-user using a popup or something like that.
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
#[strum(props(id = "300"))]
Warning(String),
/// The library-user should report an error to the end-user.
/// Passed to the callback given to dc_context_new().
///
/// As most things are asynchronous, things may go wrong at any time and the user
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.
@@ -190,6 +223,11 @@ pub enum EventType {
#[strum(props(id = "2005"))]
IncomingMsg { chat_id: ChatId, msg_id: MsgId },
/// Messages were seen or noticed.
/// chat id is always set.
#[strum(props(id = "2008"))]
MsgsNoticed(ChatId),
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
#[strum(props(id = "2010"))]
@@ -237,10 +275,16 @@ pub enum EventType {
LocationChanged(Option<u32>),
/// Inform about the configuration progress started by configure().
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
#[strum(props(id = "2041"))]
ConfigureProgress(usize),
ConfigureProgress {
/// Progress.
///
/// 0=error, 1-999=progress in permille, 1000=success and done
progress: usize,
/// Progress comment or error, something to display to the user.
comment: Option<String>,
},
/// Inform about the import/export progress started by imex().
///

View File

@@ -21,21 +21,28 @@
/// length. However, this should be rare and should not result in
/// immediate mail rejection: SMTP (RFC 2821) limit is 998 characters,
/// and Spam Assassin limit is 78 characters.
fn format_line_flowed(line: &str) -> String {
fn format_line_flowed(line: &str, prefix: &str) -> String {
let mut result = String::new();
let mut buffer = String::new();
let mut buffer = prefix.to_string();
let mut after_space = false;
for c in line.chars() {
if c == ' ' {
buffer.push(c);
after_space = true;
} else if c == '>' {
if buffer.is_empty() {
// Space stuffing, see RFC 3676
buffer.push(' ');
}
buffer.push(c);
after_space = false;
} else {
if after_space && buffer.len() >= 72 && !c.is_whitespace() && c != '>' {
if after_space && buffer.len() >= 72 && !c.is_whitespace() {
// Flush the buffer and insert soft break (SP CRLF).
result += &buffer;
result += "\r\n";
buffer = String::new();
buffer = prefix.to_string();
}
buffer.push(c);
after_space = false;
@@ -44,6 +51,28 @@ fn format_line_flowed(line: &str) -> String {
result + &buffer
}
fn format_flowed_prefix(text: &str, prefix: &str) -> String {
let mut result = String::new();
for line in text.split('\n') {
if !result.is_empty() {
result += "\r\n";
}
let line = line.trim_end();
if prefix.len() + line.len() > 78 {
result += &format_line_flowed(line, prefix);
} else {
result += prefix;
if prefix.is_empty() && line.starts_with('>') {
// Space stuffing, see RFC 3676
result.push(' ');
}
result += line;
}
}
result
}
/// Returns text formatted according to RFC 3767 (format=flowed).
///
/// This function accepts text separated by LF, but returns text
@@ -52,20 +81,12 @@ fn format_line_flowed(line: &str) -> String {
/// RFC 2646 technique is used to insert soft line breaks, so DelSp
/// SHOULD be set to "no" when sending.
pub fn format_flowed(text: &str) -> String {
let mut result = String::new();
format_flowed_prefix(text, "")
}
for line in text.split('\n') {
if !result.is_empty() {
result += "\r\n";
}
let line = line.trim_end();
if line.len() > 78 {
result += &format_line_flowed(line);
} else {
result += line;
}
}
result
/// Same as format_flowed(), but adds "> " prefix to each line.
pub fn format_flowed_quote(text: &str) -> String {
format_flowed_prefix(text, "> ")
}
/// Joins lines in format=flowed text.
@@ -122,6 +143,20 @@ mod tests {
To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
client and enter the setup code presented on the generating device.";
assert_eq!(format_flowed(text), expected);
let text = "> Not a quote";
assert_eq!(format_flowed(text), " > Not a quote");
// Test 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]
@@ -133,4 +168,21 @@ mod tests {
unwrapped on the receiver";
assert_eq!(unformat_flowed(text, false), expected);
}
#[test]
fn test_format_flowed_quote() {
let quote = "this is a quoted line";
let expected = "> this is a quoted line";
assert_eq!(format_flowed_quote(quote), expected);
let quote = "> foo bar baz";
let expected = "> > foo bar baz";
assert_eq!(format_flowed_quote(quote), expected);
let quote = "this is a very long quote that should be wrapped using format=flowed and unwrapped on the receiver";
let expected =
"> this is a very long quote that should be wrapped using format=flowed and \r\n\
> unwrapped on the receiver";
assert_eq!(format_flowed_quote(quote), expected);
}
}

View File

@@ -3,7 +3,6 @@ use mailparse::{MailHeader, MailHeaderMap};
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
#[strum(serialize_all = "kebab_case")]
#[allow(dead_code)]
pub enum HeaderDef {
MessageId,
Subject,
@@ -12,16 +11,26 @@ 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,
Precedence,
ContentType,
ContentId,
ChatVersion,
ChatGroupId,
ChatGroupName,
@@ -43,7 +52,9 @@ pub enum HeaderDef {
SecureJoinFingerprint,
SecureJoinInvitenumber,
SecureJoinAuth,
Sender,
EphemeralTimer,
Received,
_TestHeader,
}

553
src/html.rs Normal file
View File

@@ -0,0 +1,553 @@
///! # Get message as HTML.
///!
///! Use `Message.has_html()` to check if the UI shall render a
///! corresponding button and `MsgId.get_html()` to get the full message.
///!
///! Even when the original mime-message is not HTML,
///! `MsgId.get_html()` will return HTML -
///! this allows nice quoting, handling linebreaks properly etc.
use futures::future::FutureExt;
use std::future::Future;
use std::pin::Pin;
use anyhow::Result;
use lettre_email::mime::{self, Mime};
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;
impl Message {
/// Check if the message can be retrieved as HTML.
/// Typically, this is the case, when the mime structure of a Message is modified,
/// meaning that some text is cut or the original message
/// is in HTML and `simplify()` may hide some maybe important information.
/// The corresponding ffi-function is `dc_msg_has_html()`.
/// To get the HTML-code of the message, use `MsgId.get_html()`.
pub fn has_html(&self) -> bool {
self.mime_modified
}
/// Set HTML-part part of a message that is about to be sent.
/// The HTML-part is written to the database before sending and
/// used as the `text/html` part in the MIME-structure.
///
/// Received HTML parts are handled differently,
/// they are saved together with the whole MIME-structure
/// in `mime_headers` and the HTML-part is extracted using `MsgId::get_html()`.
/// (To underline this asynchronicity, we are using the wording "SendHtml")
pub fn set_html(&mut self, html: Option<String>) {
if let Some(html) = html {
self.param.set(SendHtml, html);
self.mime_modified = true;
} else {
self.param.remove(SendHtml);
self.mime_modified = false;
}
}
}
/// Type defining a rough mime-type.
/// This is mainly useful on iterating
/// to decide whether a mime-part has subtypes.
enum MimeMultipartType {
Multiple,
Single,
Message,
}
/// Function takes a content type from a ParsedMail structure
/// and checks and returns the rough mime-type.
async fn get_mime_multipart_type(ctype: &ParsedContentType) -> MimeMultipartType {
let mimetype = ctype.mimetype.to_lowercase();
if mimetype.starts_with("multipart") && ctype.params.get("boundary").is_some() {
MimeMultipartType::Multiple
} else if mimetype == "message/rfc822" {
MimeMultipartType::Message
} else {
MimeMultipartType::Single
}
}
/// HtmlMsgParser converts a mime-message to HTML.
#[derive(Debug)]
struct HtmlMsgParser {
pub html: String,
pub plain: Option<PlainText>,
}
impl HtmlMsgParser {
/// Function takes a raw mime-message string,
/// searches for the main-text part
/// and returns that as parser.html
pub async fn from_bytes(context: &Context, rawmime: &[u8]) -> Result<Self> {
let mut parser = HtmlMsgParser {
html: "".to_string(),
plain: None,
};
let parsedmail = mailparse::parse_mail(rawmime)?;
parser.collect_texts_recursive(context, &parsedmail).await?;
if parser.html.is_empty() {
if let Some(plain) = &parser.plain {
parser.html = plain.to_html().await;
}
} else {
parser.cid_to_data_recursive(context, &parsedmail).await?;
}
Ok(parser)
}
/// Function iterates over all mime-parts
/// and searches for text/plain and text/html parts and saves the
/// first one found.
/// in the corresponding structure fields.
///
/// 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,
mail: &'a mailparse::ParsedMail<'a>,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
// Boxed future to deal with recursion
async move {
match get_mime_multipart_type(&mail.ctype).await {
MimeMultipartType::Multiple => {
for cur_data in mail.subparts.iter() {
self.collect_texts_recursive(context, cur_data).await?
}
Ok(())
}
MimeMultipartType::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(());
}
let mail = mailparse::parse_mail(&raw).unwrap();
self.collect_texts_recursive(context, &mail).await
}
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype == mime::TEXT_HTML {
if self.html.is_empty() {
if let Ok(decoded_data) = mail.get_body() {
self.html = decoded_data;
}
}
} 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,
flowed: if let Some(format) = mail.ctype.params.get("format") {
format.as_str().to_ascii_lowercase() == "flowed"
} else {
false
},
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().to_ascii_lowercase() == "yes"
} else {
false
},
});
}
}
Ok(())
}
}
}
.boxed()
}
/// Replace cid:-protocol by the data:-protocol where appropriate.
/// This allows the final html-file to be self-contained.
fn cid_to_data_recursive<'a>(
&'a mut self,
context: &'a Context,
mail: &'a mailparse::ParsedMail<'a>,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
// Boxed future to deal with recursion
async move {
match get_mime_multipart_type(&mail.ctype).await {
MimeMultipartType::Multiple => {
for cur_data in mail.subparts.iter() {
self.cid_to_data_recursive(context, cur_data).await?;
}
Ok(())
}
MimeMultipartType::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(());
}
let mail = mailparse::parse_mail(&raw).unwrap();
self.cid_to_data_recursive(context, &mail).await
}
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
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 {
let re_string = format!(
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
regex::escape(&cid)
);
match regex::Regex::new(&re_string) {
Ok(re) => {
self.html = re
.replace_all(
&*self.html,
format!("${{1}}{}${{3}}", replacement).as_str(),
)
.as_ref()
.to_string()
}
Err(e) => warn!(
context,
"Cannot create regex for cid: {} throws {}",
re_string,
e
),
}
}
}
}
}
Ok(())
}
}
}
.boxed()
}
}
/// Convert a mime part to a data: url as defined in [RFC 2397](https://tools.ietf.org/html/rfc2397).
async fn mimepart_to_data_url(mail: &mailparse::ParsedMail<'_>) -> Result<String> {
let data = mail.get_body_raw()?;
let data = base64::encode(&data);
Ok(format!("data:{};base64,{}", mail.ctype.mimetype, data))
}
impl MsgId {
/// Get HTML from a message-id.
/// This requires `mime_headers` field to be set for the message;
/// 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) -> Result<Option<String>> {
let rawmime = message::get_mime_headers(context, self).await?;
if !rawmime.is_empty() {
match HtmlMsgParser::from_bytes(context, rawmime.as_bytes()).await {
Err(err) => {
warn!(context, "get_html: parser error: {}", err);
Ok(None)
}
Ok(parser) => Ok(Some(parser.html)),
}
} else {
warn!(context, "get_html: no mime for {}", self);
Ok(None)
}
}
}
/// Wraps HTML text into a new text/html mimepart structure.
///
/// Used on forwarding messages to avoid leaking the original mime structure
/// and also to avoid sending too much, maybe large data.
pub async fn new_html_mimepart(html: String) -> PartBuilder {
PartBuilder::new()
.content_type(&"text/html; charset=utf-8".parse::<mime::Mime>().unwrap())
.body(html)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat;
use crate::chat::forward_msgs;
use crate::config::Config;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::dc_receive_imf::dc_receive_imf;
use crate::message::MessengerMessage;
use crate::test_utils::TestContext;
#[async_std::test]
async fn test_htmlparse_plain_unspecified() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_unspecified.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r##"<!DOCTYPE html>
<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>
This message does not have Content-Type nor Subject.<br/>
<br/>
</body></html>
"##
);
}
#[async_std::test]
async fn test_htmlparse_plain_iso88591() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_iso88591.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r##"<!DOCTYPE html>
<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>
message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
<br/>
</body></html>
"##
);
}
#[async_std::test]
async fn test_htmlparse_plain_flowed() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_flowed.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert!(parser.plain.unwrap().flowed);
assert_eq!(
parser.html,
r##"<!DOCTYPE html>
<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>
This line ends with a space and will be merged with the next one due to format=flowed.<br/>
<br/>
This line does not end with a space<br/>
and will be wrapped as usual.<br/>
<br/>
</body></html>
"##
);
}
#[async_std::test]
async fn test_htmlparse_alt_plain() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_plain.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r##"<!DOCTYPE html>
<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>
mime-modified should not be set set as there is no html and no special stuff;<br/>
although not being a delta-message.<br/>
test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x27; :)<br/>
<br/>
<br/>
</body></html>
"##
);
}
#[async_std::test]
async fn test_htmlparse_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_html.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
// on windows, `\r\n` linends are returned from mimeparser,
// however, rust multiline-strings use just `\n`;
// therefore, we just remove `\r` before comparison.
assert_eq!(
parser.html.replace("\r", ""),
r##"
<html>
<p>mime-modified <b>set</b>; simplify is always regarded as lossy.</p>
</html>"##
);
}
#[async_std::test]
async fn test_htmlparse_alt_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_html.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html.replace("\r", ""), // see comment in test_htmlparse_html()
r##"<html>
<p>mime-modified <b>set</b>; simplify is always regarded as lossy.</p>
</html>
"##
);
}
#[async_std::test]
async fn test_htmlparse_alt_plain_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html.replace("\r", ""), // see comment in test_htmlparse_html()
r##"<html>
<p>
this is <b>html</b>
</p>
</html>
"##
);
}
#[async_std::test]
async fn test_htmlparse_apple_cid_jpg() {
// load raw mime html-data with related image-part (cid:)
// and make sure, Content-Id has angle-brackets that are removed correctly.
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.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.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.unwrap().is_none())
}
#[async_std::test]
async fn test_html_forwarding() {
// alice receives a non-delta html-message
let alice = TestContext::new_alice().await;
alice.set_config(Config::ShowEmails, Some("2")).await.ok();
let chat = alice
.create_chat_with_contact("", "sender@testrun.org")
.await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
dc_receive_imf(&alice, raw, "INBOX", 1, false)
.await
.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
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().contains("this is plain"));
assert!(msg.has_html());
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;
forward_msgs(&alice, &[msg.get_id()], chat.get_id())
.await
.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
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().contains("this is plain"));
assert!(msg.has_html());
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;
let chat = bob.create_chat_with_contact("", "alice@example.com").await;
bob.recv_msg(&alice.pop_sent_msg().await).await;
let msg = bob.get_last_msg_in(chat.get_id()).await;
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().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
}
#[async_std::test]
async fn test_html_forwarding_encrypted() {
// Alice receives a non-delta html-message
// (`ShowEmails=1` lets Alice actually receive non-delta messages for known contacts,
// the contact is marked as known by creating a chat using `chat_with_contact()`)
let alice = TestContext::new_alice().await;
alice.set_config(Config::ShowEmails, Some("1")).await.ok();
let chat = alice
.create_chat_with_contact("", "sender@testrun.org")
.await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
dc_receive_imf(&alice, raw, "INBOX", 1, false)
.await
.unwrap();
let msg = alice.get_last_msg_in(chat.get_id()).await;
// forward the message to saved-messages,
// this will encrypt the message as new_alice() has set up keys
let chat = alice.get_self_chat().await;
forward_msgs(&alice, &[msg.get_id()], chat.get_id())
.await
.unwrap();
let msg = alice.pop_sent_msg().await;
// receive the message on another device
let alice = TestContext::new_alice().await;
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;
assert_eq!(msg.get_from_id(), DC_CONTACT_ID_SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.get_showpadlock());
assert!(msg.is_forwarded());
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
}
#[async_std::test]
async fn test_set_html() {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
// alice sends a message with html-part to bob
let chat_id = alice.create_chat(&bob).await.id;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("plain text".to_string()));
msg.set_html(Some("<b>html</b> text".to_string()));
assert!(msg.mime_modified);
chat::send_msg(&alice, chat_id, &mut msg).await.unwrap();
// check the message is written correctly to alice's db
let msg = alice.get_last_msg_in(chat_id).await;
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().unwrap();
assert!(html.contains("<b>html</b> text"));
// let bob receive the message
let chat_id = bob.create_chat(&alice).await.id;
bob.recv_msg(&alice.pop_sent_msg().await).await;
let msg = bob.get_last_msg_in(chat_id).await;
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().unwrap();
assert!(html.contains("<b>html</b> text"));
}
}

View File

@@ -1,11 +1,11 @@
use super::Imap;
use anyhow::{bail, format_err, Result};
use async_imap::extensions::idle::IdleResponse;
use async_imap::types::UnsolicitedResponse;
use async_std::prelude::*;
use std::time::{Duration, SystemTime};
use crate::error::{bail, format_err, Result};
use crate::{context::Context, scheduler::InterruptInfo};
use super::session::Session;
@@ -25,7 +25,7 @@ impl Imap {
if !self.can_idle() {
bail!("IMAP server does not have IDLE capability");
}
self.setup_handle_if_needed(context).await?;
self.setup_handle(context).await?;
self.select_folder(context, watch_folder.clone()).await?;
@@ -50,6 +50,12 @@ impl Imap {
return Ok(info);
}
if let Ok(info) = self.idle_interrupt.try_recv() {
info!(context, "skip idle, got interrupt {:?}", info);
self.session = Some(session);
return Ok(info);
}
let mut handle = session.idle();
if let Err(err) = handle.init().await {
bail!("IMAP IDLE protocol failed to init/complete: {}", err);
@@ -62,14 +68,18 @@ impl Imap {
Interrupt(InterruptInfo),
}
info!(context, "Idle entering wait-on-remote state");
info!(
context,
"{}: Idle entering wait-on-remote state",
watch_folder.as_deref().unwrap_or("None")
);
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
let probe_network = self.idle_interrupt.recv().await;
let info = self.idle_interrupt.recv().await;
// cancel imap idle connection properly
drop(interrupt);
Ok(Event::Interrupt(probe_network.unwrap_or_default()))
Ok(Event::Interrupt(info.unwrap_or_default()))
});
match fut.await {
@@ -113,12 +123,15 @@ impl Imap {
// in this case, we're waiting for a configure job (and an interrupt).
let fake_idle_start_time = SystemTime::now();
info!(context, "IMAP-fake-IDLEing...");
// Do not poll, just wait for an interrupt when no folder is passed in.
if watch_folder.is_none() {
let watch_folder = if let Some(watch_folder) = watch_folder {
watch_folder
} else {
info!(context, "IMAP-fake-IDLE: no folder, waiting for interrupt");
return self.idle_interrupt.recv().await.unwrap_or_default();
}
};
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
// check every minute if there are new messages
// TODO: grow sleep durations / make them more flexible
@@ -159,19 +172,17 @@ impl Imap {
// will have already fetched the messages so perform_*_fetch
// will not find any new.
if let Some(ref watch_folder) = watch_folder {
match self.fetch_new_messages(context, watch_folder).await {
Ok(res) => {
info!(context, "fetch_new_messages returned {:?}", res);
if res {
break InterruptInfo::new(false, None);
}
}
Err(err) => {
error!(context, "could not fetch from folder: {}", err);
self.trigger_reconnect()
match self.fetch_new_messages(context, &watch_folder, false).await {
Ok(res) => {
info!(context, "fetch_new_messages returned {:?}", res);
if res {
break InterruptInfo::new(false, None);
}
}
Err(err) => {
error!(context, "could not fetch from folder: {:#}", err);
self.trigger_reconnect()
}
}
}
Event::Interrupt(info) => {

File diff suppressed because it is too large Load Diff

99
src/imap/scan_folders.rs Normal file
View File

@@ -0,0 +1,99 @@
use std::time::Instant;
use anyhow::{Context as _, Result};
use crate::config::Config;
use crate::context::Context;
use crate::imap::Imap;
use async_std::prelude::*;
use super::{get_folder_meaning, get_folder_meaning_by_name, FolderMeaning};
impl Imap {
pub async fn scan_folders(&mut self, context: &Context) -> Result<()> {
// First of all, debounce to once per minute:
let mut last_scan = context.last_full_folder_scan.lock().await;
if let Some(last_scan) = *last_scan {
let elapsed_secs = last_scan.elapsed().as_secs();
let debounce_secs = context
.get_config_u64(Config::ScanAllFoldersDebounceSecs)
.await?;
if elapsed_secs < debounce_secs {
return Ok(());
}
}
info!(context, "Starting full folder scan");
self.setup_handle(context).await?;
let session = self.session.as_mut();
let session = session.context("scan_folders(): IMAP No Connection established")?;
let folders: Vec<_> = session.list(Some(""), Some("*")).await?.collect().await;
let watched_folders = get_watched_folders(context).await;
let mut sentbox_folder = None;
let mut spam_folder = None;
for folder in folders {
let folder = match folder {
Ok(f) => f,
Err(e) => {
warn!(context, "Can't get folder: {}", e);
continue;
}
};
let foldername = folder.name();
let folder_meaning = get_folder_meaning(&folder);
let folder_name_meaning = get_folder_meaning_by_name(foldername);
if folder_meaning == FolderMeaning::SentObjects {
// Always takes precedent
sentbox_folder = Some(folder.name().to_string());
} else if folder_meaning == FolderMeaning::Spam {
spam_folder = Some(folder.name().to_string());
} else if folder_name_meaning == FolderMeaning::SentObjects {
// only set iff none has been already set
if sentbox_folder.is_none() {
sentbox_folder = Some(folder.name().to_string());
}
} else if folder_name_meaning == FolderMeaning::Spam && spam_folder.is_none() {
spam_folder = Some(folder.name().to_string());
}
// 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);
}
}
}
context
.set_config(Config::ConfiguredSentboxFolder, sentbox_folder.as_deref())
.await?;
context
.set_config(Config::ConfiguredSpamFolder, spam_folder.as_deref())
.await?;
last_scan.replace(Instant::now());
Ok(())
}
}
async fn get_watched_folders(context: &Context) -> Vec<String> {
let mut res = Vec::new();
let folder_watched_configured = &[
(Config::SentboxWatch, Config::ConfiguredSentboxFolder),
(Config::MvboxWatch, Config::ConfiguredMvboxFolder),
(Config::InboxWatch, Config::ConfiguredInboxFolder),
];
for (watched, configured) in folder_watched_configured {
if context.get_config_bool(*watched).await.unwrap_or_default() {
if let Ok(Some(folder)) = context.get_config(*configured).await {
res.push(folder);
}
}
}
res
}

View File

@@ -61,11 +61,12 @@ impl Imap {
/// select a folder, possibly update uid_validity and, if needed,
/// expunge the folder to remove delete-marked messages.
/// Returns whether a new folder was selected.
pub(super) async fn select_folder<S: AsRef<str>>(
&mut self,
context: &Context,
folder: Option<S>,
) -> Result<()> {
) -> Result<NewlySelected> {
if self.session.is_none() {
self.config.selected_folder = None;
self.config.selected_folder_needs_expunge = false;
@@ -78,7 +79,7 @@ impl Imap {
if let Some(ref folder) = folder {
if let Some(ref selected_folder) = self.config.selected_folder {
if folder.as_ref() == selected_folder {
return Ok(());
return Ok(NewlySelected::No);
}
}
}
@@ -99,7 +100,7 @@ impl Imap {
Ok(mailbox) => {
self.config.selected_folder = Some(folder.as_ref().to_string());
self.config.selected_mailbox = Some(mailbox);
Ok(())
Ok(NewlySelected::Yes)
}
Err(async_imap::error::Error::ConnectionLost) => {
self.trigger_reconnect();
@@ -119,7 +120,15 @@ impl Imap {
Err(Error::NoSession)
}
} else {
Ok(())
Ok(NewlySelected::No)
}
}
}
#[derive(PartialEq, Debug, Copy, Clone, Eq)]
pub(super) enum NewlySelected {
/// The folder was newly selected during this call to select_folder().
Yes,
/// No SELECT command was run because the folder already was selected
/// and self.config.selected_mailbox was not updated (so, e.g. it may contain an outdated uid_next)
No,
}

View File

@@ -1,35 +1,37 @@
//! # Import/export module
use std::any::Any;
use std::{
cmp::{max, min},
ffi::OsStr,
};
use std::ffi::OsStr;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use async_std::path::{Path, PathBuf};
use async_std::{
fs::{self, File},
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;
use crate::constants::*;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::context::Context;
use crate::dc_tools::*;
use crate::dc_tools::{
dc_copy_file, dc_create_folder, dc_delete_file, dc_delete_files_in_dir, dc_get_filesuffix_lc,
dc_open_file_std, dc_read_file, dc_write_file, get_next_backup_path, time, EmailAddress,
};
use crate::e2ee;
use crate::error::*;
use crate::events::EventType;
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::*;
use crate::param::Param;
use crate::pgp;
use crate::sql::{self, Sql};
use crate::stock::StockMessage;
use crate::stock_str;
use crate::{blob::BlobObject, log::LogExt};
use ::pgp::types::KeyTrait;
use async_tar::Archive;
// Name of the database file in the backup.
@@ -37,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`
@@ -77,11 +79,7 @@ pub enum ImexMode {
///
/// Only one import-/export-progress can run at the same time.
/// To cancel an import-/export-progress, drop the future returned by this function.
pub async fn imex(
context: &Context,
what: ImexMode,
param1: Option<impl AsRef<Path>>,
) -> Result<()> {
pub async fn imex(context: &Context, what: ImexMode, param1: impl AsRef<Path>) -> Result<()> {
let cancel = context.alloc_ongoing().await?;
let res = async {
@@ -94,7 +92,8 @@ pub async fn imex(
}
Err(err) => {
cleanup_aborted_imex(context, what).await;
error!(context, "{}", err);
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
error!(context, "{:#}", err);
context.emit_event(EventType::ImexProgress(0));
bail!("IMEX FAILED to complete: {}", err);
}
@@ -118,7 +117,9 @@ async fn cleanup_aborted_imex(context: &Context, what: ImexMode) {
dc_delete_files_in_dir(context, context.get_blobdir()).await;
}
if what == ImexMode::ExportBackup || what == ImexMode::ImportBackup {
context.sql.open(context, context.get_dbfile(), false).await;
if let Err(e) = context.sql.open(context, context.get_dbfile(), false).await {
warn!(context, "Re-opening db after imex failed: {}", e);
}
}
}
@@ -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 {
@@ -166,17 +168,35 @@ pub async fn has_backup_old(context: &Context, dir_name: impl AsRef<Path>) -> Re
let name = name.to_string_lossy();
if name.starts_with("delta-chat") && name.ends_with(".bak") {
let sql = Sql::new();
if sql.open(context, &path, true).await {
let curr_backup_time = sql
.get_raw_config_int(context, "backup_time")
.await
.unwrap_or_default();
if curr_backup_time > newest_backup_time {
newest_backup_path = Some(path);
newest_backup_time = curr_backup_time;
match sql.open(context, &path, true).await {
Ok(_) => {
let curr_backup_time = sql
.get_raw_config_int("backup_time")
.await?
.unwrap_or_default();
if curr_backup_time > newest_backup_time {
newest_backup_path = Some(path);
newest_backup_time = curr_backup_time;
}
info!(context, "backup_time of {} is {}", name, curr_backup_time);
sql.close().await;
}
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;
}
}
info!(context, "backup_time of {} is {}", name, curr_backup_time);
sql.close().await;
}
}
}
@@ -220,10 +240,8 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
msg.param
.set(Param::MimeType, "application/autocrypt-setup");
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
msg.param.set_int(
Param::ForcePlaintext,
ForcePlaintext::NoAutocryptHeader as i32,
);
msg.param.set_int(Param::ForcePlaintext, 1);
msg.param.set_int(Param::SkipAutocrypt, 1);
let msg_id = chat::send_msg(context, chat_id, &mut msg).await?;
info!(context, "Wait for setup message being sent ...",);
@@ -254,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!(
@@ -271,8 +289,8 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
);
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
let msg_subj = context.stock_str(StockMessage::AcSetupMsgSubject).await;
let msg_body = context.stock_str(StockMessage::AcSetupMsgBody).await;
let msg_subj = stock_str::ac_setup_msg_subject(context).await;
let msg_body = stock_str::ac_setup_msg_body(context).await;
let msg_body_html = msg_body.replace("\r", "").replace("\n", "<br>");
Ok(format!(
concat!(
@@ -316,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(
@@ -377,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 => {
@@ -387,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 {
@@ -405,6 +423,8 @@ async fn set_self_key(
},
)
.await?;
info!(context, "stored self key: {:?}", keypair.secret.key_id());
Ok(())
}
@@ -421,7 +441,7 @@ async fn decrypt_setup_file<T: std::io::Read + std::io::Seek>(
pub fn normalize_setup_code(s: &str) -> String {
let mut out = String::new();
for c in s.chars() {
if c >= '0' && c <= '9' {
if ('0'..='9').contains(&c) {
out.push(c);
if let 4 | 9 | 14 | 19 | 24 | 29 | 34 | 39 = out.len() {
out += "-"
@@ -431,19 +451,11 @@ pub fn normalize_setup_code(s: &str) -> String {
out
}
async fn imex_inner(
context: &Context,
what: ImexMode,
param: Option<impl AsRef<Path>>,
) -> Result<()> {
ensure!(param.is_some(), "No Import/export dir/file given.");
info!(context, "Import/export process started.");
async fn imex_inner(context: &Context, what: ImexMode, path: impl AsRef<Path>) -> Result<()> {
info!(context, "Import/export dir: {}", path.as_ref().display());
ensure!(context.sql.is_open().await, "Database not opened.");
context.emit_event(EventType::ImexProgress(10));
ensure!(context.sql.is_open().await, "Database not opened.");
let path = param.ok_or_else(|| format_err!("Imex: Param was None"))?;
if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
// before we export anything, make sure the private key exists
if e2ee::ensure_secret_key_exists(context).await.is_err() {
@@ -457,9 +469,7 @@ async fn imex_inner(
ImexMode::ExportSelfKeys => export_self_keys(context, path).await,
ImexMode::ImportSelfKeys => import_self_keys(context, path).await,
// TODO In some months we can change the export_backup_old() call to export_backup() and delete export_backup_old().
// (now is 07/2020)
ImexMode::ExportBackup => export_backup_old(context, path).await,
ImexMode::ExportBackup => export_backup(context, path).await,
// import_backup() will call import_backup_old() if this is an old backup.
ImexMode::ImportBackup => import_backup(context, path).await,
}
@@ -484,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!(
@@ -495,10 +509,20 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
);
let backup_file = File::open(backup_to_import).await?;
let file_size = backup_file.metadata().await?.len();
let archive = Archive::new(backup_file);
let mut entries = archive.entries()?;
while let Some(file) = entries.next().await {
let f = &mut file?;
let current_pos = f.raw_file_position();
let progress = 1000 * current_pos / file_size;
if progress > 10 && progress < 1000 {
// We already emitted ImexProgress(10) above
context.emit_event(EventType::ImexProgress(progress as usize));
}
if f.path()?.file_name() == Some(OsStr::new(DBFILE_BACKUP_NAME)) {
// async_tar can't unpack to a specified file name, so we just unpack to the blobdir and then move the unpacked file.
f.unpack_in(context.get_blobdir()).await?;
@@ -507,7 +531,6 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
context.get_dbfile(),
)
.await?;
context.emit_event(EventType::ImexProgress(400)); // Just guess the progress, we at least have the dbfile by now
} else {
// async_tar will unpack to blobdir/BLOBS_BACKUP_NAME, so we move the file afterwards.
f.unpack_in(context.get_blobdir()).await?;
@@ -522,15 +545,13 @@ async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) ->
}
}
ensure!(
context
.sql
.open(&context, &context.get_dbfile(), false)
.await,
"could not re-open db"
);
context
.sql
.open(context, &context.get_dbfile(), false)
.await
.context("Could not re-open db")?;
delete_and_reset_all_device_msgs(&context).await?;
delete_and_reset_all_device_msgs(context).await?;
Ok(())
}
@@ -544,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!(
@@ -560,21 +585,19 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
);
/* error already logged */
/* re-open copied database file */
ensure!(
context
.sql
.open(&context, &context.get_dbfile(), false)
.await,
"could not re-open db"
);
context
.sql
.open(context, &context.get_dbfile(), false)
.await
.context("Could not re-open db")?;
delete_and_reset_all_device_msgs(&context).await?;
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,
@@ -584,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;
@@ -624,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");
@@ -647,21 +666,26 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef<Path>
async fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
let now = time();
let (temp_path, dest_path) = get_next_backup_path_new(dir, now).await?;
let (temp_path, dest_path) = get_next_backup_path(dir, now).await?;
let _d = DeleteOnDrop(temp_path.clone());
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;
@@ -677,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 {
@@ -711,131 +735,37 @@ async fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> Result<(
.append_path_with_name(context.get_dbfile(), DBFILE_BACKUP_NAME)
.await?;
context.emit_event(EventType::ImexProgress(500));
let read_dir: Vec<_> = fs::read_dir(context.get_blobdir()).await?.collect().await;
let count = read_dir.len();
let mut written_files = 0;
builder
.append_dir_all(BLOBS_BACKUP_NAME, context.get_blobdir())
.await?;
for entry in read_dir.into_iter() {
let entry = entry?;
let name = entry.file_name();
if !entry.file_type().await?.is_file() {
warn!(
context,
"Export: Found dir entry {} that is not a file, ignoring",
name.to_string_lossy()
);
continue;
}
let mut file = File::open(entry.path()).await?;
let path_in_archive = PathBuf::from(BLOBS_BACKUP_NAME).join(name);
builder.append_file(path_in_archive, &mut file).await?;
written_files += 1;
let progress = 1000 * written_files / count;
if progress > 10 && progress < 1000 {
// We already emitted ImexProgress(10) above
emit_event!(context, EventType::ImexProgress(progress));
}
}
builder.finish().await?;
Ok(())
}
async fn export_backup_old(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
// FIXME: we should write to a temporary file first and rename it on success. this would guarantee the backup is complete.
// let dest_path_filename = dc_get_next_backup_file(context, dir, res);
let now = time();
let dest_path_filename = get_next_backup_path_old(dir, now).await?;
let dest_path_string = dest_path_filename.to_string_lossy().to_string();
sql::housekeeping(context).await;
context.sql.execute("VACUUM;", paramsv![]).await.ok();
// we close the database during the copy of the dbfile
context.sql.close().await;
info!(
context,
"Backup '{}' to '{}'.",
context.get_dbfile().display(),
dest_path_filename.display(),
);
let copied = dc_copy_file(context, context.get_dbfile(), &dest_path_filename).await;
context
.sql
.open(&context, &context.get_dbfile(), false)
.await;
if !copied {
bail!(
"could not copy file from '{}' to '{}'",
context.get_dbfile().display(),
dest_path_string
);
}
let dest_sql = Sql::new();
ensure!(
dest_sql.open(context, &dest_path_filename, false).await,
"could not open exported database {}",
dest_path_string
);
let res = match add_files_to_export(context, &dest_sql).await {
Err(err) => {
dc_delete_file(context, &dest_path_filename).await;
error!(context, "backup failed: {}", err);
Err(err)
}
Ok(()) => {
dest_sql
.set_raw_config_int(context, "backup_time", now as i32)
.await?;
context.emit_event(EventType::ImexFileWritten(dest_path_filename));
Ok(())
}
};
dest_sql.close().await;
Ok(res?)
}
async fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> {
// add all files as blobs to the database copy (this does not require
// the source to be locked, neigher the destination as it is used only here)
if !sql.table_exists("backup_blobs").await? {
sql.execute(
"CREATE TABLE backup_blobs (id INTEGER PRIMARY KEY, file_name, file_content);",
paramsv![],
)
.await?;
}
// copy all files from BLOBDIR into backup-db
let mut total_files_cnt = 0;
let dir = context.get_blobdir();
let dir_handle = async_std::fs::read_dir(&dir).await?;
total_files_cnt += dir_handle.filter(|r| r.is_ok()).count().await;
info!(context, "EXPORT: total_files_cnt={}", total_files_cnt);
sql.with_conn_async(|conn| async move {
// scan directory, pass 2: copy files
let mut dir_handle = async_std::fs::read_dir(&dir).await?;
let mut processed_files_cnt = 0;
while let Some(entry) = dir_handle.next().await {
let entry = entry?;
if context.shall_stop_ongoing().await {
return Ok(());
}
processed_files_cnt += 1;
let permille = max(min(processed_files_cnt * 1000 / total_files_cnt, 990), 10);
context.emit_event(EventType::ImexProgress(permille));
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
if name.starts_with("delta-chat") && name.ends_with(".bak") {
continue;
}
info!(context, "EXPORT: copying filename={}", name);
let curr_path_filename = context.get_blobdir().join(entry.file_name());
if let Ok(buf) = dc_read_file(context, &curr_path_filename).await {
if buf.is_empty() {
continue;
}
// bail out if we can't insert
let mut stmt = conn.prepare_cached(
"INSERT INTO backup_blobs (file_name, file_content) VALUES (?, ?);",
)?;
stmt.execute(paramsv![name, buf])?;
}
}
Ok(())
})
.await?;
Ok(())
}
/*******************************************************************************
* Classic key import
******************************************************************************/
@@ -871,6 +801,12 @@ async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()
continue;
}
}
info!(
context,
"considering key file: {}",
path_plus_name.display()
);
match dc_read_file(context, &path_plus_name).await {
Ok(buf) => {
let armored = std::string::String::from_utf8_lossy(&buf);
@@ -894,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)
@@ -960,7 +893,7 @@ where
let any_key = key as &dyn Any;
let kind = if any_key.downcast_ref::<SignedPublicKey>().is_some() {
"public"
} else if any_key.downcast_ref::<SignedPublicKey>().is_some() {
} else if any_key.downcast_ref::<SignedSecretKey>().is_some() {
"private"
} else {
"unknown"
@@ -968,7 +901,12 @@ where
let id = id.map_or("default".into(), |i| i.to_string());
dir.as_ref().join(format!("{}-key-{}.asc", kind, &id))
};
info!(context, "Exporting key {}", file_name.display());
info!(
context,
"Exporting key {:?} to {}",
key.key_id(),
file_name.display()
);
dc_delete_file(context, &file_name).await;
let content = key.to_asc(None).into_bytes();
@@ -984,8 +922,11 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
use crate::test_utils::*;
use crate::stock_str::StockMessage;
use crate::test_utils::{alice_keypair, TestContext};
use ::pgp::armor::BlockType;
#[async_std::test]
@@ -993,7 +934,7 @@ mod tests {
let t = TestContext::new().await;
t.configure_alice().await;
let msg = render_setup_file(&t.ctx, "hello").await.unwrap();
let msg = render_setup_file(&t, "hello").await.unwrap();
println!("{}", &msg);
// Check some substrings, indicating things got substituted.
// In particular note the mixing of `\r\n` and `\n` depending
@@ -1010,12 +951,11 @@ mod tests {
#[async_std::test]
async fn test_render_setup_file_newline_replace() {
let t = TestContext::new().await;
t.ctx
.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
t.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
.await
.unwrap();
t.configure_alice().await;
let msg = render_setup_file(&t.ctx, "pw").await.unwrap();
let msg = render_setup_file(&t, "pw").await.unwrap();
println!("{}", &msg);
assert!(msg.contains("<p>hello<br>there</p>"));
}
@@ -1023,7 +963,7 @@ mod tests {
#[async_std::test]
async fn test_create_setup_code() {
let t = TestContext::new().await;
let setupcode = create_setup_code(&t.ctx);
let setupcode = create_setup_code(&t);
assert_eq!(setupcode.len(), 44);
assert_eq!(setupcode.chars().nth(4).unwrap(), '-');
assert_eq!(setupcode.chars().nth(9).unwrap(), '-');
@@ -1036,7 +976,7 @@ mod tests {
}
#[async_std::test]
async fn test_export_key_to_asc_file() {
async fn test_export_public_key_to_asc_file() {
let context = TestContext::new().await;
let key = alice_keypair().public;
let blobdir = "$BLOBDIR";
@@ -1050,6 +990,37 @@ mod tests {
assert_eq!(bytes, key.to_asc(None).into_bytes());
}
#[async_std::test]
async fn test_export_private_key_to_asc_file() {
let context = TestContext::new().await;
let key = alice_keypair().secret;
let blobdir = "$BLOBDIR";
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key)
.await
.is_ok());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
let filename = format!("{}/private-key-default.asc", blobdir);
let bytes = async_std::fs::read(&filename).await.unwrap();
assert_eq!(bytes, key.to_asc(None).into_bytes());
}
#[async_std::test]
async fn test_export_and_import_key() {
let context = TestContext::new().await;
context.configure_alice().await;
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir).await {
panic!("got error on export: {:?}", err);
}
let context2 = TestContext::new().await;
context2.configure_alice().await;
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir).await {
panic!("got error on import: {:?}", err);
}
}
#[test]
fn test_normalize_setup_code() {
let norm = normalize_setup_code("123422343234423452346234723482349234");

File diff suppressed because it is too large Load Diff

View File

@@ -9,10 +9,11 @@ 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;
use crate::constants::*;
use crate::constants::KeyGenType;
use crate::context::Context;
use crate::dc_tools::{time, EmailAddress, InvalidEmailError};
use crate::sql;
@@ -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,34 +220,35 @@ impl DcSecretKey for SignedSecretKey {
async fn generate_keypair(context: &Context) -> Result<KeyPair> {
let addr = context
.get_config(Config::ConfiguredAddr)
.await
.ok_or_else(|| Error::NoConfiguredAddr)?;
.await?
.ok_or(Error::NoConfiguredAddr)?;
let addr = EmailAddress::new(&addr)?;
let _guard = context.generating_key_mutex.lock().await;
// 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))?;
@@ -355,7 +360,7 @@ pub async fn store_self_keypair(
}
/// A key fingerprint
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct Fingerprint(Vec<u8>);
impl Fingerprint {
@@ -375,6 +380,14 @@ impl Fingerprint {
}
}
impl fmt::Debug for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Fingerprint")
.field("hex", &self.hex())
.finish()
}
}
/// Make a human-readable fingerprint.
impl fmt::Display for Fingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -399,7 +412,7 @@ impl std::str::FromStr for Fingerprint {
let hex_repr: String = input
.to_uppercase()
.chars()
.filter(|&c| c >= '0' && c <= '9' || c >= 'A' && c <= 'F')
.filter(|&c| ('0'..='9').contains(&c) || ('A'..='F').contains(&c))
.collect();
let v: Vec<u8> = hex::decode(hex_repr)?;
let fp = Fingerprint::new(v)?;
@@ -418,16 +431,14 @@ pub enum FingerprintError {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use crate::test_utils::{alice_keypair, TestContext};
use std::error::Error;
use async_std::sync::Arc;
use lazy_static::lazy_static;
use once_cell::sync::Lazy;
lazy_static! {
static ref KEYPAIR: KeyPair = alice_keypair();
}
static KEYPAIR: Lazy<KeyPair> = Lazy::new(alice_keypair);
#[test]
fn test_from_armored_string() {
@@ -529,11 +540,11 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
#[test]
fn test_from_slice_bad_data() {
let mut bad_data: [u8; 4096] = [0; 4096];
for i in 0..4096 {
bad_data[i] = (i & 0xff) as u8;
for (i, v) in bad_data.iter_mut().enumerate() {
*v = (i & 0xff) as u8;
}
for j in 0..(4096 / 40) {
let slice = &bad_data[j..j + 4096 / 2 + j];
let slice = &bad_data.get(j..j + 4096 / 2 + j).unwrap();
assert!(SignedPublicKey::from_slice(slice).is_err());
assert!(SignedSecretKey::from_slice(slice).is_err());
}
@@ -552,31 +563,29 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
let alice = alice_keypair();
let t = TestContext::new().await;
t.configure_alice().await;
let pubkey = SignedPublicKey::load_self(&t.ctx).await.unwrap();
let pubkey = SignedPublicKey::load_self(&t).await.unwrap();
assert_eq!(alice.public, pubkey);
let seckey = SignedSecretKey::load_self(&t.ctx).await.unwrap();
let seckey = SignedSecretKey::load_self(&t).await.unwrap();
assert_eq!(alice.secret, seckey);
}
#[async_std::test]
async fn test_load_self_generate_public() {
let t = TestContext::new().await;
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
t.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
.unwrap();
let key = SignedPublicKey::load_self(&t.ctx).await;
let key = SignedPublicKey::load_self(&t).await;
assert!(key.is_ok());
}
#[async_std::test]
async fn test_load_self_generate_secret() {
let t = TestContext::new().await;
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
t.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
.unwrap();
let key = SignedSecretKey::load_self(&t.ctx).await;
let key = SignedSecretKey::load_self(&t).await;
assert!(key.is_ok());
}
@@ -585,17 +594,17 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
use std::thread;
let t = TestContext::new().await;
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
t.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
.unwrap();
let ctx = t.ctx.clone();
let ctx0 = ctx.clone();
let thr0 =
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx0)));
let ctx1 = ctx.clone();
let thr1 =
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx1)));
let thr0 = {
let ctx = t.clone();
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx)))
};
let thr1 = {
let ctx = t.clone();
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx)))
};
let res0 = thr0.join().unwrap();
let res1 = thr1.join().unwrap();
assert_eq!(res0.unwrap(), res1.unwrap());
@@ -612,12 +621,11 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
// Saving the same key twice should result in only one row in
// the keypairs table.
let t = TestContext::new().await;
let ctx = Arc::new(t.ctx);
let ctx = Arc::new(t);
let ctx1 = ctx.clone();
let nrows = || async {
ctx1.sql
.query_get_value::<u32>(&ctx1, "SELECT COUNT(*) FROM keypairs;", paramsv![])
ctx.sql
.count(sqlx::query("SELECT COUNT(*) FROM keypairs;"))
.await
.unwrap()
};

View File

@@ -62,7 +62,7 @@ where
mod tests {
use super::*;
use crate::key::{SignedPublicKey, SignedSecretKey};
use crate::test_utils::*;
use crate::test_utils::{alice_keypair, TestContext};
#[test]
fn test_keyring_add_keys() {
@@ -83,10 +83,10 @@ mod tests {
t.configure_alice().await;
let alice = alice_keypair();
let pub_ring: Keyring<SignedPublicKey> = Keyring::new_self(&t.ctx).await.unwrap();
let pub_ring: Keyring<SignedPublicKey> = Keyring::new_self(&t).await.unwrap();
assert_eq!(pub_ring.keys(), [alice.public]);
let sec_ring: Keyring<SignedSecretKey> = Keyring::new_self(&t.ctx).await.unwrap();
let sec_ring: Keyring<SignedSecretKey> = Keyring::new_self(&t).await.unwrap();
assert_eq!(sec_ring.keys(), [alice.secret]);
}
}

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