Compare commits

..

451 Commits

Author SHA1 Message Date
holger krekel
502d675b70 trying trying 2020-05-24 17:36:25 +02:00
holger krekel
89fec474d4 Merge remote-tracking branch 'origin/feat/async-jobs-parallel-fetch' into try_stress_with_parallel 2020-05-24 17:02:08 +02:00
holger krekel
f682ae6695 emit debug message 2020-05-24 17:01:26 +02:00
holger krekel
a4971b5fcb Merge branch 'stress_test' into try_stress 2020-05-24 17:01:14 +02:00
holger krekel
fa3ee4205d refactor some python infra, and don't do shutdown on __del__, it's not prepared for running during teardown 2020-05-24 15:48:12 +02:00
holger krekel
7f4627356b test and fix buggy parsing of incoming message which would show MDNs as empty incoming messages 2020-05-24 15:22:55 +02:00
holger krekel
a068b82671 actually this test passes, hum -- but there is a problem i swear 2020-05-24 08:10:03 +02:00
dignifiedquire
72d4da0095 feat(imap): process incoming messages in bulk 2020-05-24 00:06:39 +02:00
holger krekel
d4ddc2f9da make wheel building work again -- switch manylinux2014 (#1522) 2020-05-23 21:57:50 +02:00
dignifiedquire
0ea442ca36 feat: add chat::send_msg_sync 2020-05-23 18:56:45 +02:00
holger krekel
e55dc2213a fix python lifecycles so that termination works 2020-05-23 10:17:56 +02:00
dignifiedquire
05f79c1c01 update dependencies 2020-05-23 00:17:50 +02:00
dignifiedquire
8569e1c18b python: first pass at updates for dc_open/dc_close removal 2020-05-22 23:56:03 +02:00
dignifiedquire
2b1d4651fb remove dc_open and dc_close 2020-05-22 22:40:55 +02:00
dignifiedquire
c53a3d5cb4 update rust api to match ffi 2020-05-22 22:04:20 +02:00
Friedel Ziegelmayer
014d2946b2 feat: use EventEmitter for events 2020-05-22 21:03:01 +02:00
holger krekel
4b4e6e1732 use bjoern's naming suggestion 2020-05-22 18:44:57 +02:00
holger krekel
d0686ada83 a round of renaming towards dc_io_* methods for start/stop/io scheduling 2020-05-22 18:44:57 +02:00
holger krekel
c43285f6ac fix python data2/string handling and reduce extra code needed 2020-05-22 16:24:46 +02:00
dignifiedquire
3947e90b36 update names 2020-05-22 16:24:46 +02:00
dignifiedquire
916935b8d0 ffi: refactor event handling 2020-05-22 16:24:46 +02:00
holger krekel
229606fcc5 fix last failing test -- make account.shutdown() robust against getting called from event thread 2020-05-22 15:12:55 +02:00
holger krekel
23ceda5ad9 add notes from little a/v we just did 2020-05-22 14:58:17 +02:00
dignifiedquire
12e66f5a96 switch to stable toolchain by default 2020-05-22 13:28:49 +02:00
holger krekel
371a7552f5 fix superflous shutdowns -- those are called automatically after the test
function ends
2020-05-22 13:06:20 +02:00
holger krekel
641955a1ec try fix inbox ordering issue 2020-05-22 13:06:13 +02:00
holger krekel
f97538a399 slightly refine increation tests wrt to ordering 2020-05-22 12:46:12 +02:00
holger krekel
7c8758dc26 fix ordering issues with test_forward_increation 2020-05-22 12:24:52 +02:00
dignifiedquire
da7db04c0e example: happy clippy 2020-05-22 11:48:40 +02:00
dignifiedquire
28ef5164ce happy clippy 2020-05-22 11:37:12 +02:00
dignifiedquire
d1f9563e1f remove tracking of current sql query
this will not work like this anymore with async
2020-05-22 11:37:03 +02:00
dignifiedquire
70a2dbb4bb back to stable async-std + use surf instead of reqwest
removes tokio from our dependency tree, now only one async executor
2020-05-22 11:10:26 +02:00
holger krekel
c43e7cdbdc fix lint and a typo 2020-05-22 10:31:41 +02:00
dignifiedquire
931967353e Merge remote-tracking branch 'origin/master' into feat/async-jobs 2020-05-21 16:26:26 +02:00
dignifiedquire
69f095687d check correct running flag in scheduler stop 2020-05-21 15:50:56 +02:00
dignifiedquire
7b10ec26a3 improve connection management 2020-05-21 15:38:04 +02:00
dignifiedquire
82c85566dc fix securejoin cancelation 2020-05-21 15:09:32 +02:00
dignifiedquire
c89d7b5b18 fix and improve load_next job logic 2020-05-21 14:48:55 +02:00
dignifiedquire
e8e82d9760 improve error handling and fix sleeps 2020-05-21 14:35:07 +02:00
dignifiedquire
9817ccebcf improve logging 2020-05-21 12:00:34 +02:00
holger krekel
ad522cd798 simplify and speedup configuration handling
refactor conftest.py away
2020-05-20 22:15:27 +02:00
holger krekel
fedc946886 fix online configure tests 2020-05-20 19:44:06 +02:00
holger krekel
c3458ec59f fix test 2020-05-20 19:17:48 +02:00
holger krekel
2f09bb468e fix configure_canceled test 2020-05-20 19:10:36 +02:00
holger krekel
2279e18329 fix logging and docstrings 2020-05-20 18:57:51 +02:00
dignifiedquire
16e519430a fix(job): avoid double insertion 2020-05-20 18:38:53 +02:00
holger krekel
0ec5b8d6dd fix --ignored handling 2020-05-20 18:10:13 +02:00
holger krekel
1029c63a20 python fixes 2020-05-20 17:21:57 +02:00
holger krekel
9e43540dfa fix docs, some renames 2020-05-20 17:05:00 +02:00
dignifiedquire
3c7b3faa7f unify deps and more strict start and stop 2020-05-20 16:42:59 +02:00
holger krekel
4855584de9 rename eventlogger to events 2020-05-20 16:37:27 +02:00
holger krekel
f67c86cb39 refactor callback thread handling 2020-05-20 16:32:12 +02:00
B. Petersen
779a906d97 move standards.md here
the document standards.md exist since a long time
to get an overview but also a nice reference to get some links.

however, its placement in the android-repo is a bit unlogical,
probably comes from a time where there was only one implementation :)

as ios and desktop use the same standards and as
all acutal implementations are in the core,
we move it to the core repo.

android can have a link to the new place.
2020-05-20 15:48:13 +02:00
holger krekel
b91d7f314b progress 2020-05-20 14:52:17 +02:00
holger krekel
3703a1c36c refinement startup 2020-05-20 13:31:45 +02:00
dignifiedquire
03fd311bfe log shutdown msg 2020-05-20 13:04:44 +02:00
B. Petersen
7e78d6a92b fix link 2020-05-19 22:52:11 +03:00
dignifiedquire
71f7a3e902 avoid clone 2020-05-19 18:07:04 +02:00
dignifiedquire
84abe257f1 bring back intermediate job fetching 2020-05-19 17:44:37 +02:00
dignifiedquire
133ff4914d first pass at resolving CR 2020-05-19 17:26:22 +02:00
dignifiedquire
ba4df23bff python: remove unused start_threads calls 2020-05-19 15:47:20 +02:00
dignifiedquire
14c9161268 python: fix basic start and stop 2020-05-19 15:45:08 +02:00
dignifiedquire
236e9562fd python: run and shutdown 2020-05-19 15:31:28 +02:00
holger krekel
a6409dcd27 attempt to fix cb/python event handling 2020-05-19 15:02:05 +02:00
holger krekel
71442db39f snap 2020-05-19 14:49:16 +02:00
dignifiedquire
cfd68f9f2e python: remove unused queue 2020-05-19 14:40:59 +02:00
dignifiedquire
2cfd5754ca python: start integration of get_next_event 2020-05-19 14:29:47 +02:00
dignifiedquire
f81c1afde7 bring back busy_timeout 2020-05-19 13:30:15 +02:00
dignifiedquire
f7a7debd9d update dependencies 2020-05-19 12:18:47 +02:00
dignifiedquire
af56ebb04e Merge remote-tracking branch 'origin/master' into feat/async-jobs 2020-05-19 12:07:34 +02:00
holger krekel
452c9225dc snap 2020-05-18 18:26:50 +02:00
holger krekel
c1593c5c53 sipmlify, no randomness anymore 2020-05-18 18:26:50 +02:00
holger krekel
b0dbb28422 less randomness 2020-05-18 18:26:50 +02:00
holger krekel
9c02f6db6e some more mesh 2020-05-18 18:26:50 +02:00
holger krekel
508b985249 first working stress test 2020-05-18 18:26:50 +02:00
B. Petersen
6483b8c138 change to WAL only on one handle.
journal_mode is persisted, it is sufficient to change it only for one handle.
with_init() is called for a bunch of handles directly on pool-creation,
so changing to WAL here easily results in busy-errors.
2020-05-18 18:19:08 +02:00
B. Petersen
9702647044 enable sqlite's WAL-mode unconditionally 2020-05-18 06:55:06 +02:00
B. Petersen
3b2192a046 make clippy happy 2020-05-18 06:55:06 +02:00
B. Petersen
b864911e18 return current journal_mode in dc_get_info()
values are either 'delete' or 'wal';
having this info in debug reports is useful for debugging anyway,
if we switch to WAL or not.
2020-05-18 06:55:06 +02:00
bjoern
9e0506cb48 Merge pull request #1488 from deltachat/fix-get-sent-folder
improve finding out the sent-folder
2020-05-17 03:01:39 +02:00
B. Petersen
a0000b9489 update provider-db 2020-05-16 23:26:53 +02:00
Hocuri
df3fac4e5e Make clear that dc_set_chat_mute_duration() and dc_chat_is_muted() belong together. 2020-05-16 22:32:07 +02:00
bjoern
8a36f55439 Merge pull request #1491 from deltachat/fix-get-sent-folder1
Code style (#1488)
2020-05-16 15:25:04 +02:00
Hocuri
ab253744f8 Code style 2020-05-16 15:07:01 +02:00
bjoern
fff1eaba45 Merge pull request #1485 from deltachat/tweak-delete-docs
clarify delete_server_after option
2020-05-16 11:29:13 +02:00
B. Petersen
eafb7b979d make clippy happy 2020-05-16 00:09:22 +02:00
B. Petersen
9e22bf39cc smoother searching for sentbox
search for the sentbox in two passes:
- first check the folder attributes
- only if that fails, check for some known names

this way, the sent-attribute always has precedence
over the name; this was not the case before.

moreover, this fixes an possibly wrong early exist
when the attribute list is completely empty.
2020-05-16 00:01:42 +02:00
B. Petersen
fc6691ce5b add 'sentmail' to the list of known names for 'sent', acutally used by chello.at 2020-05-15 23:05:23 +02:00
holger krekel
598dc86ca5 make sure we don't garble output during test runs by more carefully
switching logging on/off globally
2020-05-15 21:19:57 +02:00
Alexander Krotov
3e2bfc35e3 Delete hidden expired messages
The condition remains from the time when expired messages were hidden
instead of being moved into trash chat. As a result, old hidden
messages, such as location messages, were not deleted.
2020-05-15 20:46:41 +03:00
B. Petersen
6658ad8618 clarify delete_server_after option 2020-05-15 17:12:19 +02:00
bjoern
759ccdbee2 Merge pull request #1441 from deltachat/config-from-provider-db
optionally get default-options from provider-db
2020-05-15 12:19:14 +02:00
bjoern
635060a02d Merge pull request #1481 from deltachat/adbenitez-patch-1
Update CHANGELOG.md
2020-05-14 11:16:15 +02:00
Asiel Díaz Benítez
a128e7e7ab Update CHANGELOG.md 2020-05-14 03:22:45 -04:00
Jikstra
3cb69b8035 Merge pull request #1479 from deltachat/prep-1.33
Prep 1.33
2020-05-13 23:17:54 +02:00
B. Petersen
7dc58bb330 bump version 2020-05-13 22:46:04 +02:00
B. Petersen
8bd0a62cb3 changelog 2020-05-13 22:45:01 +02:00
dignifiedquire
b25bec53d8 refactor next_event 2020-05-13 18:42:56 +02:00
dignifiedquire
8a7923c974 Merge remote-tracking branch 'origin/master' into feat/async-jobs 2020-05-13 18:29:22 +02:00
Alexander Krotov
3ee81cbee0 Revert "imap: simplify select_folder() interface"
This reverts commit b614de2f80.
2020-05-13 11:36:33 +02:00
B. Petersen
4b744337fe apply user_defaults after the first successful call to dc_configure() 2020-05-12 23:02:22 +02:00
B. Petersen
8d7d2f7a44 run provider/update.py 2020-05-12 23:02:22 +02:00
B. Petersen
24bf1dbffb fixup 2020-05-12 23:02:22 +02:00
B. Petersen
c1890bb126 adapt provider/update.py so that it generates config_defaults 2020-05-12 23:02:22 +02:00
B. Petersen
a4a570896a add config_defaults array to provider-db 2020-05-12 23:02:22 +02:00
bjoern
0594034ee6 Merge pull request #1477 from deltachat/extract-flags
Extract flags in try_load() to variables
2020-05-12 22:56:00 +02:00
bjoern
396e376f49 Merge pull request #1475 from deltachat/clarify-flags
clarify docs for DC_GCL_FOR_FORWARDING
2020-05-12 22:55:48 +02:00
Hocuri
fca9eae0fd Extract flags in try_load() to variables 2020-05-12 20:28:20 +02:00
B. Petersen
f0d9bdd901 clarify docs for DC_GCL_FOR_FORWARDING 2020-05-12 17:48:03 +02:00
Alexander Krotov
c185d5b0b5 Fix python lint error about unused format 2020-05-12 11:02:27 +02:00
bjoern
6ece2a3449 Merge pull request #1471 from deltachat/tweak-help
add dc_estimate_deletion_cnt() to docs, add some references
2020-05-11 16:01:28 +02:00
B. Petersen
682d52441d add dc_estimate_deletion_cnt() to docs, add some references 2020-05-11 15:31:54 +02:00
Simon Laux
c2c0c81f1c cargo fmt 2020-05-11 11:51:40 +02:00
Simon Laux
fe23907eb3 fix muting dm chats
and rewrite the erroro message so that it makes more sense
2020-05-11 11:51:40 +02:00
bjoern
e2bf8a8a11 Merge pull request #1469 from deltachat/prep-1.32
prep 1.32
2020-05-11 00:15:13 +02:00
B. Petersen
3f0136ae7c bump version to 1.32.0 2020-05-10 23:58:22 +02:00
B. Petersen
9f992409c7 changelog 2020-05-10 23:57:21 +02:00
bjoern
cd53ed16e9 Merge pull request #1468 from deltachat/log_precheck_error
Log precheck_imf errors
2020-05-10 23:30:56 +02:00
Alexander Krotov
cc56edc91d Log precheck_imf errors 2020-05-11 00:19:14 +03:00
bjoern
25eb4b3547 Merge pull request #1463 from deltachat/rfc724_mid_exists-ensure
Do not return "empty rfc724_mid" errors from rfc724_mid_cnt
2020-05-10 23:13:14 +02:00
bjoern
c5eb112f5a Merge pull request #1465 from deltachat/smtp-network-error
Better SMTP ErrorNetwork message
2020-05-10 23:05:20 +02:00
bjoern
8d904f415a Merge pull request #1464 from deltachat/database_busy-warn
sql: do not send DC_EVENT_ERROR on database errors
2020-05-10 22:59:24 +02:00
bjoern
c34173ca6e Merge pull request #1466 from deltachat/fix-prefetch
do normal receive_imf() if message-id is empty or if prefetch failed …
2020-05-10 22:54:16 +02:00
B. Petersen
aa292ac6b8 do normal receive_imf() if message-id is empty or if prefetch failed for other reasons. there are servers not sending a message ids, this and other cases is handled in receive_imf() - but not in prefetch (would be too much to maintain, also we need more information). this normal processing also prevents trying over the same message over and over as the server_uid is updated. 2020-05-10 22:43:04 +02:00
Alexander Krotov
c36227e2fc Better SMTP ErrorNetwork message
It uses stock string, just as for IMAP errors, and is distinguishable
from IMAP errors: protocol is specified in the error message now.
2020-05-10 23:24:03 +03:00
B. Petersen
a406e0416f use new Message-ID parser 2020-05-10 22:58:22 +03:00
B. Petersen
215cc5e71d add function for parsing multiple Message-Ids 2020-05-10 22:58:22 +03:00
B. Petersen
0e72acee10 more tolerant message-id parsing 2020-05-10 22:58:22 +03:00
B. Petersen
000ed3175d add failing test 2020-05-10 22:58:22 +03:00
Alexander Krotov
2f6bae4e2a sql: do not send DC_EVENT_ERROR on database errors
These errors are usually just "database busy" errors, it is enough to
write them to the log instead of displaying to the user.
2020-05-10 17:51:01 +03:00
Alexander Krotov
0fefe11bfd Do not return "empty rfc724_mid" errors from rfc724_mid_cnt
This function should only return temporary errors, e.g. database errors,
as precheck_imf() and dc_receive_imf::add_parts() treat them as such,
retrying the fetch on failure. When permanent errors, like missing
Message-ID, are bubbled up, they cause infinite fetch loop.
2020-05-10 16:43:26 +03:00
Alexander Krotov
2dbb1bbbea Do not reply to hidden messages
Especially with read receipts, it is wrong, because they are never
encrypted and their Message-IDs are not known to other users in a group.
2020-05-09 19:11:23 +03:00
Hocuri
a586a1d525 Fix #1120 Contact requests are not shown when name of sender includes a comma character (#1438)
* First try making get_recipients use MailHeader (nice and functional)

* Get it to compile by using not-so-functional style

* Add "empty-from" test, drop unnecessary check for error; continue using addrparse_header() instead of addrparse()

* Try to use functional style, unfortunately, I can't get the compiler to accept it

* Do it imperative-style: Do not overwrite To with Cc and vice versa

* Use addrparse_header() once more

* Still addrparse_header()

* Clippy

* Fix compile errors in tests

* Fix typo

* Fix tests again ;-)

* Code style

* Code style; try a HashMap<addr: String, display_name: String> as an address list but I am not convinced

* Code style; Use Vec<SingleInfo> as address list

* Clippy

* Add tests

* Add another test

* Remove stale comments
2020-05-07 13:55:09 +02:00
holger krekel
4724101e75 fix upload error?! (#1454)
* use latest setuptools 
* clear indexes also if nothing was uploaded to dc/* branches
2020-05-06 17:31:43 +02:00
bjoern
1b921cd533 Merge pull request #1457 from deltachat/prep-1.31
prep 1.31
2020-05-06 00:15:34 +02:00
B. Petersen
fcf3786fc5 bump version to 1.31.0 2020-05-05 23:53:54 +02:00
B. Petersen
d78f75aa60 changelog 2020-05-05 23:52:53 +02:00
bjoern
56056cf10e Merge pull request #1451 from deltachat/better-errors
Always describe the context of the displayed error
2020-05-05 23:40:35 +02:00
Alexander Krotov
5fd9b20213 Parse attachment filenames from Content-Type "name" attribute
Outlook specifies filename there and omits Content-Disposition.
2020-05-06 00:37:36 +03:00
B. Petersen
076cdae3fd do not show errors during sending as a ephemeral popup or so, just set the message-state to failed, the error can be queried by the user at any time via 'Info' or so 2020-05-05 21:44:03 +02:00
Alexander Krotov
6543c7c26f fetch_single_msg: do not ignore dc_receive_imf errors
If error is ignored, the message will never be fetched again, even if
there was a database write error.

dc_receive_imf itself is modified to ignore unrecoverable errors, to
prevent endless refetching of incorrect messages.
2020-05-04 18:19:32 +03:00
Alexander Krotov
3035c8af30 Always describe the context of the displayed error 2020-05-04 16:35:42 +03:00
bjoern
3cbd647dad Merge pull request #1449 from deltachat/media-quality
support dc_get|set_config("media_quality")
2020-05-04 10:55:35 +02:00
B. Petersen
4efcbee772 support dc_get|set_config("media_quality") 2020-05-03 13:38:39 +02:00
B. Petersen
bb59cf94e9 bump version to 1.30.0 2020-05-02 19:14:35 +03:00
B. Petersen
96436814f5 changelog 2020-05-02 19:14:35 +03:00
Alexander Krotov
e8763e936d imap: simplify select_folder() interface
Accept AsRef<str> instead of Option<impl AsRef<str>>.

There is no need to pass None to force expunge anymore.
2020-04-30 23:48:41 +03:00
Alexander Krotov
c41a6b87b8 imap: always close folder before selecting if expunge is needed 2020-04-30 23:48:41 +03:00
B. Petersen
54395a7252 do not send DC_EVENT_MSGS_CHANGED or DC_EVENT_INCOMING_MSG for hidden messages
these events take the message-id as parameter and might be used
to update an existing list (although to recommended)

if the event is issued for hidden messages,
this might led to "empty" messages flashing up -
the ui tries to get the message from the event,
after a moment, on the next update, the message disappears again
as hidden messages are of course not returned eg. by dc_get_chat_msgs().

the effect was probably always visible for secure-join-messages on ios,
however, become much more visible recently when read-receipts are added
as hidden messages as well (to make them auto-deletable).
2020-04-30 19:44:33 +03:00
bjoern
4322b8b932 Merge pull request #1435 from deltachat/prep-1.29
prepare release 1.29
2020-04-29 16:36:48 +02:00
B. Petersen
f444af825f bump version to 1.29.0 2020-04-29 14:49:40 +02:00
B. Petersen
0d0e7f774e update readme 2020-04-28 23:01:10 +02:00
bjoern
30ed27ae5c Merge pull request #1436 from deltachat/get_info_btreemap
Return BTreeMap instead of HashMap from get_info()
2020-04-28 23:00:28 +02:00
bjoern
b7283487b2 Merge pull request #1426 from deltachat/fix-uptime
fix uptime debug output
2020-04-28 23:00:07 +02:00
jikstra
551f7dc05a Make starts_with_ignore_case() private 2020-04-28 22:52:39 +03:00
jikstra
1b485770b6 cargo fmt 2020-04-28 22:52:39 +03:00
jikstra
50e18f84c2 Also support lowercased dcaccount: uris 2020-04-28 22:52:39 +03:00
jikstra
9eab96090d Cargo fmt 2020-04-28 22:52:39 +03:00
jikstra
737a741a54 Support lowercased openpgp4fpr uri scheme 2020-04-28 22:52:39 +03:00
Alexander Krotov
46253039df Return BTreeMap instead of HashMap from get_info()
BTreeMap is sorted by keys when iterated, making it easier to find the
required line.
2020-04-28 21:26:04 +03:00
B. Petersen
2f5b6a115d update provider database 2020-04-28 21:02:42 +03:00
bjoern
05d63fcbe6 Merge pull request #1430 from deltachat/fix-footer
regard line with ony '--' as footer mark partly
2020-04-28 17:35:15 +02:00
bjoern
ae5bc76123 Merge pull request #1432 from deltachat/Explain-database-path
Update Readme.md: Explain database path, fix #1431
2020-04-27 19:45:55 +02:00
Hocuri
b70e92effb Update Readme.md: Explain database path, fix #1431
Also set the database path to ~/deltachat-db so that nothing has to be changed in the command (it can just be copy-pasted).
2020-04-27 19:18:19 +02:00
B. Petersen
2a9b967d2d remove footer-escape-character from message texts 2020-04-27 16:47:16 +02:00
B. Petersen
459fec56db protect '--' in message from being treated as a footer-beginning 2020-04-27 16:47:16 +02:00
B. Petersen
bfdd6f36e2 regard line with ony '--' as footer mark partly
the footer mark normally used in email-conversations is `-- `,
note the trailing space, see RFC 3676, §4.3

unfortunately, the final space is removed by some providers,
which lead to footers showing up on delta-to-delta-conversations
(on nondc-to-delta, this is not an issue as we cannot be sure anyway
and show a [...] therefore)

this change accepts lines with only `--` as a footer separator
if there is no other footer separator
and if the line before is empty and the line after is not.

as there is still some chance to remove text accidentally,
see tests, some protection against that is needed in another commit.
2020-04-27 16:32:59 +02:00
B. Petersen
432e4b7f0a use std::time::SystemTime for uptime calculation
std::time::Instant does not count eg. doze-time on android
2020-04-26 11:44:27 +02:00
B. Petersen
39cb9c425c fixup 2020-04-26 11:44:27 +02:00
B. Petersen
dff1ae0fb4 move duration-formatting to a separate function and add tests for that 2020-04-26 11:16:59 +02:00
bjoern
2943624439 Merge pull request #1424 from deltachat/show-uptime
add context-uptime to dc_get_info()
2020-04-25 16:27:53 +02:00
bjoern
94064e526a Merge pull request #1423 from deltachat/perform-20-jobs
job: perform no more than 20 IMAP jobs in a row
2020-04-25 01:49:20 +02:00
B. Petersen
95cac4dfb9 add context-uptime to dc_get_info() 2020-04-25 01:37:56 +02:00
Alexander Krotov
46fb6a21ee job: perform no more than 20 IMAP jobs in a row
Let the thread load new messages. This may happen when user switches
the setting to delete messages on the server on and there are a lot of
messages to delete.
2020-04-25 01:43:35 +03:00
bjoern
3d6ca973c4 Merge pull request #1417 from deltachat/dynamic-imap-delete-job-generation
Dynamic imap delete job generation
2020-04-24 21:16:21 +02:00
Alexander Krotov
fc03f4c87a job: generate IMAP deletion jobs dynamically
This prevents generation of a large number of jobs when IMAP deletion
setting is enabled for the first time. Writing jobs to the database
locks it for readers and may cause UI freezing, because chatlist and
messages can't be read until all jobs are written.

Note that on failure job will be written to the database, to make sure
it is postponed instead of being retried immediately.
2020-04-24 21:06:57 +03:00
Alexander Krotov
502ec2a56f job: new API for dynamic job creation
Job::new() can be used to create jobs in-memory

Job.update() is replaced with Job.save() which can create new database
entries and consumes Job to avoid the need to update job ID after saving
it to the database.
2020-04-24 21:06:57 +03:00
Alexander Krotov
66d5c3f620 job: derive PartialEq for Action
This makes it possible to compare action priorities in Rust code.
2020-04-24 21:06:57 +03:00
Floris Bruynooghe
220500efbb Move key loading from deprecated Key struct to DcKey trait
This moves the loading of the keys from the database to the trait and
thus with types differing between public and secret keys.  This
fetches the Config::ConfiguredAddr (configured_addr) directly from the
database in the SQL to simplify the API and consistency instead of
making this the responsiblity of all callers to get this right.

Since anyone invoking these methods also wants to be sure the keys
exist, move key generation here as well.  This already simplifies some
code in contact.rs and will eventually replace all manual checks for
existing keys.

To make errors more manageable this gives EmailAddress it's own error
type and adds some conversions for it.  Otherwise the general error
type leaks to far.  The EmailAddress type also gets its ToSql trait impl
to be able to save it to the database directly.
2020-04-24 01:11:11 +02:00
Hocuri
d29c5eabbb Improve descriptions for the profile image files 2020-04-23 22:47:53 +02:00
Alexander Krotov
979d7c5625 Do not ignore database read errors in precheck_imf
If precheck_imf fails to check if message with the same rfc724_mid
already exists, the same message may be downloaded twice. Instead,
abort the whole operation and retry later.
2020-04-23 15:36:45 +03:00
B. Petersen
4e828199c8 test, that also unrecoded avatars are copied to the blob-directory 2020-04-23 06:49:41 +02:00
Hocuri
61c84c8e01 Log more in tests 2020-04-23 06:48:18 +02:00
bjoern
30be5e33d7 Merge pull request #1410 from deltachat/remove_addremove
remove all member_added/remove_events
2020-04-23 00:59:09 +02:00
bjoern
b1c524d4a3 Merge pull request #1415 from deltachat/py_fresh
expose obtaining list of fresh messages to python
2020-04-21 14:14:24 +02:00
holger krekel
7c33c7f7da expose obtaining list of fresh messages to python 2020-04-21 13:53:56 +02:00
bjoern
7846c23edd Merge pull request #1411 from deltachat/delete-no-hide
Delete expired messages instead of hiding them
2020-04-20 18:04:30 +02:00
Alexander Krotov
6c0dd8543d Delete expired messages instead of hiding them
For hidden messages, blobs are not deleted during housekeeping. To
actually free the space used by media files, messages should be moved to
trash instead of being hidden.
2020-04-20 02:29:55 +03:00
holger krekel
70c082a1b1 remove all member_added/remove_events 2020-04-19 21:15:45 +02:00
holger krekel
02cda1e611 refine member/add remove further, and introduce ac_outgoing_message 2020-04-19 21:00:55 +02:00
holger krekel
a1c82eaea6 refine bot testing and ac_member* handling 2020-04-19 21:00:55 +02:00
holger krekel
9eda710538 refine member-added and member-removed plugin hooks to signal the sender (who added/removed a contact )
add ac_chat_modified hook event
add account.get_contact_by_addr (thanks @r10s)
2020-04-19 21:00:55 +02:00
Hocuri
a87a2d0b71 Change to functional style 2020-04-19 18:28:10 +03:00
Hocuri
6e2f4d85a3 Do not ellipsize non-standard footers in chat messages 2020-04-19 18:28:10 +03:00
Hocuri
511727fdfa Do not ellipsize everything in a message 2020-04-19 18:28:10 +03:00
bjoern
a86c7c767a Merge pull request #1409 from deltachat/ffi-doc-fix
fix doc for dc_lookup_contact_id_by_addr()
2020-04-18 13:17:50 +02:00
bjoern
da88d8f17f Update deltachat-ffi/deltachat.h
Co-Authored-By: holger krekel  <holger@merlinux.eu>
2020-04-18 13:17:33 +02:00
B. Petersen
dbd3705441 fix doc for return value of dc_lookup_contact_id_by_addr() 2020-04-18 01:47:36 +02:00
Alexander Krotov
8d2f526ee7 Fix a typo 2020-04-17 22:29:58 +03:00
B. Petersen
b075a73222 adapt doc wrt events not longer having a return value 2020-04-17 21:26:58 +02:00
bjoern
eb5387ca38 Merge pull request #1403 from deltachat/remove-unused
remove unused api
2020-04-17 13:02:09 +02:00
B. Petersen
1eaef94c71 make clippy happy 2020-04-16 22:49:34 +02:00
B. Petersen
e1903edd04 remove unused dc_array_add_id() api 2020-04-16 22:11:54 +02:00
B. Petersen
0b6b8ced92 remove unused dc_chat_get_subtitle() api 2020-04-16 22:08:17 +02:00
B. Petersen
cc6ce72f6e remove unused dc_get_version_str() api 2020-04-16 21:48:51 +02:00
Alexander Krotov
857a384d8b Switch to nightly-2020-03-12 2020-04-16 08:51:41 +02:00
Alexander Krotov
13dd88b7ad mimeparser: display inline images received from Thunderbird 2020-04-15 11:13:26 +02:00
Alexander Krotov
e85cdc8c9f mimeparser: test parsing of inline images 2020-04-14 07:01:50 +02:00
Alexander Krotov
1760740a4c Parse inline attachments from non-DC email clients
Some mobile email clients, such as apple mail, attach or inline images
after description, just like Delta Chat. It is better to display them
instead of ignoring.
2020-04-14 07:01:50 +02:00
Alexander Krotov
daf40fde82 mimefactory: use .next() instead of .nth(0) 2020-04-13 23:02:57 +03:00
Alexander Krotov
e909b8199b lot: use as_deref() 2020-04-13 23:02:57 +03:00
Alexander Krotov
ec089faf3a configure/auto_mozilla: remove indexing 2020-04-13 23:02:57 +03:00
Alexander Krotov
9fcb30ac33 Remove indexing in chatlist.get_summary() 2020-04-13 23:02:57 +03:00
Alexander Krotov
cb92579461 Use slice patterns in EmailAddress::from_str() 2020-04-13 23:02:57 +03:00
Alexander Krotov
0327000f8d squash_attachment_parts: use slice patterns 2020-04-13 23:02:57 +03:00
Alexander Krotov
3a91c87c73 clippy: use as_deref() 2020-04-13 23:02:57 +03:00
Alexander Krotov
db5b5d321b clippy: remove redundant imports 2020-04-13 23:02:57 +03:00
Alexander Krotov
76b7e7408a chatlist: remove indexing in get_{chat,msg}_id() 2020-04-13 23:02:57 +03:00
Alexander Krotov
324e5d0258 Use slice pattern to parse KML coordinates 2020-04-13 23:02:57 +03:00
Alexander Krotov
d997bbc081 skip_forward_header: get rid of indexing with subslice patterns 2020-04-13 23:02:57 +03:00
Alexander Krotov
1c21d4f356 constants: remove unnecessary parenthesis
Rust warns about them now
2020-04-13 23:02:57 +03:00
Alexander Krotov
5f574cf283 get_next_media: replace indexing with .get() 2020-04-13 23:02:57 +03:00
Alexander Krotov
92f1e6da1e get_next_media: enumerate() instead of indexing 2020-04-13 23:02:57 +03:00
Alexander Krotov
24aa3c781b Remove indexing in Aheader::from_str 2020-04-13 23:02:57 +03:00
Alexander Krotov
32bd6109e3 Update rust-toolchain and proptest
It is required for stabilized subslice patterns.

proptest 0.9.6 fixes compatibility with the latest rustc nightly.
2020-04-13 23:02:57 +03:00
B. Petersen
52442017e2 mark contacts as verified if a secure-join for them was observed on another device 2020-04-13 19:15:16 +02:00
B. Petersen
278454287c make sure, Secure-Join-Fingerprint is not accepted unencrypted 2020-04-13 19:15:16 +02:00
B. Petersen
5ded8fb400 add vc-contact-confirm-received message needed for multi-device-verification 2020-04-13 19:15:16 +02:00
B. Petersen
24730e7ad6 do not delete handshake-messages needed for a multi-device-verification 2020-04-13 19:15:16 +02:00
B. Petersen
016a780632 check that incoming read-receipts do not unarchive chats 2020-04-13 17:51:56 +02:00
Hocuri
6d89638ca4 Get ChatId from Message 2020-04-13 17:43:01 +02:00
Hocuri
fdc091319b Make Clippy happy 2020-04-13 17:43:01 +02:00
Hocuri
960c8745d9 For smtp error 5.5.0, try again. For others, send info message (doesn't work) 2020-04-13 17:43:01 +02:00
Hocuri
134b09dba5 Fix #1373, ignore incorrect html close tags 2020-04-13 17:40:07 +02:00
holger krekel
76b93274e8 use latest sphinx -- seems to work again 2020-04-13 17:39:31 +02:00
holger krekel
ea455323d8 increase timeout while waiting for rsa2048 keygen -- default timeout is 30secs, and it sometimes takes 31 or more on the CI machines 2020-04-13 17:39:31 +02:00
holger krekel
1855f84fe0 fix bug in that remove-contact failed on new groups where we didn't have the peerstate of the removed-contact yet 2020-04-13 17:39:31 +02:00
holger krekel
323d996d5f a few streamlinings 2020-04-13 17:39:31 +02:00
holger krekel
1b858393c5 make create_contact accept email addresses that parse into routable_email and display_name 2020-04-13 17:39:31 +02:00
holger krekel
ca88c5b41c rename hooks to use "ac_" (account) and "dc_" (global) 2020-04-13 17:39:31 +02:00
holger krekel
7e1470ea46 refactor preconfigure handling to not break deltabot's usage of deltachat's test fixtures and
relax timestamp comparisons
2020-04-13 17:39:31 +02:00
holger krekel
f98d0bbc1f fix failing sync test 2020-04-13 17:39:31 +02:00
holger krekel
2a34022619 refine example doc and address https://github.com/deltachat/deltachat-core-rust/pull/1307#pullrequestreview-380876587 2020-04-13 17:39:31 +02:00
holger krekel
57f879a6ba fix buffer handling so that the group-tracking bot example passes 2020-04-13 17:39:31 +02:00
holger krekel
d4ba09c753 refactor bot-setup and testing into a helper function 2020-04-13 17:39:31 +02:00
holger krekel
6c3a8448cf Apply suggestions from code review
some fixes thanks to @adbenitez

Co-Authored-By: Asiel Díaz Benítez <asieldbenitez@gmail.com>
2020-04-13 17:39:31 +02:00
holger krekel
d4e1c1b109 refine handling of accepted contacts in example 2020-04-13 17:39:31 +02:00
holger krekel
a1d5120e58 sipmlify plugins and tests and remove superflous core event 2020-04-13 17:39:31 +02:00
holger krekel
724e1ea97e simplify example 2020-04-13 17:39:31 +02:00
holger krekel
6f8067ffd3 address @adbenitez and @r10s comments 2020-04-13 17:39:31 +02:00
holger krekel
f38386d164 fix member_added/member_removed event with tests and and provide a group-tracking example 2020-04-13 17:39:31 +02:00
holger krekel
d66829702f fix #164 add MEMBER_REMOVED event and member_removed plugin python hook 2020-04-13 17:39:31 +02:00
holger krekel
36b50436d7 add Message.mark_seen shortcut 2020-04-13 17:39:31 +02:00
holger krekel
33dd747ec7 some more test setup refinements and make example testing part of tox runs 2020-04-13 17:39:31 +02:00
holger krekel
d8e14d9993 refine example and make Contact accept Account object 2020-04-13 17:39:31 +02:00
holger krekel
f61b9f7964 add a test echo_and_quit examples 2020-04-13 17:39:31 +02:00
holger krekel
91cdc76414 refactor docs and ffi/high level event handling to pass all tests again 2020-04-13 17:39:31 +02:00
holger krekel
a1379f61da fix up docs 2020-04-13 17:39:31 +02:00
holger krekel
a665d6de59 add chat.is_group() API to help callers avoid having to check with constants
deprecate get_type()
2020-04-13 17:39:31 +02:00
holger krekel
6a6a719ab6 shift pytest support code into deltachat package so deltabot can make use of the test infrastructure 2020-04-13 17:39:31 +02:00
holger krekel
84f17b7539 emit "DC_EVENT_MEMBER_ADDED" and python plugin event "member_added" for securejoin or non-securejoin additions of a contact to a chat. also fixup some docs 2020-04-13 17:39:31 +02:00
holger krekel
57141e478c also add a changelog for plugin things 2020-04-13 17:39:31 +02:00
holger krekel
6213917089 start some docs 2020-04-13 17:39:31 +02:00
holger krekel
fb33c31378 fix a couple of issues wrt to configuring move/mvbox behaviour in tests 2020-04-13 17:39:31 +02:00
holger krekel
79f5e736b0 make eventlogger module a global plugin 2020-04-13 17:39:31 +02:00
holger krekel
0d4b6f5627 move io thread handling into own module 2020-04-13 17:39:31 +02:00
holger krekel
5c8f558f60 - simplify to offer start() and shutdown() as primary account methods, strike start_threads/stop_threads.
- introduce update_config(kwargs) method.
- group APIs a bit better
2020-04-13 17:39:31 +02:00
holger krekel
c851f9d5a3 simplify internal thread handling 2020-04-13 17:39:31 +02:00
holger krekel
2d74514dd0 add some incoming/outgoing message hooks 2020-04-13 17:39:31 +02:00
holger krekel
84012e760e refine low level event handling
slight refactor on printing
2020-04-13 17:39:31 +02:00
holger krekel
6baef49f9d add after_shutdown hook 2020-04-13 17:39:31 +02:00
holger krekel
f55d4fa73a rename process_low_level_event to process_ffi_event 2020-04-13 17:39:31 +02:00
holger krekel
ce00c627d4 don't run Eventlogging by default -- the tests instantiate it, though. 2020-04-13 17:39:31 +02:00
holger krekel
d3c6f530e2 introduce global plugin manager 2020-04-13 17:39:31 +02:00
holger krekel
cf6391d51b move event tracking to new tracker.py file
some api cleanups
2020-04-13 17:39:31 +02:00
holger krekel
57311d731e simplify logging 2020-04-13 17:39:31 +02:00
holger krekel
95d45b386f separate out FFI eventracking to only be used in running tests 2020-04-13 17:39:31 +02:00
holger krekel
bbc8bed39c move temp_plugin to account 2020-04-13 17:39:31 +02:00
holger krekel
ec67b3975c good bye global plugin manager ... we only do per-account object plugin_management for now 2020-04-13 17:39:31 +02:00
Alexander Krotov
e9967c32e6 chat: filter parent messages by state instead of UID
Since introduction of server message deletion, message may not have
server UID even when it is not a draft or pending message. To handle
such messages correctly, we explicitly check message state.
2020-04-13 17:38:42 +02:00
Alexander Krotov
493a213d41 sql: move QueryReturnedNoRows and NULL handling into query_row_optional() 2020-04-13 17:38:42 +02:00
Alexander Krotov
f65dbee74b Add basic test for ChatId.parent_is_encrypted() 2020-04-13 17:38:42 +02:00
Alexander Krotov
f51fd1267f chat: move parent_query() and related methods from Chat to ChatId 2020-04-13 17:38:42 +02:00
Hocuri
711f3f69da Emit an event when SMTP fails (the same is already done for IMAP) 2020-04-12 18:17:24 +03:00
Friedel Ziegelmayer
24f4cbbb27 refactor: replace failure
- failure is deprecated
- thiserror for deriving Error impl
- anyhow for highlevel error handling
2020-04-10 22:39:28 +02:00
dignifiedquire
307a3e078e Merge remote-tracking branch 'origin/master' into feat/async-jobs 2020-04-09 23:41:34 +02:00
Alexander Krotov
d31265895d Update rPGP to 0.5.2 2020-04-03 12:57:13 +03:00
bjoern
6e35a879a3 Merge pull request #1375 from deltachat/robust-hide-device-expired-messages
Make "hide_device_expired_messages" more robust
2020-04-03 11:48:09 +02:00
Alexander Krotov
9c2a3b8a82 Chatlist::try_load: make hide_device_expired_messages errors non-fatal 2020-04-03 12:13:42 +03:00
Alexander Krotov
916fab7d4b hide_device_expired_messages: allow missing self or device chat 2020-04-03 12:12:56 +03:00
Alexander Krotov
3163ef87c6 imap: display errors with {}, not {:?} 2020-04-01 23:27:15 +03:00
Alexander Krotov
1934181b52 Terminate new SQL statement lines with \
This way rustfmt can change indentation. However, care should be taken
to keep a space before each \
2020-04-01 20:06:27 +03:00
Alexander Krotov
1b815a7d96 Display an error if message cannot be trashed 2020-04-01 20:06:27 +03:00
Alexander Krotov
4e0a08106d ChatId.get_param: remove unnecessary type annotation 2020-04-01 20:06:27 +03:00
Alexander Krotov
e8cc739fbd Reload chatlist when "delete_device_after" is set 2020-04-01 20:06:27 +03:00
Alexander Krotov
fc57cbfb49 Refactor hiding of expired messages
Now there is only one function: hide_device_expired_messages().

If any messages are hidden event DC_EVENT_MSGS_CHANGED(0,0) is emitted
now, which is more correct than DC_EVENT_CHAT_MODIFIED and also triggers
chatlist reload.
2020-04-01 20:06:27 +03:00
Alexander Krotov
7522fec044 Do not emit "chat modified" events when loading chatlist
Otherwise chatlist will be loaded twice, once right after hiding expired
messages, and once in response to event emitted by try_load().
2020-04-01 20:06:27 +03:00
Alexander Krotov
237dabb907 Do not hide hidden messages in ChatId.delete_device_expired_messages()
This prevents infinite event loop, when chatlist is reloaded in response
to event, and event is emitted by hiding messages before chatlist reload.
2020-04-01 20:06:27 +03:00
Alexander Krotov
3686048ab6 chatlist: hide all expired messages before loading 2020-04-01 20:06:27 +03:00
Alexander Krotov
2bf4c5d7e7 Emit "chat modified" event when messages expire 2020-04-01 20:06:27 +03:00
Alexander Krotov
051d80b2f3 Move delete_device_expired_messages() to ChatId
This allows to check if chat is a self-talk or device chat.

Messages are deleted only in viewed chats now.
2020-04-01 20:06:27 +03:00
Alexander Krotov
adaa1e856c chat: add ChatId.is_self_talk() and ChatId.is_device_talk() 2020-04-01 20:06:27 +03:00
Alexander Krotov
4daa57c98e delete_device_expired_messages: set text to DELETED for hidden messages
This makes debugging easier: DELETED messages should never be shown.
2020-04-01 20:06:27 +03:00
Alexander Krotov
7e67b2cbb3 Do not delete messages from special chats 2020-04-01 20:06:27 +03:00
Alexander Krotov
270d18a88a Move prune_tombstones() to sql:: and call from housekeeping() 2020-04-01 20:06:27 +03:00
Alexander Krotov
25f8a735a9 get_chat_msgs: remove locally expired messages
Expired messages are hidden right before retrieving messages from the
database, so expired messages are not shown to the user.
2020-04-01 20:06:27 +03:00
Alexander Krotov
9eb672ea17 Move removal of chat message tombstones to a separate function
DELETE operation may be slow compared to UPDATE. It is better to do in
a separate job.
2020-04-01 20:06:27 +03:00
Alexander Krotov
9febc762da Add chat::delete_device_expired_messages() function 2020-04-01 20:06:27 +03:00
Alexander Krotov
4b742c220c Add Context.get_config_delete_device_after() method
In contrast to get_config_delete_server_after(), value 1 does not mean
"delete at once", because it does not make sense to delete messages
immediately after receivning them.
2020-04-01 20:06:27 +03:00
Alexander Krotov
9d03d441e1 Add Config::DeleteDeviceAfter option 2020-04-01 20:06:27 +03:00
B. Petersen
ff8b249cc6 For now, 'Saved messages' are auto-deleted from the server, but not from device 2020-04-01 20:06:27 +03:00
B. Petersen
248e6ea5e7 make clippy happy 2020-04-01 20:06:27 +03:00
B. Petersen
be0afdebfd target comments of @link2xt, fix ci 2020-04-01 20:06:27 +03:00
B. Petersen
9f19d20344 implement message estimating 2020-04-01 20:06:27 +03:00
B. Petersen
aea8a32ba5 add 'estimatedeletion' to repl tool 2020-04-01 20:06:27 +03:00
B. Petersen
d1a4c82937 prototype ffi for ephemeral messages 2020-04-01 20:06:27 +03:00
Alexander Krotov
4f73812673 Prioritize message deletion over message moving
If Delta Chat goes online and gets an expired message is in the Inbox,
it should delete it instead of moving and then deleting.
2020-04-01 20:06:27 +03:00
Alexander Krotov
33150615a1 Improve logging for server UID updates in precheck_imf()
Log all folders and UIDs and warn about UID changes without folder change.
2020-04-01 20:06:27 +03:00
Alexander Krotov
491f83c86d Remove ServerFolder and ServerUid job parameters
They were used by MarkseenMdnOnImap
2020-04-01 20:06:27 +03:00
Alexander Krotov
41f776763b Remove MarkseenMdnOnImap
MarkseenMdnOnImap stored server folder and UID which are never updated
by update_server_uid. Now hidden entries are created for MDNs, so they
should be handled as ordinary messages.
2020-04-01 20:06:27 +03:00
Alexander Krotov
65fdfac866 Remove unused dest_uid argument from Imap.mv()
It was always set to 0 because we don't know the destination UID.
2020-04-01 20:06:27 +03:00
Alexander Krotov
cb0c00bc6d Log rfc724_mid in DeleteMsgOnImap 2020-04-01 20:06:27 +03:00
Alexander Krotov
ad53678c19 Remove msgs.unlinked column
It is not used anymore.

Database version 64 migration introducing this column is also removed.
2020-04-01 20:06:27 +03:00
Alexander Krotov
62097765a6 update_server_uid: set server_uid even for unlinked messages
Sometimes message deletion job marks message as unlinked without
actually deleting it. It is possible if the message was already moved
into another folder, possibly by second device, but not detected there
yet. It should be detected later in the other folder, and the
server_uid in the database should be set.

Since introduction of add_imap_deletion_jobs() any expired message
record will be marked as unlinked eventually, because another message
deletion job will be scheduled even if update_server_uid() resurrects
the message once.
2020-04-01 20:06:27 +03:00
Alexander Krotov
efb7280e99 Do not schedule delayed DeleteMsgOnImap jobs
All IMAP deletion jobs are scheduled either immediately, or later by
job::add_imap_deletion_jobs().
2020-04-01 20:06:27 +03:00
Alexander Krotov
bdb2a47743 Revert "Automatically delete messages in 2 weeks window only"
This may result in messages not being deleted. Optimization and
traffic-saving is postponed for later, one idea is to optimize message
deletion to avoid checking if Message-ID on the server matches
Message-ID in the database.
2020-04-01 20:06:27 +03:00
Alexander Krotov
c4677190be Postpone DeleteMsgOnImap on error
If job returns Status::Finished, it will be deleted. Then
add_imap_deletion_jobs will recreate it immediately if the message is
expired. To actually backoff the job, we should postpone it instead of
removing.
2020-04-01 20:06:27 +03:00
Alexander Krotov
055aba189c Automatically delete messages in 2 weeks window only
This is to avoid creating thousands of jobs when user enables
"delete_server_after" setting for the first time.

If device is offline for more than 2 weeks, some messages may not be
deleted.
2020-04-01 20:06:27 +03:00
Alexander Krotov
314c3d5e78 add_imap_deletion_jobs: check only for DeleteMsgOnImap jobs
Other jobs may have different meaning for foreign_id.

Also removed " \" at the end of lines.
2020-04-01 20:06:27 +03:00
Alexander Krotov
6db03356b5 Return AlreadyDone from Imap.delete_msg if message is gone
This way DeleteMsgOnImap will remove invalid server_uid from the database.
2020-04-01 20:06:27 +03:00
Alexander Krotov
28af919b09 Create DeleteMsgOnImap jobs before performing IMAP jobs
When "delete_server_after" setting is configured, postponed
DeleteMsgOnImap jobs are created for incoming messages.

This commit adds job::add_imap_deletion_jobs function which creates
DeleteMsgOnImap jobs right before performing IMAP jobs. This way even
messages that expired when the setting was disabled are going to be
deleted.

Job creation on message reception is unnecessary now, and even harmful
because it will create jobs with an expiration time which may later be
reduced. It is planned to be removed in following commits.
2020-04-01 20:06:27 +03:00
Alexander Krotov
8f7a456a39 Use 0 value for "delete_server_after" default.
Now 0 means "never delete", 1 means "delete at once" and other values
indicate the number of seconds after which them message should be
deleted from the server.

Configuration value interpretation is moved into
Context.get_config_delete_server_after() function.
2020-04-01 20:06:27 +03:00
Alexander Krotov
5b3bec1aac Create entries in msgs table for MDNs
At least one entry is required for DeleteMsgOnImap job. Additionally,
adding a hidden entry makes it possible to avoid redownloading the
message if it gets a new UID on the server.
2020-04-01 20:06:27 +03:00
Alexander Krotov
f2aa17c9d0 Resultify MsgId.delete_from_db() 2020-04-01 20:06:27 +03:00
Alexander Krotov
bc06b9e051 Do not send BCC-Self copy if we are going to remove it immediately 2020-04-01 20:06:27 +03:00
Alexander Krotov
6d216af507 Delete BCC-self messages after "delete_server_after" time 2020-04-01 20:06:27 +03:00
Alexander Krotov
b2f1d9f376 Do not remove rfc724_mid for unlinked messages
Message-ID is used to send read receipts. Instead, add a separate
"unlinked" column.
2020-04-01 20:06:27 +03:00
Alexander Krotov
a653e469f2 Add user-configurable option "delete_server_after"
The option sets timer in seconds after which all parts of the message
are deleted from the server.
2020-04-01 20:06:27 +03:00
Alexander Krotov
4f4241ba3a dc_receive_imf: delete all message parts if message should be deleted
DeleteMsgOnImap deletes files from the server only when the last part
is deleted. Removing only the first part of the hidden or trashed
message does not result in message deletion.
2020-04-01 20:06:27 +03:00
Alexander Krotov
2cf9c68040 Implement MsgId.unlink() and use it in DeleteMsgOnImap
Currently only trashed or hidden messages are deleted by
DeleteMsgOnImap, so it is safe to remove database records.

It is planned to delete messages on IMAP server after
user-configurable time to cleanup the server even for messages
displayed in chats. For such messages, we unlink them from the
Message-ID, but keep the database record to display them.
2020-04-01 20:06:27 +03:00
Alexander Krotov
cc0f977d6f Document rfc724_mid_cnt 2020-04-01 20:06:27 +03:00
Alexander Krotov
7879952fde Delete MDNs first in MsgId.delete_from_db() 2020-04-01 20:06:27 +03:00
Alexander Krotov
4452cab987 Turn Message::Delete_from_db into MsgId method 2020-04-01 20:06:27 +03:00
Alexander Krotov
98bd64621a Refactor Imap.delete_msg() error handling
Mutable UID reference is removed. Instead, ImapActionResult is
returned immediately.

If message has changed and UID does not correspond to expected
message, ImapActionResult::Failed is returned. Temprorary IMAP errors
result in ImapActionResult::RetryLater.

On the job side, ImapActionResult::RetryLater does not result in
immediate job retry anymore. Instead, job is retried with a backoff.
2020-04-01 20:06:27 +03:00
Alexander Krotov
c1c769ceb0 Add MsgId.trash() and use it to delete messages locally
In addition to moving the message into trash chat, this function
removes message text to make sure the message does not remain in the
database. Only the information necessary to delete message from the
server and avoid redownloading it should be kept, such as Message-Id
and IMAP UID.
2020-04-01 20:06:27 +03:00
Alexander Krotov
d64e55c66f delete_msgs: remove explicit .iter() 2020-04-01 20:06:27 +03:00
Alexander Krotov
76fc84be37 job: document DeleteMsgOnImap 2020-04-01 20:06:27 +03:00
bjoern
8cd5f5990e Merge pull request #1295 from deltachat/draft_group_consistency
draft doc for healing group-inconsistencies
2020-03-31 18:27:57 +02:00
B. Petersen
6ffe54d68f do no longer ignore keypair generation test, due to the ecc-move, it is no longer expensive 2020-03-31 18:20:40 +02:00
Alexander Krotov
d78ea882c8 Update mailparse to 0.12 2020-03-31 18:11:44 +02:00
Alexander Krotov
958802a233 Add system message only if contact was removed successfully 2020-03-31 18:08:19 +02:00
bjoern
00b02efdc2 Merge pull request #1367 from deltachat/no-forward-to-device-chat
hide the device-chat from default forward-to-lists
2020-03-31 16:03:53 +02:00
Floris Bruynooghe
50569f12f5 Remove unsafe CString::yolo from ffi
CString::yolo was still used in the ffi, this was an unsafe
transitional thing.  To remove it there were two choices: 1. make
errors in creating CStrings hard errors or 2. try and be as lenient as
possible.  Given the to_string_lossy() convention adopted in the ffi
this choose the lenient option and simply skips over embedded null
bytes, leaving the rest of the strings intact.

Thus now CString::new_lossy().  It's only used for .strdup() however
so no longer a public trait.

This also cleans up the public visibility of things in the strings.rs
file:

- Rename StrExt/OptStrExt traits to what they actually do: provide
  .strdup() -> Strdup/OptStrdup.

- dc_strdup() should be an implementation detail, replace all usages
  with Strdup.strdup() method.

- Only allow visibility inside the crate for all things.

- Reduce visibility to only the module for things not used in lib.rs.
2020-03-31 09:12:41 +02:00
B. Petersen
8aa4ceb570 hide the device-chat from default forward-to-lists 2020-03-30 15:59:07 +02:00
Alexander Krotov
a7afbf85ad Replace test Alice key with ed25519 key
Autocrypt Setup Message does not contain "==" anymore since key length
has changed.
2020-03-29 19:52:45 +03:00
bjoern
8fdf3dcdb8 Merge pull request #1314 from deltachat/fix-secure-join
ignore handshake messages seen from another device
2020-03-28 10:28:55 +01:00
B. Petersen
818c20e0cb switch to ecc keys
after fixing some issues wrt ecc keys, see #1319,
and waiting some time (three core releases, two ios/android/desktop releases),
it is now the time to switch again to ecc keys again,
after the first attempt was stopped in #1319
2020-03-28 01:05:24 +03:00
dignifiedquire
49b2f80ded Merge remote-tracking branch 'origin/master' into feat/async-jobs 2020-03-26 16:52:07 +01:00
bjoern
c1d4996777 Merge pull request #1361 from deltachat/prep-1.28
Prep 1.28
2020-03-25 22:09:34 +01:00
B. Petersen
fd3e6e0ee4 bump version to 1.28 2020-03-25 19:30:52 +01:00
B. Petersen
edc5754c68 changelog 2020-03-25 19:29:44 +01:00
bjoern
bd75dea000 Merge pull request #1359 from deltachat/fix-imap-delimiter
fix imap delimiter
2020-03-25 19:01:58 +01:00
B. Petersen
e09a0a548f using first() instead of [0] 2020-03-25 18:10:02 +01:00
bjoern
15ee8b4362 Merge pull request #1357 from deltachat/ad-hoc-groups
fix classic-email interactions
2020-03-25 17:57:59 +01:00
dignifiedquire
f8cd602cbd happy python lint 2020-03-25 17:56:35 +01:00
dignifiedquire
97951aec15 implement imape secure upgrade 2020-03-25 15:41:27 +01:00
dignifiedquire
3871c5a4a0 fix some more python test 2020-03-25 13:48:02 +01:00
B. Petersen
ab8d75b192 remove now unused ImapConfig.imap_delimiter 2020-03-25 13:23:29 +01:00
B. Petersen
e135c969c9 figure out and use folder-delimiter provided by LIST command 2020-03-25 13:16:23 +01:00
B. Petersen
36e7090466 force folder-reconfigure on dc_configure() 2020-03-25 02:39:48 +01:00
B. Petersen
f28f177c6b use constant for folders_configured cache 2020-03-24 19:18:18 +01:00
B. Petersen
785973c624 allow ad-hoc-group creation if there is an accepted chat with the contact 2020-03-24 16:47:54 +01:00
B. Petersen
9c06acff72 add tests wrt classic email-interaction 2020-03-24 16:47:54 +01:00
dignifiedquire
69f1e1753c improve logging and avoid race 2020-03-23 19:49:37 +01:00
bjoern
4fabddeb47 Merge pull request #1354 from deltachat/fix-bcc-self-group-crash
fix crash in self-only-groups with bcc_self enabled
2020-03-23 12:21:27 +01:00
B. Petersen
17ff1ab372 this corrects the To:-list for group-messages with only SELF as member.
before, the list was empty
and trying to send to groups that only contain SELF lead to a crash.

in theory, this happens for both, bcc_self enabled or not, however,
if bcc_self was disabled (default setting),
things worked as the whole mimerendering was skipped on a higher level.
also, the saved-messages-chat was not affected as this was checked explicitly.

this pr changes the mimerendering so that From: is used as To:
if there is no recipient-list and the messasge will be sent to SELF only.
2020-03-23 01:48:27 +01:00
dignifiedquire
01b88f876e fix idle interrupts 2020-03-23 00:09:22 +01:00
dignifiedquire
0ead27a05b update toolchain 2020-03-22 22:33:07 +01:00
dignifiedquire
c9742bb6ea update toolchain for gh actions 2020-03-22 22:13:02 +01:00
dignifiedquire
d50c1e3658 fixes from merge with master 2020-03-22 22:05:37 +01:00
dignifiedquire
3a9c2a0356 Merge remote-tracking branch 'origin/master' into feat/async-jobs 2020-03-22 22:02:42 +01:00
dignifiedquire
b616a2b3e7 fix more tests 2020-03-22 21:57:26 +01:00
B. Petersen
3c34096392 add a failing test that crashes when sending a message to a self-only group with bcc_self enabled 2020-03-22 18:23:55 +01:00
bjoern
70e0d3b571 Merge pull request #1353 from deltachat/show-nondc-replies
fix showing non-dc replies
2020-03-22 16:48:49 +01:00
dignifiedquire
20ef115eb2 fix various integration tests with the python bindings 2020-03-22 16:21:15 +01:00
B. Petersen
ae5a2396f3 fix typo 2020-03-22 16:18:53 +01:00
B. Petersen
8f82bf40e0 let function that search for Message-IDs accept widly used square brackets format 2020-03-22 12:20:28 +01:00
B. Petersen
fe398de2fa add test that fail on checking existance of a Message-ID with angle brackets 2020-03-22 12:20:28 +01:00
Alexander Krotov
a770d75e2e Fix condition in normalize_name() 2020-03-22 00:11:41 +00:00
Alexander Krotov
a330104e9b Add tests that crash normalize_name() 2020-03-22 00:11:41 +00:00
dignifiedquire
9b4c195872 minimal get_next_event api 2020-03-22 01:07:06 +01:00
dignifiedquire
11fa60d690 stop python tests from blowing up 2020-03-21 23:37:52 +01:00
dignifiedquire
1214609546 get ffi api to compile 2020-03-21 23:13:38 +01:00
dignifiedquire
d798356c19 refactor stop logic 2020-03-21 19:05:50 +01:00
dignifiedquire
4d6a9e4f14 improve simple example 2020-03-21 17:00:07 +01:00
dignifiedquire
8a7eaba668 fix imap fetch loop and watch folders 2020-03-21 16:26:27 +01:00
dignifiedquire
1846f20f6e upgrade repl 2020-03-21 14:24:41 +01:00
dignifiedquire
18c1787552 Merge remote-tracking branch 'origin/master' into feat/async-jobs 2020-03-21 12:17:27 +01:00
bjoern
aae3cae4bb Merge pull request #1342 from deltachat/group-rejoin-bug
Rebuild group member list on group rejoin
2020-03-20 00:39:26 +01:00
Alexander Krotov
e7e4821804 Reset group member list when we are added to the group
This makes test_synchronize_member_list_on_group_rejoin pass.
2020-03-19 17:03:31 +03:00
Alexander Krotov
9654802acc Add failing group rejoin test 2020-03-19 17:03:31 +03:00
Friedel Ziegelmayer
06a24fa4d0 Merge pull request #1345 from deltachat/ci/fix-online-tests
ci: ensure dcc_new_temp_email is propagated
2020-03-19 15:02:30 +01:00
dignifiedquire
62b1b0519a ci: ensure dcc_new_temp_email is propagated
Closes #1344
2020-03-19 11:14:37 +01:00
dignifiedquire
9981e84a42 avoid race condition on scheduler start 2020-03-18 16:34:18 +01:00
dignifiedquire
9d313b4e0e cleanup and fix most of imap 2020-03-18 16:29:34 +01:00
dignifiedquire
ab2cb1ad1f add missing loops 2020-03-18 15:31:37 +01:00
dignifiedquire
f85b14a7f7 the basics work 2020-03-18 15:11:36 +01:00
dignifiedquire
94c6a01420 some cleanup 2020-03-18 02:23:22 +01:00
dignifiedquire
563b550f08 integrate imex and imap 2020-03-18 01:50:19 +01:00
dignifiedquire
aa45716ef7 cleanup and setup configure 2020-03-18 00:36:13 +01:00
dignifiedquire
846ef043d5 hook up scheduler with jobs 2020-03-18 00:01:59 +01:00
dignifiedquire
ce5b95f8e5 start setting up new scheduler 2020-03-17 13:48:03 +01:00
dignifiedquire
efc17983c3 switch to queue based logging 2020-03-17 10:07:52 +01:00
dignifiedquire
7140898db9 async file io 2020-03-14 16:26:15 +01:00
dignifiedquire
6db253e1cc rebase fixes 2020-03-14 15:21:19 +01:00
dignifiedquire
4c9d049b10 blocking for with_conn 2020-03-14 15:17:47 +01:00
dignifiedquire
818e921192 it compiles 2020-03-14 15:17:47 +01:00
dignifiedquire
6ea1d665bb start making sql async 2020-03-14 15:17:23 +01:00
dignifiedquire
7326ba1403 send bounds wip 2020-03-14 15:16:27 +01:00
dignifiedquire
62bfa5157b async sleep 2020-03-14 15:16:27 +01:00
dignifiedquire
43a8828430 asyncify smtp thread state handling 2020-03-14 15:16:27 +01:00
dignifiedquire
618202cf8b more async 2020-03-14 15:16:27 +01:00
dignifiedquire
9614a23506 first pass at async job 2020-03-14 15:14:49 +01:00
bjoern
10afdfecdd Merge pull request #1336 from deltachat/forward-to-saved
optionally sort 'saved messages' atop of the chatlist
2020-03-11 22:05:50 +01:00
B. Petersen
c0e08fb927 fix typo 2020-03-11 16:17:29 +01:00
B. Petersen
6d6bc9b050 for forwarding, sort 'saved messages' atop of the chatlist 2020-03-09 23:30:22 +01:00
Alexander Krotov
4714fb6887 Reset server_folder and server_uid in Imap.empty_folder()
This way we avoid trying to delete already deleted messages in the future.
2020-03-09 01:21:12 +03:00
bjoern
5f47810964 Merge pull request #1328 from deltachat/prep-1.27
Prep 1.27
2020-03-04 18:37:31 +01:00
B. Petersen
0f6024e055 bump version to 1.27 2020-03-04 17:04:26 +01:00
B. Petersen
fafc15f80c changelog 2020-03-04 16:55:44 +01:00
bjoern
9a85ea861d Merge pull request #1327 from deltachat/fix/update-rpgp
fix: update to pgp@0.5
2020-03-04 16:49:56 +01:00
dignifiedquire
9541960307 update fixed rpgp 2020-03-04 15:10:19 +01:00
dignifiedquire
95073deb96 fix: update to pgp@0.5 2020-03-04 14:51:58 +01:00
Alexander Krotov
82b4647b95 Update dc_truncate() comment 2020-03-02 23:09:38 +03:00
Alexander Krotov
0c770a8b37 Fix Origin::AddressBook spelling 2020-03-02 22:57:22 +03:00
B. Petersen
0e4031348f remove unwrap-flag from dc_truncate() - it was never really used on-purpose 2020-03-02 14:10:44 +01:00
B. Petersen
5cc26762c2 unwrap lineends in summaries 2020-03-02 14:10:44 +01:00
B. Petersen
b8b4853d1f add failing test for checking linebreak-unwrapping in get_summary 2020-03-02 14:10:44 +01:00
Alexander Krotov
fbabe27fc1 Make add_or_lookup errors non-fatal in add_address_book() 2020-03-02 14:08:20 +01:00
bjoern
4d1554c85b Merge pull request #1323 from deltachat/prep-1.26
prepare 1.26.0
2020-03-01 22:34:37 +01:00
B. Petersen
ab40495d5c update rPGP in .toml, this was missed in #1321 2020-03-01 21:54:11 +01:00
B. Petersen
42ebf49f92 bump version to 1.26.0 2020-03-01 21:46:26 +01:00
B. Petersen
c5ccd88f79 changelog 2020-03-01 21:43:57 +01:00
B. Petersen
dbd1b227d9 revert generating ecc keys for now
the currently released versions fail sometimes in encrypting to ecc keys,
see #1313, the issue is about to be fixed,
however, we should not generate ecc keys until the
fix is rolled out - otherwise new users will get encryption errors every some
messages if their counterpart is not yet using the most recent version.

we can start generating ecc keys a few weeks after the fix is rolled out.
2020-03-01 21:34:04 +03:00
Alexander Krotov
63baac3c61 Update rPGP to 0.4.1
This version fixes incorrect serialization of ECC session keys with
leading zeros.
2020-03-01 20:52:08 +03:00
B. Petersen
7c39bb6659 ignore handshake messages seen from another device
moreover, prepare for marking peers as verified accross devices,
see detailed comment for observe_securejoin_on_other_device()
2020-02-27 15:04:11 +01:00
holger krekel
13e361aabc add two variants 2020-02-19 14:21:15 +01:00
holger krekel
d1a26e66a7 update 2020-02-19 13:48:05 +01:00
holger krekel
ffe3c84e7c small refinements 2020-02-19 13:39:33 +01:00
holger krekel
702c7382a7 move 2020-02-19 13:35:07 +01:00
holger krekel
b138d486e4 two ideas to tackle group consistency 2020-02-19 13:29:58 +01:00
116 changed files with 16134 additions and 12838 deletions

View File

@@ -7,6 +7,10 @@ executors:
doxygen:
docker:
- image: hrektts/doxygen
python:
docker:
- image: 3.7.7-stretch
restore-workspace: &restore-workspace
attach_workspace:

View File

@@ -10,7 +10,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2019-11-06
toolchain: nightly-2020-03-19
override: true
- uses: actions-rs/cargo@v1
with:
@@ -25,7 +25,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2019-11-06
toolchain: nightly-2020-03-19
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
@@ -39,10 +39,10 @@ jobs:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly-2019-11-06
toolchain: nightly-2020-03-19
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features
args: --all-features

54
ASYNC-API-TODO.txt Normal file
View File

@@ -0,0 +1,54 @@
Delta Chat ASYNC (friedel, bjoern, floris, friedel)
- smtp fake-idle/load jobs gerade noch alle fuenf sekunden , sollte alle zehn minuten (oder gar nicht)
APIs:
dc_context_new # opens the database
dc_open # FFI only
-> drop it and move parameters to dc_context_new()
dc_configure # note: dc_start_jobs() is NOT allowed to run concurrently
dc_imex NEVER goes through the job system
dc_imex import_backup needs to ensure dc_stop_jobs()
dc_start_io # start smtp/imap and job handling subsystems
dc_stop_io # stop smtp/imap and job handling subsystems
dc_is_io_running # return 1 if smtp/imap/jobs susbystem is running
dc_close # FFI only
-> can be dropped
dc_context_unref
for ios share-extension:
Int dc_direct_send() -> try send out without going through jobs system, but queue a job in db if it needs to be retried on failure
0: message was sent
1: message failed to go out, is queued as a job to be retried later
2: message permanently failed?
EVENT handling:
start a callback thread and call get_next_event() which is BLOCKING
it's fine to start this callback thread later, it will see all events.
Note that the core infinitely fills the internal queue if you never drain it.
FFI-get_next_event() returns NULL if the context is unrefed already?
sidenote: how python's callback thread does it currently:
CB-thread runs this while loop:
while not QUITFLAG:
ev = context.get_next_event( )
...
So in order to shutdown properly one has to set QUITFLAG
before calling dc_stop_jobs() and dc_context_unref
event API:
get_data1_int
get_data2_int
get_data3_str
- userdata likely only used for the callbacks, likely can be dropped, needs verification
- iOS needs for the share app to call "try_send_smtp" wihtout a full dc_context_run and without going

View File

@@ -1,5 +1,109 @@
# Changelog
## 1.33.0
- let `dc_set_muted()` also mute one-to-one chats #1470
- fix a bug that led to load and traffic if the server does not use sent-folder
#1472
## 1.32.0
- fix endless loop when trying to download messages with bad RFC Message-ID,
also be more reliable on similar errors #1463 #1466 #1462
- fix bug with comma in contact request #1438
- do not refer to hidden messages on replies #1459
- improve error handling #1468 #1465 #1464
## 1.31.0
- always describe the context of the displayed error #1451
- do not emit `DC_EVENT_ERROR` when message sending fails;
`dc_msg_get_state()` and `dc_get_msg_info()` are sufficient #1451
- new config-option `media_quality` #1449
- try over if writing message to database fails #1447
## 1.30.0
- expunge deleted messages #1440
- do not send `DC_EVENT_MSGS_CHANGED|INCOMING_MSG` on hidden messages #1439
## 1.29.0
- new config options `delete_device_after` and `delete_server_after`,
each taking an amount of seconds after which messages
are deleted from the device and/or the server #1310 #1335 #1411 #1417 #1423
- new api `dc_estimate_deletion_cnt()` to estimate the effect
of `delete_device_after` and `delete_server_after`
- use Ed25519 keys by default, these keys are much shorter
than RSA keys, which results in saving traffic and speed improvements #1362
- improve message ellipsizing #1397 #1430
- emit `DC_EVENT_ERROR_NETWORK` also on smtp-errors #1378
- do not show badly formatted non-delta-messages as empty #1384
- try over SMTP on potentially recoverable error 5.5.0 #1379
- remove device-chat from forward-to-chat-list #1367
- improve group-handling #1368
- `dc_get_info()` returns uptime (how long the context is in use)
- python improvements and adaptions #1408 #1415
- log to the stdout and stderr in tests #1416
- refactoring, code improvements #1363 #1365 #1366 #1370 #1375 #1389 #1390 #1418 #1419
- removed api: `dc_chat_get_subtitle()`, `dc_get_version_str()`, `dc_array_add_id()`
- removed events: `DC_EVENT_MEMBER_ADDED`, `DC_EVENT_MEMBER_REMOVED`
## 1.28.0
- new flag DC_GCL_FOR_FORWARDING for dc_get_chatlist()
that will sort the "saved messages" chat to the top of the chatlist #1336
- mark mails as being deleted from server in dc_empty_server() #1333
- fix interaction with servers that do not allow folder creation on root-level;
use path separator as defined by the email server #1359
- fix group creation if group was created by non-delta clients #1357
- fix showing replies from non-delta clients #1353
- fix member list on rejoining left groups #1343
- fix crash when using empty groups #1354
- fix potential crash on special names #1350
## 1.27.0
- handle keys reliably on armv7 #1327
## 1.26.0
- change generated key type back to RSA as shipped versions
have problems to encrypt to Ed25519 keys
- update rPGP to encrypt reliably to Ed25519 keys;
one of the next versions can finally use Ed25519 keys then
## 1.25.0
- save traffic by downloading only messages that are really displayed #1236

3018
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +1,45 @@
[package]
name = "deltachat"
version = "1.25.0"
version = "1.33.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
[profile.release]
lto = true
# lto = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
libc = "0.2.51"
pgp = { version = "0.4.0", default-features = false }
pgp = { version = "0.5.1", default-features = false }
hex = "0.4.0"
sha2 = "0.8.0"
rand = "0.7.0"
smallvec = "1.0.0"
reqwest = { version = "0.10.0", features = ["blocking", "json"] }
surf = { version = "2.0.0-alpha.2", default-features = false, features = ["h1-client"] }
num-derive = "0.3.0"
num-traits = "0.2.6"
async-smtp = "0.2"
async-smtp = { version = "0.3" }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
async-imap = "0.2"
async-native-tls = "0.3.1"
async-std = { version = "1.4", features = ["unstable"] }
async-imap = "0.3.0"
async-native-tls = { version = "0.3.3" }
async-std = { version = "1.6.0", features = ["unstable"] }
base64 = "0.11"
charset = "0.1"
percent-encoding = "2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4.6"
failure = "0.1.5"
failure_derive = "0.1.5"
indexmap = "1.3.0"
lazy_static = "1.4.0"
regex = "1.1.6"
rusqlite = { version = "0.21", features = ["bundled"] }
r2d2_sqlite = "0.13.0"
rusqlite = { version = "0.22", features = ["bundled"] }
r2d2_sqlite = "0.15.0"
r2d2 = "0.8.5"
strum = "0.16.0"
strum_macros = "0.16.0"
thread-local-object = "0.1.0"
backtrace = "0.3.33"
byteorder = "1.3.1"
itertools = "0.8.0"
@@ -53,19 +50,28 @@ bitflags = "1.1.0"
debug_stub_derive = "0.3.0"
sanitize-filename = "0.2.1"
stop-token = { version = "0.1.1", features = ["unstable"] }
mailparse = "0.10.2"
mailparse = "0.12.0"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
native-tls = "0.2.3"
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
pretty_env_logger = "0.3.1"
futures = "0.3.4"
thiserror = "1.0.14"
anyhow = "1.0.28"
async-trait = "0.1.31"
url = "2.1.1"
pretty_env_logger = { version = "0.3.1", optional = true }
log = {version = "0.4.8", optional = true }
rustyline = { version = "4.1.0", optional = true }
ansi_term = { version = "0.12.1", optional = true }
[dev-dependencies]
tempfile = "3.0"
pretty_assertions = "0.6.1"
pretty_env_logger = "0.3.0"
proptest = "0.9.4"
async-std = { version = "1.6.0", features = ["unstable", "attributes"] }
[workspace]
members = [
@@ -80,11 +86,13 @@ path = "examples/simple.rs"
[[example]]
name = "repl"
path = "examples/repl/main.rs"
required-features = ["rustyline"]
required-features = ["repl"]
[features]
default = ["nightly", "ringbuf"]
vendored = ["async-native-tls/vendored", "reqwest/native-tls-vendored", "async-smtp/native-tls-vendored"]
default = []
internals = []
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term"]
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored"]
nightly = ["pgp/nightly"]
ringbuf = ["pgp/ringbuf"]

View File

@@ -9,7 +9,7 @@
To download and install the official compiler for the Rust programming language, and the Cargo package manager, run the command in your user environment:
```
curl https://sh.rustup.rs -sSf | sh
$ curl https://sh.rustup.rs -sSf | sh
```
## Using the CLI client
@@ -17,8 +17,9 @@ curl https://sh.rustup.rs -sSf | sh
Compile and run Delta Chat Core command line utility, using `cargo`:
```
cargo run --example repl -- /path/to/db
$ RUST_LOG=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.
Configure your account (if not already configured):
@@ -108,7 +109,6 @@ $ cargo test -- --ignored
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
- `nightly`: Enable nightly only performance and security related features.
- `ringbuf`: Enable the use of [`slice_deque`](https://github.com/gnzlbg/slice_deque) in pgp.
[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/

View File

@@ -4,7 +4,7 @@ environment:
install:
- appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
- rustup-init -yv --default-toolchain nightly-2019-07-10
- rustup-init -yv --default-toolchain nightly-2020-03-19
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin
- rustc -vV
- cargo -vV

View File

@@ -37,7 +37,7 @@ echo -----------------------
# Bundle external shared libraries into the wheels
pushd $WHEELHOUSEDIR
pip3 install -U pip
pip3 install -U pip setuptools
pip3 install devpi-client
devpi use https://m.devpi.net
devpi login dc --password $DEVPI_LOGIN

View File

@@ -48,7 +48,7 @@ def run():
projectnames = get_projectnames(baseurl, username, indexname)
if indexname == "master" or not indexname:
continue
assert projectnames == ["deltachat"]
clear_index = not projectnames
for projectname in projectnames:
dates = get_release_dates(baseurl, username, indexname, projectname)
if not dates:
@@ -60,8 +60,11 @@ def run():
date = datetime.datetime(*max(dates))
if (datetime.datetime.now() - date) > datetime.timedelta(days=MAXDAYS):
assert username and indexname
url = baseurl + username + "/" + indexname
subprocess.check_call(["devpi", "index", "-y", "--delete", url])
clear_index = True
break
if clear_index:
url = baseurl + username + "/" + indexname
subprocess.check_call(["devpi", "index", "-y", "--delete", url])

View File

@@ -1,4 +1,4 @@
FROM quay.io/pypa/manylinux1_x86_64
FROM quay.io/pypa/manylinux2010_x86_64
# Configure ld.so/ldconfig and pkg-config
RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \

View File

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

View File

@@ -46,6 +46,7 @@ if [ -n "$TESTS" ]; then
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
popd

View File

@@ -32,11 +32,11 @@ ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
set +x -e
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_PY_LIVECONFIG \
docker run -e DCC_NEW_TMP_EMAIL -e DCC_PY_LIVECONFIG \
--rm -it -v \$(pwd):/mnt -w /mnt \
deltachat/coredeps ci_scripts/run_all.sh
@@ -46,6 +46,6 @@ echo "--- Running $CIRCLE_JOB remotely"
ssh -t $SSHTARGET bash "$BUILDDIR/exec_docker_run"
mkdir -p workspace
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse/*manylinux1*" workspace/wheelhouse/
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse/*manylinux201*" workspace/wheelhouse/
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/dist/*" workspace/wheelhouse/
rsync -avz "$SSHTARGET:$BUILDDIR/python/doc/_build/" workspace/py-docs

View File

@@ -30,6 +30,7 @@ ssh $SSHTARGET <<_HERE
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
#rm -rf virtualenv venv

View File

@@ -21,7 +21,8 @@ export DCC_RS_DEV=$(pwd)
export PATH=$PATH:/opt/python/cp35-cp35m/bin
export PYTHONDONTWRITEBYTECODE=1
pushd /bin
ln -s /opt/python/cp27-cp27m/bin/python2.7
rm -f python3.5
ln -s /opt/python/cp35-cp35m/bin/python3.5
ln -s /opt/python/cp36-cp36m/bin/python3.6
ln -s /opt/python/cp37-cp37m/bin/python3.7
ln -s /opt/python/cp38-cp38/bin/python3.8
@@ -37,7 +38,8 @@ 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_PY_LIVECONFIG
unset DCC_NEW_TMP_EMAIL
tox --workdir "$TOXWORKDIR" -e py35,py36,py37,py38,auditwheels
popd

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.25.0"
version = "1.33.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -19,11 +19,13 @@ deltachat = { path = "../", default-features = false }
libc = "0.2"
human-panic = "1.0.1"
num-traits = "0.2.6"
failure = "0.1.6"
serde_json = "1.0"
async-std = "1.6.0"
anyhow = "1.0.28"
thiserror = "1.0.14"
[features]
default = ["vendored", "nightly", "ringbuf"]
default = ["vendored"]
vendored = ["deltachat/vendored"]
nightly = ["deltachat/nightly"]
ringbuf = ["deltachat/ringbuf"]

View File

@@ -19,6 +19,8 @@ typedef struct _dc_msg dc_msg_t;
typedef struct _dc_contact dc_contact_t;
typedef struct _dc_lot dc_lot_t;
typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
/**
@@ -41,7 +43,7 @@ typedef struct _dc_provider dc_provider_t;
* uintptr_t event_handler_func(dc_context_t* context, int event,
* uintptr_t data1, uintptr_t data2)
* {
* return 0; // for unhandled events, it is always safe to return 0
* return 0;
* }
*
* dc_context_t* context = dc_context_new(event_handler_func, NULL, NULL);
@@ -162,7 +164,7 @@ typedef struct _dc_provider dc_provider_t;
* - Strings in function arguments or return values are usually UTF-8 encoded.
*
* - The issue-tracker for the core library is here:
* <https://github.com/deltachat/deltachat-core/issues>
* <https://github.com/deltachat/deltachat-core-rust/issues>
*
* The following points are important mainly
* for the authors of the library itself:
@@ -189,6 +191,24 @@ typedef struct _dc_provider dc_provider_t;
*/
/**
* TODO: document
*/
dc_event_t* dc_get_next_event(dc_event_emitter_t* emitter);
dc_event_emitter_t* dc_get_event_emitter(dc_context_t* context);
void dc_event_emitter_unref(dc_event_emitter_t* emitter);
int dc_event_get_id (dc_event_t* event);
int dc_event_get_data1_int(dc_event_t* event);
int dc_event_get_data2_int(dc_event_t* event);
char* dc_event_get_data3_str(dc_event_t* event);
/**
* TODO: document
*/
void dc_event_unref (dc_event_t* event);
/**
* @class dc_context_t
*
@@ -199,20 +219,6 @@ typedef struct _dc_provider dc_provider_t;
* settings.
*/
/**
* Callback function that should be given to dc_context_new().
*
* @memberof dc_context_t
* @param context The context object as returned by dc_context_new().
* @param event one of the @ref DC_EVENT constants
* @param data1 depends on the event parameter
* @param data2 depends on the event parameter
* @return return 0 unless stated otherwise in the event parameter documentation
*/
typedef uintptr_t (*dc_callback_t) (dc_context_t* context, int event, uintptr_t data1, uintptr_t data2);
// create/open/config/information
/**
@@ -229,9 +235,12 @@ typedef uintptr_t (*dc_callback_t) (dc_context_t* context, int event, uintptr_t
* otherwise!
* - The callback SHOULD return _fast_, for GUI updates etc. you should
* post yourself an asynchronous message to your GUI thread, if needed.
* - If not mentioned otherweise, the callback should return 0.
* @param userdata can be used by the client for any purpuse. He finds it
* later in dc_get_userdata().
* - events do not expect a return value, just always return 0.
* @param dbfile The file to use to store the database, something like `~/file` won't
* work on all systems, if in doubt, use absolute paths.
* @param blobdir A directory to store the blobs in; a trailing slash is not needed.
* If you pass NULL or the empty string, deltachat-core creates a directory
* beside _dbfile_ with the same name and the suffix `-blobs`.
* @param os_name is only for decorative use
* and is shown eg. in the `X-Mailer:` header
* in the form "Delta Chat Core <version>/<os_name>".
@@ -243,7 +252,7 @@ typedef uintptr_t (*dc_callback_t) (dc_context_t* context, int event, uintptr_t
* The object must be passed to the other context functions
* and must be freed using dc_context_unref() after usage.
*/
dc_context_t* dc_context_new (dc_callback_t cb, void* userdata, const char* os_name);
dc_context_t* dc_context_new (const char* os_name, const char* dbfile, const char* blobdir);
/**
@@ -260,60 +269,6 @@ dc_context_t* dc_context_new (dc_callback_t cb, void* userdata,
*/
void dc_context_unref (dc_context_t* context);
/**
* Get user data associated with a context object.
*
* @memberof dc_context_t
* @param context The context object as created by dc_context_new().
* @return User data, this is the second parameter given to dc_context_new().
*/
void* dc_get_userdata (dc_context_t* context);
/**
* Open context database. If the given file does not exist, it is
* created and can be set up using dc_set_config() afterwards.
*
* @memberof dc_context_t
* @param context The context object as created by dc_context_new().
* @param dbfile The file to use to store the database, something like `~/file` won't
* work on all systems, if in doubt, use absolute paths.
* @param blobdir A directory to store the blobs in; a trailing slash is not needed.
* If you pass NULL or the empty string, deltachat-core creates a directory
* beside _dbfile_ with the same name and the suffix `-blobs`.
* @return 1 on success, 0 on failure
* eg. if the file is not writable
* or if there is already a database opened for the context.
*/
int dc_open (dc_context_t* context, const char* dbfile, const char* blobdir);
/**
* Close context database opened by dc_open().
* Before this, connections to SMTP and IMAP are closed; these connections
* are started automatically as needed eg. by sending for fetching messages.
* This function is also implicitly called by dc_context_unref().
* Multiple calls to this functions are okay, the function takes care not
* to free objects twice.
*
* @memberof dc_context_t
* @param context The context object as created by dc_context_new().
* @return None.
*/
void dc_close (dc_context_t* context);
/**
* Check if the context database is open.
*
* @memberof dc_context_t
* @param context The context object as created by dc_context_new().
* @return 0=context is not open, 1=context is open.
*/
int dc_is_open (const dc_context_t* context);
/**
* Get the blob directory.
*
@@ -342,7 +297,8 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `smtp_certificate_checks` = how to check SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way eg. using CC, defaults to empty
* - `selfstatus` = Own status to display eg. in email footers, defaults to a standard text
* - `selfavatar` = File containing avatar. Will be copied to blob directory.
* - `selfavatar` = File containing avatar. Will immediately be copied to the
* `blobdir`; the original image will not be needed anymore.
* NULL to remove the avatar.
* It is planned for future versions
* to send this image together with the next messages.
@@ -373,6 +329,25 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `save_mime_headers` = 1=save mime headers
* and make dc_get_mime_headers() work for subsequent calls,
* 0=do not save mime headers (default)
* - `delete_device_after` = 0=do not delete messages from device automatically (default),
* >=1=seconds, after which messages are deleted automatically from the device.
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
* Messages are deleted whether they were seen or not, the UI should clearly point that out.
* See also dc_estimate_deletion_cnt().
* - `delete_server_after` = 0=do not delete messages from server automatically (default),
* 1=delete messages directly after receiving from server, mvbox is skipped.
* >1=seconds, after which messages are deleted automatically from the server, mvbox is used as defined.
* "Saved messages" are deleted from the server as well as
* emails matching the `show_emails` settings above, the UI should clearly point that out.
* See also dc_estimate_deletion_cnt().
* - `media_quality` = DC_MEDIA_QUALITY_BALANCED (0) =
* good outgoing images/videos/voice quality at reasonable sizes (default)
* DC_MEDIA_QUALITY_WORSE (1)
* allow worse images/videos/voice quality to gain smaller sizes,
* suitable for providers or areas known to have a bad connection.
* In contrast to other options, the implementation of this option is currently up to the UIs;
* this may change in future, however,
* having the option in the core allows provider-specific-defaults already today.
*
* If you want to retrieve a value, use dc_get_config().
*
@@ -557,311 +532,36 @@ void dc_configure (dc_context_t* context);
* @return 1=context is configured and can be used;
* 0=context is not configured and a configuration by dc_configure() is required.
*/
int dc_is_configured (const dc_context_t* context);
int dc_is_configured (const dc_context_t* context);
/**
* Execute pending imap-jobs.
* This function and dc_perform_imap_fetch() and dc_perform_imap_idle()
* must be called from the same thread, typically in a loop.
*
* Example:
*
* void* imap_thread_func(void* context)
* {
* while (true) {
* dc_perform_imap_jobs(context);
* dc_perform_imap_fetch(context);
* dc_perform_imap_idle(context);
* }
* }
*
* // start imap-thread that runs forever
* pthread_t imap_thread;
* pthread_create(&imap_thread, NULL, imap_thread_func, context);
*
* ... program runs ...
*
* // network becomes available again -
* // the interrupt causes dc_perform_imap_idle() in the thread above
* // to return so that jobs are executed and messages are fetched.
* dc_maybe_network(context);
* Start job and IMAP/SMTP tasks.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
* @param context The context object as created by dc_context_new().
* @return None
*/
void dc_perform_imap_jobs (dc_context_t* context);
void dc_start_io (dc_context_t* context);
/**
* Fetch new messages, if any.
* This function and dc_perform_imap_jobs() and dc_perform_imap_idle() must be called from the same thread,
* typically in a loop.
*
* See dc_perform_imap_jobs() for an example.
* Check if IO (SMTP/IMAP/Jobs) has been started.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
* @param context The context object as created by dc_context_new().
* @return 1=IO is running;
* 0=IO is not running.
*/
void dc_perform_imap_fetch (dc_context_t* context);
int dc_is_io_running(const dc_context_t* context);
/**
* Wait for messages or jobs.
* This function and dc_perform_imap_jobs() and dc_perform_imap_fetch() must be called from the same thread,
* typically in a loop.
*
* You should call this function directly after calling dc_perform_imap_fetch().
*
* See dc_perform_imap_jobs() for an example.
* Stop job and IMAP/SMTP tasks and return when they are finished.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
* @param context The context object as created by dc_context_new().
* @return None
*/
void dc_perform_imap_idle (dc_context_t* context);
/**
* Interrupt waiting for imap-jobs.
* If dc_perform_imap_jobs(), dc_perform_imap_fetch() and dc_perform_imap_idle() are called in a loop,
* calling this function causes imap-jobs to be executed and messages to be fetched.
*
* dc_interrupt_imap_idle() does _not_ interrupt dc_perform_imap_jobs() or dc_perform_imap_fetch().
* If the imap-thread is inside one of these functions when dc_interrupt_imap_idle() is called, however,
* the next call of the imap-thread to dc_perform_imap_idle() is interrupted immediately.
*
* Internally, this function is called whenever a imap-jobs should be processed
* (delete message, markseen etc.).
*
* When you need to call this function just because to get jobs done after network changes,
* use dc_maybe_network() instead.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_interrupt_imap_idle (dc_context_t* context);
/**
* Execute pending mvbox-jobs.
* This function and dc_perform_mvbox_fetch() and dc_perform_mvbox_idle()
* must be called from the same thread, typically in a loop.
*
* Example:
*
* void* mvbox_thread_func(void* context)
* {
* while (true) {
* dc_perform_mvbox_jobs(context);
* dc_perform_mvbox_fetch(context);
* dc_perform_mvbox_idle(context);
* }
* }
*
* // start mvbox-thread that runs forever
* pthread_t mvbox_thread;
* pthread_create(&mvbox_thread, NULL, mvbox_thread_func, context);
*
* ... program runs ...
*
* // network becomes available again -
* // the interrupt causes dc_perform_mvbox_idle() in the thread above
* // to return so that jobs are executed and messages are fetched.
* dc_maybe_network(context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_mvbox_jobs (dc_context_t* context);
/**
* Fetch new messages from the MVBOX, if any.
* The MVBOX is a folder on the account where chat messages are moved to.
* The moving is done to not disturb shared accounts that are used by both,
* Delta Chat and a classical MUA.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_mvbox_fetch (dc_context_t* context);
/**
* Wait for messages or jobs in the MVBOX-thread.
* This function and dc_perform_mvbox_fetch().
* must be called from the same thread, typically in a loop.
*
* You should call this function directly after calling dc_perform_mvbox_fetch().
*
* See dc_perform_mvbox_fetch() for an example.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_mvbox_idle (dc_context_t* context);
/**
* Interrupt waiting for MVBOX-fetch.
* dc_interrupt_mvbox_idle() does _not_ interrupt dc_perform_mvbox_fetch().
* If the MVBOX-thread is inside this function when dc_interrupt_mvbox_idle() is called, however,
* the next call of the MVBOX-thread to dc_perform_mvbox_idle() is interrupted immediately.
*
* Internally, this function is called whenever a imap-jobs should be processed.
*
* When you need to call this function just because to get jobs done after network changes,
* use dc_maybe_network() instead.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_interrupt_mvbox_idle (dc_context_t* context);
/**
* Execute pending sentbox-jobs.
* This function and dc_perform_sentbox_fetch() and dc_perform_sentbox_idle()
* must be called from the same thread, typically in a loop.
*
* Example:
*
* void* sentbox_thread_func(void* context)
* {
* while (true) {
* dc_perform_sentbox_jobs(context);
* dc_perform_sentbox_fetch(context);
* dc_perform_sentbox_idle(context);
* }
* }
*
* // start sentbox-thread that runs forever
* pthread_t sentbox_thread;
* pthread_create(&sentbox_thread, NULL, sentbox_thread_func, context);
*
* ... program runs ...
*
* // network becomes available again -
* // the interrupt causes dc_perform_sentbox_idle() in the thread above
* // to return so that jobs are executed and messages are fetched.
* dc_maybe_network(context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_sentbox_jobs (dc_context_t* context);
/**
* Fetch new messages from the Sent folder, if any.
* This function and dc_perform_sentbox_idle()
* must be called from the same thread, typically in a loop.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_sentbox_fetch (dc_context_t* context);
/**
* Wait for messages or jobs in the SENTBOX-thread.
* This function and dc_perform_sentbox_fetch()
* must be called from the same thread, typically in a loop.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_sentbox_idle (dc_context_t* context);
/**
* Interrupt waiting for messages or jobs in the SENTBOX-thread.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_interrupt_sentbox_idle (dc_context_t* context);
/**
* Execute pending smtp-jobs.
* This function and dc_perform_smtp_idle() must be called from the same thread,
* typically in a loop.
*
* Example:
*
* void* smtp_thread_func(void* context)
* {
* while (true) {
* dc_perform_smtp_jobs(context);
* dc_perform_smtp_idle(context);
* }
* }
*
* // start smtp-thread that runs forever
* pthread_t smtp_thread;
* pthread_create(&smtp_thread, NULL, smtp_thread_func, context);
*
* ... program runs ...
*
* // network becomes available again -
* // the interrupt causes dc_perform_smtp_idle() in the thread above
* // to return so that jobs are executed
* dc_maybe_network(context);
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_smtp_jobs (dc_context_t* context);
/**
* Wait for smtp-jobs.
* This function and dc_perform_smtp_jobs() must be called from the same thread,
* typically in a loop.
*
* See dc_interrupt_smtp_idle() for an example.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_perform_smtp_idle (dc_context_t* context);
/**
* Interrupt waiting for smtp-jobs.
* If dc_perform_smtp_jobs() and dc_perform_smtp_idle() are called in a loop,
* calling this function causes jobs to be executed.
*
* dc_interrupt_smtp_idle() does _not_ interrupt dc_perform_smtp_jobs().
* If the smtp-thread is inside this function when dc_interrupt_smtp_idle() is called, however,
* the next call of the smtp-thread to dc_perform_smtp_idle() is interrupted immediately.
*
* Internally, this function is called whenever a message is to be sent.
*
* When you need to call this function just because to get jobs done after network changes,
* use dc_maybe_network() instead.
*
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @return None.
*/
void dc_interrupt_smtp_idle (dc_context_t* context);
void dc_stop_io(dc_context_t* context);
/**
* This function can be called whenever there is a hint
@@ -875,6 +575,7 @@ void dc_interrupt_smtp_idle (dc_context_t* context);
void dc_maybe_network (dc_context_t* context);
/**
* Save a keypair as the default keys for the user.
*
@@ -901,6 +602,7 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
#define DC_GCL_ARCHIVED_ONLY 0x01
#define DC_GCL_NO_SPECIALS 0x02
#define DC_GCL_ADD_ALLDONE_HINT 0x04
#define DC_GCL_FOR_FORWARDING 0x08
/**
@@ -939,6 +641,10 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
* if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
* the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are _any_ archived
* chats
* - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
* and hides the "Device chat" and the deaddrop.
* typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
* to also hide the archive link.
* - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
* to the list (may be used eg. for selecting chats on forwarding, the flag is
* not needed when DC_GCL_ARCHIVED_ONLY is already set)
@@ -1096,6 +802,23 @@ uint32_t dc_prepare_msg (dc_context_t* context, uint32_t ch
*/
uint32_t dc_send_msg (dc_context_t* context, uint32_t chat_id, dc_msg_t* msg);
/**
* Send a message defined by a dc_msg_t object to a chat, synchronously.
* This bypasses the IO scheduler and creates its own SMTP connection. Which means
* this is useful when the scheduler is not running.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id Chat ID to send the message to.
* If dc_prepare_msg() was called before, this parameter can be 0.
* @param msg Message object to send to the chat defined by the chat ID.
* On succcess, msg_id of the object is set up,
* The function does not take ownership of the object,
* so you have to free it using dc_msg_unref() as usual.
* @return The ID of the message that is about to be sent. 0 in case of errors.
*/
uint32_t dc_send_msg_sync (dc_context_t* context, uint32_t chat_id, dc_msg_t* msg);
/**
* Send a simple text message a given chat.
@@ -1292,6 +1015,22 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch
int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t chat_id);
/**
* Estimate the number of messages that will be deleted
* by the dc_set_config()-options `delete_device_after` or `delete_server_after`.
* This is typically used to show the estimated impact to the user before actually enabling ephemeral messages.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param from_server 1=Estimate deletion count for server, 0=Estimate deletion count for device
* @param seconds Count messages older than the given number of seconds.
* @return Number of messages that are older than the given number of seconds.
* This includes emails downloaded due to the `show_emails` option.
* Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
*/
int dc_estimate_deletion_cnt (dc_context_t* context, int from_server, int64_t seconds);
/**
* Returns the message IDs of all _fresh_ messages of any chat.
* Typically used for implementing notification summaries.
@@ -1586,8 +1325,10 @@ int dc_set_chat_name (dc_context_t* context, uint32_t ch
* @memberof dc_context_t
* @param context The context as created by dc_context_new().
* @param chat_id The chat ID to set the image for.
* @param image Full path of the image to use as the group image. If you pass NULL here,
* the group image is deleted (for promoted groups, all members are informed about this change anyway).
* @param image Full path of the image to use as the group image. The image will immediately be copied to the
* `blobdir`; the original image will not be needed anymore.
* If you pass NULL here, the group image is deleted (for promoted groups, all members are informed about
* this change anyway).
* @return 1=success, 0=error
*/
int dc_set_chat_profile_image (dc_context_t* context, uint32_t chat_id, const char* image);
@@ -1597,7 +1338,7 @@ int dc_set_chat_profile_image (dc_context_t* context, uint32_t ch
/**
* Set mute duration of a chat.
*
* This value can be checked by the ui upon receiving a new message to decide whether it should trigger an notification.
* The ui can then call dc_chat_is_muted() when receiving a new message to decide whether it should trigger an notification.
*
* Sends out #DC_EVENT_CHAT_MODIFIED.
*
@@ -1655,8 +1396,9 @@ char* dc_get_mime_headers (dc_context_t* context, uint32_t ms
*/
void dc_delete_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
/**
/*
* Empty IMAP server folder: delete all messages.
* Deprecated, use dc_set_config() with the key "delete_server_after" instead.
*
* @memberof dc_context_t
* @param context The context object as created by dc_context_new()
@@ -1759,7 +1501,7 @@ int dc_may_be_valid_addr (const char* addr);
/**
* Check if an e-mail address belongs to a known and unblocked contact.
* Known and unblocked contacts will be returned by dc_get_contacts().
* To get a list of all known and unblocked contacts, use dc_get_contacts().
*
* To validate an e-mail address independently of the contact database
* use dc_may_be_valid_addr().
@@ -1767,7 +1509,8 @@ int dc_may_be_valid_addr (const char* addr);
* @memberof dc_context_t
* @param context The context object as created by dc_context_new().
* @param addr The e-mail-address to check.
* @return 1=address is a contact in use, 0=address is not a contact in use.
* @return Contact ID of the contact belonging to the e-mail-address
* or 0 if there is no contact that is or was introduced by an accepted contact.
*/
uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char* addr);
@@ -2706,7 +2449,6 @@ dc_context_t* dc_chatlist_get_context (dc_chatlist_t* chatlist);
* last-message-date:
* avatar-path: path-to-blobfile
* is_verified: yes/no
* @return a utf8-encoded json string containing all requested info. Must be freed using dc_str_unref(). NULL is never returned.
*/
char* dc_chat_get_info_json (dc_context_t* context, size_t chat_id);
@@ -2800,19 +2542,6 @@ int dc_chat_get_type (const dc_chat_t* chat);
char* dc_chat_get_name (const dc_chat_t* chat);
/*
* Get a subtitle for a chat. The subtitle is eg. the email-address or the
* number of group members.
*
* Deprecated function. Subtitles should be created in the ui
* where plural forms and other specials can be handled more gracefully.
*
* @param chat The chat object to calulate the subtitle for.
* @return Subtitle as a string. Must be released using dc_str_unref() after usage. Never NULL.
*/
char* dc_chat_get_subtitle (const dc_chat_t* chat);
/**
* Get the chat's profile image.
* For groups, this is the image set by any group member
@@ -2946,7 +2675,7 @@ int dc_chat_is_sending_locations (const dc_chat_t* chat);
/**
* Check whether the chat is currently muted
* Check whether the chat is currently muted (can be changed by dc_set_chat_mute_duration()).
*
* @memberof dc_chat_t
* @param chat The chat object.
@@ -4124,28 +3853,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
/**
* @defgroup DC_EMPTY DC_EMPTY
*
* These constants configure emptying imap folders with dc_empty_server()
*
* @addtogroup DC_EMPTY
* @{
*/
/**
* Clear all mvbox messages.
*/
#define DC_EMPTY_MVBOX 0x01
/**
* Clear all INBOX messages.
*/
#define DC_EMPTY_INBOX 0x02
/**
* @}
*/
#define DC_EMPTY_MVBOX 0x01 // Deprecated, flag for dc_empty_server(): Clear all mvbox messages
#define DC_EMPTY_INBOX 0x02 // Deprecated, flag for dc_empty_server(): Clear all INBOX messages
/**
@@ -4153,8 +3862,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* 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.
*
* @addtogroup DC_EVENT
* @{
@@ -4169,7 +3876,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_INFO 100
@@ -4180,7 +3886,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_SMTP_CONNECTED 101
@@ -4191,7 +3896,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_IMAP_CONNECTED 102
@@ -4201,7 +3905,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_SMTP_MESSAGE_SENT 103
@@ -4211,7 +3914,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_IMAP_MESSAGE_DELETED 104
@@ -4221,7 +3923,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) Info string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_IMAP_MESSAGE_MOVED 105
@@ -4231,7 +3932,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) folder name.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_IMAP_FOLDER_EMPTIED 106
@@ -4241,7 +3941,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) path name
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_NEW_BLOB_FILE 150
@@ -4251,7 +3950,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) path name
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_DELETED_BLOB_FILE 151
@@ -4264,7 +3962,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 0
* @param data2 (const char*) Warning string in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_WARNING 300
@@ -4287,7 +3984,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* Some error strings are taken from dc_set_stock_translation(),
* however, most error strings will be in english language.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_ERROR 400
@@ -4311,7 +4007,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* 0=subsequent network error, should be logged only
* @param data2 (const char*) Error string, always set, never NULL.
* Must not be unref'd or modified and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_ERROR_NETWORK 401
@@ -4327,7 +4022,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data2 (const char*) Info string in english language.
* Must not be unref'd or modified
* and is valid only until the callback returns.
* @return 0
*/
#define DC_EVENT_ERROR_SELF_NOT_IN_GROUP 410
@@ -4341,7 +4035,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) chat_id for single added messages
* @param data2 (int) msg_id for single added messages
* @return 0
*/
#define DC_EVENT_MSGS_CHANGED 2000
@@ -4354,7 +4047,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
* @return 0
*/
#define DC_EVENT_INCOMING_MSG 2005
@@ -4365,7 +4057,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
* @return 0
*/
#define DC_EVENT_MSG_DELIVERED 2010
@@ -4376,7 +4067,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
* @return 0
*/
#define DC_EVENT_MSG_FAILED 2012
@@ -4387,7 +4077,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
* @return 0
*/
#define DC_EVENT_MSG_READ 2015
@@ -4400,7 +4089,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) chat_id
* @param data2 0
* @return 0
*/
#define DC_EVENT_CHAT_MODIFIED 2020
@@ -4410,7 +4098,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) If not 0, this is the contact_id of an added contact that should be selected.
* @param data2 0
* @return 0
*/
#define DC_EVENT_CONTACTS_CHANGED 2030
@@ -4423,7 +4110,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* If the locations of several contacts have been changed,
* eg. after calling dc_delete_all_locations(), this parameter is set to 0.
* @param data2 0
* @return 0
*/
#define DC_EVENT_LOCATION_CHANGED 2035
@@ -4433,7 +4119,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) 0=error, 1-999=progress in permille, 1000=success and done
* @param data2 0
* @return 0
*/
#define DC_EVENT_CONFIGURE_PROGRESS 2041
@@ -4443,7 +4128,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*
* @param data1 (int) 0=error, 1-999=progress in permille, 1000=success and done
* @param data2 0
* @return 0
*/
#define DC_EVENT_IMEX_PROGRESS 2051
@@ -4458,7 +4142,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data1 (const char*) Path and file name.
* Must not be unref'd or modified and is valid only until the callback returns.
* @param data2 0
* @return 0
*/
#define DC_EVENT_IMEX_FILE_WRITTEN 2052
@@ -4476,7 +4159,6 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
* 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
* 1000=Protocol finished for this contact.
* @return 0
*/
#define DC_EVENT_SECUREJOIN_INVITER_PROGRESS 2060
@@ -4492,21 +4174,9 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* @param data2 (int) Progress as:
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
* (Bob has verified alice and waits until Alice does the same for him)
* @return 0
*/
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
/**
* This event is sent out to the inviter when a joiner successfully joined a group.
*
* @param data1 (int) chat_id
* @param data2 (int) contact_id
* @return 0
*/
#define DC_EVENT_SECUREJOIN_MEMBER_ADDED 2062
/**
* @}
*/
@@ -4518,12 +4188,10 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_ERROR_SEE_STRING 0 // not used anymore
#define DC_ERROR_SELF_NOT_IN_GROUP 1 // not used anymore
#define DC_STR_SELFNOTINGRP 21 // not used anymore
#define DC_EVENT_DATA1_IS_STRING(e) ((e)==DC_EVENT_IMEX_FILE_WRITTEN || (e)==DC_EVENT_FILE_COPIED)
#define DC_EVENT_DATA2_IS_STRING(e) ((e)>=100 && (e)<=499)
#define DC_EVENT_DATA1_IS_STRING(e) 0 // not used anymore
#define DC_EVENT_DATA2_IS_STRING(e) ((e)==DC_EVENT_IMEX_FILE_WRITTEN || ((e)>=100 && (e)<=499))
#define DC_EVENT_RETURNS_INT(e) ((e)==DC_EVENT_IS_OFFLINE) // not used anymore
#define DC_EVENT_RETURNS_STRING(e) ((e)==DC_EVENT_GET_STRING) // not used anymore
char* dc_get_version_str (void); // deprecated
void dc_array_add_id (dc_array_t*, uint32_t); // deprecated
#define dc_archive_chat(a,b,c) dc_set_chat_visibility((a), (b), (c)? 1 : 0) // not used anymore
#define dc_chat_get_archived(a) (dc_chat_get_visibility((a))==1? 1 : 0) // not used anymore
@@ -4535,6 +4203,14 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
#define DC_SHOW_EMAILS_ACCEPTED_CONTACTS 1
#define DC_SHOW_EMAILS_ALL 2
/*
* Values for dc_get|set_config("media_quality")
*/
#define DC_MEDIA_QUALITY_BALANCED 0
#define DC_MEDIA_QUALITY_WORSE 1
/*
* Values for dc_get|set_config("key_gen_type")
*/
@@ -4653,8 +4329,6 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
#define DC_STR_NOMESSAGES 1
#define DC_STR_SELF 2
#define DC_STR_DRAFT 3
#define DC_STR_MEMBER 4
#define DC_STR_CONTACT 6
#define DC_STR_VOICEMESSAGE 7
#define DC_STR_DEADDROP 8
#define DC_STR_IMAGE 9
@@ -4686,7 +4360,6 @@ void dc_array_add_id (dc_array_t*, uint32_t); // depreca
#define DC_STR_STARREDMSGS 41
#define DC_STR_AC_SETUP_MSG_SUBJECT 42
#define DC_STR_AC_SETUP_MSG_BODY 43
#define DC_STR_SELFTALK_SUBTITLE 50
#define DC_STR_CANNOT_LOGIN 60
#define DC_STR_SERVER_RESPONSE 61
#define DC_STR_MSGACTIONBYUSER 62

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
use failure::Fail;
use std::ffi::{CStr, CString};
use std::ptr;
@@ -9,7 +8,7 @@ use std::ptr;
/// # Examples
///
/// ```rust,norun
/// use deltachat::dc_tools::{dc_strdup, to_string_lossy};
/// use crate::string::{dc_strdup, to_string_lossy};
/// unsafe {
/// let str_a = b"foobar\x00" as *const u8 as *const libc::c_char;
/// let str_a_copy = dc_strdup(str_a);
@@ -17,7 +16,7 @@ use std::ptr;
/// assert_ne!(str_a, str_a_copy);
/// }
/// ```
pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
let ret: *mut libc::c_char;
if !s.is_null() {
ret = libc::strdup(s);
@@ -31,13 +30,13 @@ pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
}
/// Error type for the [OsStrExt] trait
#[derive(Debug, Fail, PartialEq)]
pub enum CStringError {
#[derive(Debug, PartialEq, thiserror::Error)]
pub(crate) enum CStringError {
/// The string contains an interior null byte
#[fail(display = "String contains an interior null byte")]
#[error("String contains an interior null byte")]
InteriorNullByte,
/// The string is not valid Unicode
#[fail(display = "String is not valid unicode")]
#[error("String is not valid unicode")]
NotUnicode,
}
@@ -66,7 +65,7 @@ pub enum CStringError {
/// let mut c_ptr: *mut libc::c_char = dc_strdup(path_c.as_ptr());
/// }
/// ```
pub trait OsStrExt {
pub(crate) trait OsStrExt {
/// Convert a [std::ffi::OsStr] to an [std::ffi::CString]
///
/// This is useful to convert e.g. a [std::path::Path] to
@@ -131,15 +130,16 @@ fn os_str_to_c_string_unicode(
}
/// Convenience methods/associated functions for working with [CString]
///
/// This is helps transitioning from unsafe code.
pub trait CStringExt {
/// Create a new [CString], yolo style
trait CStringExt {
/// Create a new [CString], best effort
///
/// This unwrap the result, panicking when there are embedded NULL
/// bytes.
fn yolo<T: Into<Vec<u8>>>(t: T) -> CString {
CString::new(t).expect("String contains null byte, can not be CString")
/// Like the [to_string_lossy] this doesn't give up in the face of
/// bad input (embedded null bytes in this case) instead it does
/// the best it can by stripping the embedded null bytes.
fn new_lossy<T: Into<Vec<u8>>>(t: T) -> CString {
let mut s = t.into();
s.retain(|&c| c != 0);
CString::new(s).unwrap_or_default()
}
}
@@ -151,7 +151,7 @@ impl CStringExt for CString {}
/// Rust strings to raw C strings. This can be clumsy to do correctly
/// and the compiler sometimes allows it in an unsafe way. These
/// methods make it more succinct and help you get it right.
pub trait StrExt {
pub(crate) trait Strdup {
/// Allocate a new raw C `*char` version of this string.
///
/// This allocates a new raw C string which must be freed using
@@ -168,35 +168,44 @@ pub trait StrExt {
unsafe fn strdup(&self) -> *mut libc::c_char;
}
impl<T: AsRef<str>> StrExt for T {
impl<T: AsRef<str>> Strdup for T {
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = CString::yolo(self.as_ref());
let tmp = CString::new_lossy(self.as_ref());
dc_strdup(tmp.as_ptr())
}
}
// We can not implement for AsRef<OsStr> because we already implement
// AsRev<str> and this conflicts. So implement for Path directly.
impl Strdup for std::path::Path {
unsafe fn strdup(&self) -> *mut libc::c_char {
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
dc_strdup(tmp.as_ptr())
}
}
/// Convenience methods to turn optional strings into C strings.
///
/// This is the same as the [StrExt] trait but a different trait name
/// to work around the type system not allowing to implement [StrExt]
/// for `Option<impl StrExt>` When we already have an [StrExt] impl
/// This is the same as the [Strdup] trait but a different trait name
/// to work around the type system not allowing to implement [Strdup]
/// for `Option<impl Strdup>` When we already have an [Strdup] impl
/// for `AsRef<&str>`.
///
/// When the [Option] is [Option::Some] this behaves just like
/// [StrExt::strdup], when it is [Option::None] a null pointer is
/// [Strdup::strdup], when it is [Option::None] a null pointer is
/// returned.
pub trait OptStrExt {
pub(crate) trait OptStrdup {
/// Allocate a new raw C `*char` version of this string, or NULL.
///
/// See [StrExt::strdup] for details.
/// See [Strdup::strdup] for details.
unsafe fn strdup(&self) -> *mut libc::c_char;
}
impl<T: AsRef<str>> OptStrExt for Option<T> {
impl<T: AsRef<str>> OptStrdup for Option<T> {
unsafe fn strdup(&self) -> *mut libc::c_char {
match self {
Some(s) => {
let tmp = CString::yolo(s.as_ref());
let tmp = CString::new_lossy(s.as_ref());
dc_strdup(tmp.as_ptr())
}
None => ptr::null_mut(),
@@ -204,7 +213,7 @@ impl<T: AsRef<str>> OptStrExt for Option<T> {
}
}
pub fn to_string_lossy(s: *const libc::c_char) -> String {
pub(crate) fn to_string_lossy(s: *const libc::c_char) -> String {
if s.is_null() {
return "".into();
}
@@ -214,7 +223,7 @@ pub fn to_string_lossy(s: *const libc::c_char) -> String {
cstr.to_string_lossy().to_string()
}
pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
if s.is_null() {
return None;
}
@@ -235,7 +244,7 @@ pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
///
/// [Path]: std::path::Path
#[cfg(not(target_os = "windows"))]
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
assert!(!s.is_null(), "cannot be used on null pointers");
use std::os::unix::ffi::OsStrExt;
unsafe {
@@ -247,7 +256,7 @@ pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
// as_path() implementation for windows, documented above.
#[cfg(target_os = "windows")]
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
as_path_unicode(s)
}
@@ -354,8 +363,14 @@ mod tests {
}
#[test]
fn test_cstring_yolo() {
assert_eq!(CString::new("hello").unwrap(), CString::yolo("hello"));
fn test_cstring_new_lossy() {
assert!(CString::new("hel\x00lo").is_err());
assert!(CString::new(String::from("hel\x00o")).is_err());
let r = CString::new("hello").unwrap();
assert_eq!(CString::new_lossy("hello"), r);
assert_eq!(CString::new_lossy("hel\x00lo"), r);
assert_eq!(CString::new_lossy(String::from("hello")), r);
assert_eq!(CString::new_lossy(String::from("hel\x00lo")), r);
}
#[test]

View File

@@ -3,7 +3,6 @@ extern crate proc_macro;
use crate::proc_macro::TokenStream;
use quote::quote;
use syn;
// For now, assume (not check) that these macroses are applied to enum without
// data. If this assumption is violated, compiler error will point to

126
draft/group-sync.rst Normal file
View File

@@ -0,0 +1,126 @@
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
- Alice adds Carol, 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.
Note that for verified groups any mitigation mechanism likely
needs to make all clients to know who originally added a member.
solution: memorize+attach (possible encrypted) chat-meta mime messages
----------------------------------------------------------------------
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.
- 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.
- 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.
Notes:
- mechanism works for both encrypted and unencrypted add/del messages
- 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.
- multiple member-added/removed messages can be attached in a single
correction message
- 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)
- 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.
- 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
b) could lead to a lot of members sending corrections
- instead of broadcasting correction messages we could only send it to
the sender of the inconsistent member-added/removed message.
A receiver of such a correction message would then need to forward
the message to the members it thinks also have an inconsistent view.
This sounds complicated and error-prone. Concretely, if Alice
receives Bob's "Member-added: Doris" message, then Alice
broadcasting the correction message with "Member-added: Carol"
would reach all four members, healing group consistency in one step.
If Bob meanwhile receives Alice's "Member-Added: Carol" message,
Bob would broadcast a correction message to all four members as well.
(Imagine a situation where Alice/Bob added Carol/Doris
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

@@ -1,6 +1,7 @@
use std::path::Path;
use std::str::FromStr;
use anyhow::{bail, ensure};
use async_std::path::Path;
use deltachat::chat::{self, Chat, ChatId, ChatVisibility};
use deltachat::chatlist::*;
use deltachat::constants::*;
@@ -10,7 +11,6 @@ use deltachat::dc_receive_imf::*;
use deltachat::dc_tools::*;
use deltachat::error::Error;
use deltachat::imex::*;
use deltachat::job::*;
use deltachat::location;
use deltachat::lot::LotState;
use deltachat::message::{self, Message, MessageState, MsgId};
@@ -23,78 +23,79 @@ use deltachat::{config, provider};
/// Reset database tables.
/// Argument is a bitmask, executing single or multiple actions in one call.
/// e.g. bitmask 7 triggers actions definded with bits 1, 2 and 4.
fn dc_reset_tables(context: &Context, bits: i32) -> i32 {
async fn reset_tables(context: &Context, bits: i32) {
println!("Resetting tables ({})...", bits);
if 0 != bits & 1 {
sql::execute(context, &context.sql, "DELETE FROM jobs;", params![]).unwrap();
context
.sql()
.execute("DELETE FROM jobs;", paramsv![])
.await
.unwrap();
println!("(1) Jobs reset.");
}
if 0 != bits & 2 {
sql::execute(
context,
&context.sql,
"DELETE FROM acpeerstates;",
params![],
)
.unwrap();
context
.sql()
.execute("DELETE FROM acpeerstates;", paramsv![])
.await
.unwrap();
println!("(2) Peerstates reset.");
}
if 0 != bits & 4 {
sql::execute(context, &context.sql, "DELETE FROM keypairs;", params![]).unwrap();
context
.sql()
.execute("DELETE FROM keypairs;", paramsv![])
.await
.unwrap();
println!("(4) Private keypairs reset.");
}
if 0 != bits & 8 {
sql::execute(
context,
&context.sql,
"DELETE FROM contacts WHERE id>9;",
params![],
)
.unwrap();
sql::execute(
context,
&context.sql,
"DELETE FROM chats WHERE id>9;",
params![],
)
.unwrap();
sql::execute(
context,
&context.sql,
"DELETE FROM chats_contacts;",
params![],
)
.unwrap();
sql::execute(
context,
&context.sql,
"DELETE FROM msgs WHERE id>9;",
params![],
)
.unwrap();
sql::execute(
context,
&context.sql,
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
params![],
)
.unwrap();
sql::execute(context, &context.sql, "DELETE FROM leftgrps;", params![]).unwrap();
context
.sql()
.execute("DELETE FROM contacts WHERE id>9;", paramsv![])
.await
.unwrap();
context
.sql()
.execute("DELETE FROM chats WHERE id>9;", paramsv![])
.await
.unwrap();
context
.sql()
.execute("DELETE FROM chats_contacts;", paramsv![])
.await
.unwrap();
context
.sql()
.execute("DELETE FROM msgs WHERE id>9;", paramsv![])
.await
.unwrap();
context
.sql()
.execute(
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
paramsv![],
)
.await
.unwrap();
context
.sql()
.execute("DELETE FROM leftgrps;", paramsv![])
.await
.unwrap();
println!("(8) Rest but server config reset.");
}
context.call_cb(Event::MsgsChanged {
context.emit_event(Event::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
1
}
fn dc_poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), Error> {
let data = dc_read_file(context, filename)?;
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), anyhow::Error> {
let data = dc_read_file(context, filename).await?;
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false) {
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false).await {
println!("dc_receive_imf errored: {:?}", err);
}
Ok(())
@@ -103,38 +104,29 @@ fn dc_poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(),
/// Import a file to the database.
/// For testing, import a folder with eml-files, a single eml-file, e-mail plus public key and so on.
/// For normal importing, use imex().
///
/// @private @memberof Context
/// @param context The context as created by dc_context_new().
/// @param spec The file or directory to import. NULL for the last command.
/// @return 1=success, 0=error.
fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int {
if !context.sql.is_open() {
error!(context, "Import: Database not opened.");
return 0;
}
let mut read_cnt = 0;
async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
let mut read_cnt: usize = 0;
let real_spec: String;
/* if `spec` is given, remember it for later usage; if it is not given, try to use the last one */
// if `spec` is given, remember it for later usage; if it is not given, try to use the last one
if let Some(spec) = spec {
real_spec = spec.to_string();
context
.sql
.sql()
.set_raw_config(context, "import_spec", Some(&real_spec))
.await
.unwrap();
} else {
let rs = context.sql.get_raw_config(context, "import_spec");
let rs = context.sql().get_raw_config(context, "import_spec").await;
if rs.is_none() {
error!(context, "Import: No file or folder given.");
return 0;
return false;
}
real_spec = rs.unwrap();
}
if let Some(suffix) = dc_get_filesuffix_lc(&real_spec) {
if suffix == "eml" && dc_poke_eml_file(context, &real_spec).is_ok() {
if suffix == "eml" && poke_eml_file(context, &real_spec).await.is_ok() {
read_cnt += 1
}
} else {
@@ -143,7 +135,7 @@ fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int {
let dir = std::fs::read_dir(dir_name);
if dir.is_err() {
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
return 0;
return false;
} else {
let dir = dir.unwrap();
for entry in dir {
@@ -156,7 +148,7 @@ fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int {
if name.ends_with(".eml") {
let path_plus_name = format!("{}/{}", &real_spec, name);
println!("Import: {}", path_plus_name);
if dc_poke_eml_file(context, path_plus_name).is_ok() {
if poke_eml_file(context, path_plus_name).await.is_ok() {
read_cnt += 1
}
}
@@ -165,16 +157,19 @@ fn poke_spec(context: &Context, spec: Option<&str>) -> libc::c_int {
}
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
if read_cnt > 0 {
context.call_cb(Event::MsgsChanged {
context.emit_event(Event::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
}
1
true
}
fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
let contact = Contact::get_by_id(context, msg.get_from_id()).expect("invalid contact");
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_id = contact.get_id();
@@ -217,7 +212,7 @@ fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
);
}
fn log_msglist(context: &Context, msglist: &Vec<MsgId>) -> Result<(), Error> {
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error> {
let mut lines_out = 0;
for &msg_id in msglist {
if msg_id.is_daymarker() {
@@ -233,8 +228,8 @@ fn log_msglist(context: &Context, msglist: &Vec<MsgId>) -> Result<(), Error> {
);
lines_out += 1
}
let msg = Message::load_from_db(context, msg_id)?;
log_msg(context, "", &msg);
let msg = Message::load_from_db(context, msg_id).await?;
log_msg(context, "", &msg).await;
}
}
if lines_out > 0 {
@@ -245,18 +240,18 @@ fn log_msglist(context: &Context, msglist: &Vec<MsgId>) -> Result<(), Error> {
Ok(())
}
fn log_contactlist(context: &Context, contacts: &Vec<u32>) {
let mut contacts = contacts.clone();
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) {
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
let name = contact.get_name();
let addr = contact.get_addr();
let verified_state = contact.is_verified(context);
let verified_state = contact.is_verified(context).await;
let verified_str = if VerifiedStatus::Unverified != verified_state {
if verified_state == VerifiedStatus::BidirectVerified {
" √√"
@@ -280,7 +275,7 @@ fn log_contactlist(context: &Context, contacts: &Vec<u32>) {
"addr unset"
}
);
let peerstate = Peerstate::from_addr(context, &context.sql, &addr);
let peerstate = Peerstate::from_addr(context, &addr).await;
if peerstate.is_some() && contact_id != 1 as libc::c_uint {
line2 = format!(
", prefer-encrypt={}",
@@ -297,10 +292,9 @@ fn chat_prefix(chat: &Chat) -> &'static str {
chat.typ.into()
}
pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let chat_id = *context.cmdline_sel_chat_id.read().unwrap();
pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Result<(), Error> {
let mut sel_chat = if !chat_id.is_unset() {
Chat::load_from_db(context, chat_id).ok()
Chat::load_from_db(&context, *chat_id).await.ok()
} else {
None
};
@@ -341,7 +335,6 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
configure\n\
connect\n\
disconnect\n\
interrupt\n\
maybenetwork\n\
housekeeping\n\
help imex (Import/Export)\n\
@@ -397,13 +390,14 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
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\
============================================="
),
},
"initiate-key-transfer" => match initiate_key_transfer(context) {
"initiate-key-transfer" => match initiate_key_transfer(&context).await {
Ok(setup_code) => println!(
"Setup code for the transferred setup message: {}",
setup_code,
@@ -413,9 +407,9 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
"get-setupcodebegin" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let msg_id: MsgId = MsgId::new(arg1.parse()?);
let msg = Message::load_from_db(context, msg_id)?;
let msg = Message::load_from_db(&context, msg_id).await?;
if msg.is_setupmessage() {
let setupcodebegin = msg.get_setupcodebegin(context);
let setupcodebegin = msg.get_setupcodebegin(&context).await;
println!(
"The setup code for setup message {} starts with: {}",
msg_id,
@@ -430,29 +424,29 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
!arg1.is_empty() && !arg2.is_empty(),
"Arguments <msg-id> <setup-code> expected"
);
continue_key_transfer(context, MsgId::new(arg1.parse()?), &arg2)?;
continue_key_transfer(&context, MsgId::new(arg1.parse()?), &arg2).await?;
}
"has-backup" => {
has_backup(context, blobdir)?;
has_backup(&context, blobdir).await?;
}
"export-backup" => {
imex(context, ImexMode::ExportBackup, Some(blobdir));
imex(&context, ImexMode::ExportBackup, Some(blobdir)).await?;
}
"import-backup" => {
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
imex(context, ImexMode::ImportBackup, Some(arg1));
imex(&context, ImexMode::ImportBackup, Some(arg1)).await?;
}
"export-keys" => {
imex(context, ImexMode::ExportSelfKeys, Some(blobdir));
imex(&context, ImexMode::ExportSelfKeys, Some(blobdir)).await?;
}
"import-keys" => {
imex(context, ImexMode::ImportSelfKeys, Some(blobdir));
imex(&context, ImexMode::ImportSelfKeys, Some(blobdir)).await?;
}
"export-setup" => {
let setup_code = create_setup_code(context);
let setup_code = create_setup_code(&context);
let file_name = blobdir.join("autocrypt-setup-message.html");
let file_content = render_setup_file(context, &setup_code)?;
std::fs::write(&file_name, file_content)?;
let file_content = render_setup_file(&context, &setup_code).await?;
async_std::fs::write(&file_name, file_content).await?;
println!(
"Setup message written to: {}\nSetup code: {}",
file_name.display(),
@@ -460,49 +454,47 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
);
}
"poke" => {
ensure!(0 != poke_spec(context, Some(arg1)), "Poke failed");
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
}
"reset" => {
ensure!(!arg1.is_empty(), "Argument <bits> missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config");
let bits: i32 = arg1.parse()?;
ensure!(bits < 16, "<bits> must be lower than 16.");
ensure!(0 != dc_reset_tables(context, bits), "Reset failed");
reset_tables(&context, bits).await;
}
"stop" => {
context.stop_ongoing();
context.stop_ongoing().await;
}
"set" => {
ensure!(!arg1.is_empty(), "Argument <key> missing.");
let key = config::Config::from_str(&arg1)?;
let value = if arg2.is_empty() { None } else { Some(arg2) };
context.set_config(key, value)?;
context.set_config(key, value).await?;
}
"get" => {
ensure!(!arg1.is_empty(), "Argument <key> missing.");
let key = config::Config::from_str(&arg1)?;
let val = context.get_config(key);
let val = context.get_config(key).await;
println!("{}={:?}", key, val);
}
"info" => {
println!("{:#?}", context.get_info());
}
"interrupt" => {
interrupt_inbox_idle(context);
println!("{:#?}", context.get_info().await);
}
"maybenetwork" => {
maybe_network(context);
context.maybe_network().await;
}
"housekeeping" => {
sql::housekeeping(context);
sql::housekeeping(&context).await;
}
"listchats" | "listarchived" | "chats" => {
let listflags = if arg0 == "listarchived" { 0x01 } else { 0 };
let chatlist = Chatlist::try_load(
context,
&context,
listflags,
if arg1.is_empty() { None } else { Some(arg1) },
None,
)?;
)
.await?;
let cnt = chatlist.len();
if cnt > 0 {
@@ -511,20 +503,20 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
);
for i in (0..cnt).rev() {
let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?;
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
println!(
"{}#{}: {} [{} fresh] {}",
chat_prefix(&chat),
chat.get_id(),
chat.get_name(),
chat.get_id().get_fresh_msg_cnt(context),
chat.get_id().get_fresh_msg_cnt(&context).await,
match chat.visibility {
ChatVisibility::Normal => "",
ChatVisibility::Archived => "📦",
ChatVisibility::Pinned => "📌",
},
);
let lot = chatlist.get_summary(context, i, Some(&chat));
let lot = chatlist.get_summary(&context, i, Some(&chat)).await;
let statestr = if chat.visibility == ChatVisibility::Archived {
" [Archived]"
} else {
@@ -557,7 +549,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
);
}
}
if location::is_sending_locations_to_chat(context, ChatId::new(0)) {
if location::is_sending_locations_to_chat(&context, ChatId::new(0)).await {
println!("Location streaming enabled.");
}
println!("{} chats", cnt);
@@ -567,21 +559,21 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
bail!("Argument [chat-id] is missing.");
}
if !arg1.is_empty() {
let chat_id = ChatId::new(arg1.parse()?);
println!("Selecting chat {}", chat_id);
sel_chat = Some(Chat::load_from_db(context, chat_id)?);
*context.cmdline_sel_chat_id.write().unwrap() = chat_id;
let id = ChatId::new(arg1.parse()?);
println!("Selecting chat {}", id);
sel_chat = Some(Chat::load_from_db(&context, id).await?);
*chat_id = id;
}
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);
let members = chat::get_chat_contacts(context, sel_chat.id);
let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await;
let members = chat::get_chat_contacts(&context, sel_chat.id).await;
let subtitle = if sel_chat.is_device_talk() {
"device-talk".to_string()
} else if sel_chat.get_type() == Chattype::Single && !members.is_empty() {
let contact = Contact::get_by_id(context, members[0])?;
let contact = Contact::get_by_id(&context, members[0]).await?;
contact.get_addr().to_string()
} else {
format!("{} member(s)", members.len())
@@ -597,7 +589,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
} else {
""
},
match sel_chat.get_profile_image(context) {
match sel_chat.get_profile_image(&context).await {
Some(icon) => match icon.to_str() {
Some(icon) => format!(" Icon: {}", icon),
_ => " Icon: Err".to_string(),
@@ -605,38 +597,42 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
_ => "".to_string(),
},
);
log_msglist(context, &msglist)?;
if let Some(draft) = sel_chat.get_id().get_draft(context)? {
log_msg(context, "Draft", &draft);
log_msglist(&context, &msglist).await?;
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
log_msg(&context, "Draft", &draft).await;
}
println!("{} messages.", sel_chat.get_id().get_msg_cnt(context));
chat::marknoticed_chat(context, sel_chat.get_id())?;
println!(
"{} messages.",
sel_chat.get_id().get_msg_cnt(&context).await
);
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
}
"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)?;
let chat_id = chat::create_by_contact_id(&context, contact_id as u32).await?;
println!("Single#{} created successfully.", chat_id,);
}
"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)?;
let chat = Chat::load_from_db(context, chat_id)?;
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,);
}
"creategroup" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id = chat::create_group_chat(context, VerifiedStatus::Unverified, arg1)?;
let chat_id =
chat::create_group_chat(&context, VerifiedStatus::Unverified, arg1).await?;
println!("Group#{} created successfully.", chat_id);
}
"createverified" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id = chat::create_group_chat(context, VerifiedStatus::Verified, arg1)?;
let chat_id = chat::create_group_chat(&context, VerifiedStatus::Verified, arg1).await?;
println!("VerifiedGroup#{} created successfully.", chat_id);
}
@@ -646,10 +642,12 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let contact_id_0: libc::c_int = arg1.parse()?;
if chat::add_contact_to_chat(
context,
&context,
sel_chat.as_ref().unwrap().get_id(),
contact_id_0 as u32,
) {
)
.await
{
println!("Contact added to chat.");
} else {
bail!("Cannot add contact to chat.");
@@ -660,17 +658,18 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id_1: libc::c_int = arg1.parse()?;
chat::remove_contact_from_chat(
context,
&context,
sel_chat.as_ref().unwrap().get_id(),
contact_id_1 as u32,
)?;
)
.await?;
println!("Contact added to chat.");
}
"groupname" => {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "Argument <name> missing.");
chat::set_chat_name(context, sel_chat.as_ref().unwrap().get_id(), arg1)?;
chat::set_chat_name(&context, sel_chat.as_ref().unwrap().get_id(), arg1).await?;
println!("Chat name set");
}
@@ -678,24 +677,27 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "Argument <image> missing.");
chat::set_chat_profile_image(context, sel_chat.as_ref().unwrap().get_id(), arg1)?;
chat::set_chat_profile_image(&context, sel_chat.as_ref().unwrap().get_id(), arg1)
.await?;
println!("Chat image set");
}
"chatinfo" => {
ensure!(sel_chat.is_some(), "No chat selected.");
let contacts = chat::get_chat_contacts(context, sel_chat.as_ref().unwrap().get_id());
let contacts =
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await;
println!("Memberlist:");
log_contactlist(context, &contacts);
log_contactlist(&context, &contacts).await;
println!(
"{} contacts\nLocation streaming: {}",
contacts.len(),
location::is_sending_locations_to_chat(
context,
&context,
sel_chat.as_ref().unwrap().get_id()
),
)
.await,
);
}
"getlocations" => {
@@ -703,12 +705,13 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let contact_id = arg1.parse().unwrap_or_default();
let locations = location::get_range(
context,
&context,
sel_chat.as_ref().unwrap().get_id(),
contact_id,
0,
0,
);
)
.await;
let default_marker = "-".to_string();
for location in &locations {
let marker = location.marker.as_ref().unwrap_or(&default_marker);
@@ -734,7 +737,12 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
ensure!(!arg1.is_empty(), "No timeout given.");
let seconds = arg1.parse()?;
location::send_locations_to_chat(context, sel_chat.as_ref().unwrap().get_id(), seconds);
location::send_locations_to_chat(
&context,
sel_chat.as_ref().unwrap().get_id(),
seconds,
)
.await;
println!(
"Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.",
sel_chat.as_ref().unwrap().get_id(),
@@ -749,7 +757,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let latitude = arg1.parse()?;
let longitude = arg2.parse()?;
let continue_streaming = location::set(context, latitude, longitude, 0.);
let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
if continue_streaming {
println!("Success, streaming should be continued.");
} else {
@@ -757,7 +765,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
}
}
"dellocations" => {
location::delete_all(context)?;
location::delete_all(&context).await?;
}
"send" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -765,11 +773,11 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let msg = format!("{} {}", arg1, arg2);
chat::send_text_msg(context, sel_chat.as_ref().unwrap().get_id(), msg)?;
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?;
}
"sendempty" => {
ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_text_msg(context, sel_chat.as_ref().unwrap().get_id(), "".into())?;
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
}
"sendimage" | "sendfile" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -784,7 +792,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
if !arg2.is_empty() {
msg.set_text(Some(arg2.to_string()));
}
chat::send_msg(context, sel_chat.as_ref().unwrap().get_id(), &mut msg)?;
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"listmsgs" => {
ensure!(!arg1.is_empty(), "Argument <query> missing.");
@@ -795,9 +803,9 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
ChatId::new(0)
};
let msglist = context.search_msgs(chat, arg1);
let msglist = context.search_msgs(chat, arg1).await;
log_msglist(context, &msglist)?;
log_msglist(&context, &msglist).await?;
println!("{} messages.", msglist.len());
}
"draft" => {
@@ -810,10 +818,16 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
.as_ref()
.unwrap()
.get_id()
.set_draft(context, Some(&mut draft));
.set_draft(&context, Some(&mut draft))
.await;
println!("Draft saved.");
} else {
sel_chat.as_ref().unwrap().get_id().set_draft(context, None);
sel_chat
.as_ref()
.unwrap()
.get_id()
.set_draft(&context, None)
.await;
println!("Draft deleted.");
}
}
@@ -824,21 +838,22 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
);
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some(arg1.to_string()));
chat::add_device_msg(context, None, Some(&mut msg))?;
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
}
"updatedevicechats" => {
context.update_device_chats()?;
context.update_device_chats().await?;
}
"listmedia" => {
ensure!(sel_chat.is_some(), "No chat selected.");
let images = chat::get_chat_media(
context,
&context,
sel_chat.as_ref().unwrap().get_id(),
Viewtype::Image,
Viewtype::Gif,
Viewtype::Video,
);
)
.await;
println!("{} images or videos: ", images.len());
for (i, data) in images.iter().enumerate() {
if 0 == i {
@@ -847,36 +862,38 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
print!(", {}", data);
}
}
print!("\n");
println!();
}
"archive" | "unarchive" | "pin" | "unpin" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id.set_visibility(
context,
match arg0 {
"archive" => ChatVisibility::Archived,
"unarchive" | "unpin" => ChatVisibility::Normal,
"pin" => ChatVisibility::Pinned,
_ => panic!("Unexpected command (This should never happen)"),
},
)?;
chat_id
.set_visibility(
&context,
match arg0 {
"archive" => ChatVisibility::Archived,
"unarchive" | "unpin" => ChatVisibility::Normal,
"pin" => ChatVisibility::Pinned,
_ => panic!("Unexpected command (This should never happen)"),
},
)
.await?;
}
"delchat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id.delete(context)?;
chat_id.delete(&context).await?;
}
"msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?);
let res = message::get_msg_info(context, id);
let res = message::get_msg_info(&context, id).await;
println!("{}", res);
}
"listfresh" => {
let msglist = context.get_fresh_msgs();
let msglist = context.get_fresh_msgs().await;
log_msglist(context, &msglist)?;
log_msglist(&context, &msglist).await?;
print!("{} fresh messages.", msglist.len());
}
"forward" => {
@@ -888,37 +905,38 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
let mut msg_ids = [MsgId::new(0); 1];
let chat_id = ChatId::new(arg2.parse()?);
msg_ids[0] = MsgId::new(arg1.parse()?);
chat::forward_msgs(context, &msg_ids, chat_id)?;
chat::forward_msgs(&context, &msg_ids, chat_id).await?;
}
"markseen" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut msg_ids = [MsgId::new(0); 1];
let mut msg_ids = vec![MsgId::new(0)];
msg_ids[0] = MsgId::new(arg1.parse()?);
message::markseen_msgs(context, &msg_ids);
message::markseen_msgs(&context, msg_ids).await;
}
"star" | "unstar" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let mut msg_ids = [MsgId::new(0); 1];
let mut msg_ids = vec![MsgId::new(0); 1];
msg_ids[0] = MsgId::new(arg1.parse()?);
message::star_msgs(context, &msg_ids, arg0 == "star");
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];
ids[0] = MsgId::new(arg1.parse()?);
message::delete_msgs(context, &ids);
message::delete_msgs(&context, &ids).await;
}
"listcontacts" | "contacts" | "listverified" => {
let contacts = Contact::get_all(
context,
&context,
if arg0 == "listverified" {
0x1 | 0x2
} else {
0x2
},
Some(arg1),
)?;
log_contactlist(context, &contacts);
)
.await?;
log_contactlist(&context, &contacts).await;
println!("{} contacts.", contacts.len());
}
"addcontact" => {
@@ -926,30 +944,30 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
if !arg2.is_empty() {
let book = format!("{}\n{}", arg1, arg2);
Contact::add_address_book(context, book)?;
Contact::add_address_book(&context, book).await?;
} else {
Contact::create(context, "", arg1)?;
Contact::create(&context, "", arg1).await?;
}
}
"contactinfo" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id = arg1.parse()?;
let contact = Contact::get_by_id(context, contact_id)?;
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) {
match contact.get_profile_image(&context).await {
Some(image) => image.to_str().unwrap().to_string(),
None => "NoIcon".to_string(),
}
);
res += &Contact::get_encrinfo(context, contact_id)?;
res += &Contact::get_encrinfo(&context, contact_id).await?;
let chatlist = Chatlist::try_load(context, 0, None, Some(contact_id))?;
let chatlist = Chatlist::try_load(&context, 0, None, Some(contact_id)).await?;
let chatlist_cnt = chatlist.len();
if chatlist_cnt > 0 {
res += &format!(
@@ -960,7 +978,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
if 0 != i {
res += ", ";
}
let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?;
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
res += &format!("{}#{}", chat_prefix(&chat), chat.get_id());
}
}
@@ -969,11 +987,11 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
}
"delcontact" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
Contact::delete(context, arg1.parse()?)?;
Contact::delete(&context, arg1.parse()?).await?;
}
"checkqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
let res = check_qr(context, arg1);
let res = check_qr(&context, arg1).await;
println!(
"state={}, id={}, text1={:?}, text2={:?}",
res.get_state(),
@@ -984,7 +1002,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
}
"setqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
match set_config_from_qr(context, arg1) {
match set_config_from_qr(&context, arg1).await {
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
Err(err) => println!("Cannot set config from QR code: {:?}", err),
}
@@ -1012,7 +1030,7 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
// ensure!(!arg1.is_empty(), "Argument <id> missing.");
// let event = arg1.parse()?;
// let event = Event::from_u32(event).ok_or(format_err!("Event::from_u32({})", event))?;
// let r = context.call_cb(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
// 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,
@@ -1021,17 +1039,27 @@ pub fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::Error> {
"fileinfo" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");
if let Ok(buf) = dc_read_file(context, &arg1) {
if let Ok(buf) = dc_read_file(&context, &arg1).await {
let (width, height) = dc_get_filemeta(&buf)?;
println!("width={}, height={}", width, height);
} else {
bail!("Command failed.");
}
}
"estimatedeletion" => {
ensure!(!arg1.is_empty(), "Argument <seconds> missing");
let seconds = arg1.parse()?;
let device_cnt = message::estimate_deletion_cnt(&context, false, seconds).await?;
let server_cnt = message::estimate_deletion_cnt(&context, true, seconds).await?;
println!(
"estimated count of messages older than {} seconds:\non device: {}\non server: {}",
seconds, device_cnt, server_cnt
);
}
"emptyserver" => {
ensure!(!arg1.is_empty(), "Argument <flags> missing");
message::dc_empty_server(context, arg1.parse()?);
message::dc_empty_server(&context, arg1.parse()?).await;
}
"" => (),
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),

View File

@@ -6,27 +6,21 @@
#[macro_use]
extern crate deltachat;
#[macro_use]
extern crate failure;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate rusqlite;
use std::borrow::Cow::{self, Borrowed, Owned};
use std::io::{self, Write};
use std::path::Path;
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use ansi_term::Color;
use anyhow::{bail, Error};
use async_std::path::Path;
use deltachat::chat::ChatId;
use deltachat::config;
use deltachat::context::*;
use deltachat::job::*;
use deltachat::oauth2::*;
use deltachat::securejoin::*;
use deltachat::Event;
use log::{error, info, warn};
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::config::OutputStreamType;
use rustyline::error::ReadlineError;
@@ -39,179 +33,83 @@ use rustyline::{
mod cmdline;
use self::cmdline::*;
// Event Handler
fn receive_event(_context: &Context, event: Event) {
/// Event Handler
fn receive_event(event: Event) {
let yellow = Color::Yellow.normal();
match event {
Event::Info(msg) => {
/* do not show the event as this would fill the screen */
println!("{}", msg);
info!("{}", msg);
}
Event::SmtpConnected(msg) => {
println!("[DC_EVENT_SMTP_CONNECTED] {}", msg);
info!("[SMTP_CONNECTED] {}", msg);
}
Event::ImapConnected(msg) => {
println!("[DC_EVENT_IMAP_CONNECTED] {}", msg);
info!("[IMAP_CONNECTED] {}", msg);
}
Event::SmtpMessageSent(msg) => {
println!("[DC_EVENT_SMTP_MESSAGE_SENT] {}", msg);
info!("[SMTP_MESSAGE_SENT] {}", msg);
}
Event::Warning(msg) => {
println!("[Warning] {}", msg);
warn!("{}", msg);
}
Event::Error(msg) => {
println!("\x1b[31m[DC_EVENT_ERROR] {}\x1b[0m", msg);
error!("{}", msg);
}
Event::ErrorNetwork(msg) => {
println!("\x1b[31m[DC_EVENT_ERROR_NETWORK] msg={}\x1b[0m", msg);
error!("[NETWORK] msg={}", msg);
}
Event::ErrorSelfNotInGroup(msg) => {
println!("\x1b[31m[DC_EVENT_ERROR_SELF_NOT_IN_GROUP] {}\x1b[0m", msg);
error!("[SELF_NOT_IN_GROUP] {}", msg);
}
Event::MsgsChanged { chat_id, msg_id } => {
print!(
"\x1b[33m{{Received DC_EVENT_MSGS_CHANGED(chat_id={}, msg_id={})}}\n\x1b[0m",
chat_id, msg_id,
info!(
"{}",
yellow.paint(format!(
"Received MSGS_CHANGED(chat_id={}, msg_id={})",
chat_id, msg_id,
))
);
}
Event::ContactsChanged(_) => {
print!("\x1b[33m{{Received DC_EVENT_CONTACTS_CHANGED()}}\n\x1b[0m");
info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
}
Event::LocationChanged(contact) => {
print!(
"\x1b[33m{{Received DC_EVENT_LOCATION_CHANGED(contact={:?})}}\n\x1b[0m",
contact,
info!(
"{}",
yellow.paint(format!("Received LOCATION_CHANGED(contact={:?})", contact))
);
}
Event::ConfigureProgress(progress) => {
print!(
"\x1b[33m{{Received DC_EVENT_CONFIGURE_PROGRESS({} ‰)}}\n\x1b[0m",
progress,
info!(
"{}",
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
);
}
Event::ImexProgress(progress) => {
print!(
"\x1b[33m{{Received DC_EVENT_IMEX_PROGRESS({} ‰)}}\n\x1b[0m",
progress,
info!(
"{}",
yellow.paint(format!("Received IMEX_PROGRESS({} ‰)", progress))
);
}
Event::ImexFileWritten(file) => {
print!(
"\x1b[33m{{Received DC_EVENT_IMEX_FILE_WRITTEN({})}}\n\x1b[0m",
file.display()
info!(
"{}",
yellow.paint(format!("Received IMEX_FILE_WRITTEN({})", file.display()))
);
}
Event::ChatModified(chat) => {
print!(
"\x1b[33m{{Received DC_EVENT_CHAT_MODIFIED({})}}\n\x1b[0m",
chat
info!(
"{}",
yellow.paint(format!("Received CHAT_MODIFIED({})", chat))
);
}
_ => {
print!("\x1b[33m{{Received {:?}}}\n\x1b[0m", event);
info!("Received {:?}", event);
}
}
}
// Threads for waiting for messages and for jobs
lazy_static! {
static ref HANDLE: Arc<Mutex<Option<Handle>>> = Arc::new(Mutex::new(None));
static ref IS_RUNNING: AtomicBool = AtomicBool::new(true);
}
struct Handle {
handle_imap: Option<std::thread::JoinHandle<()>>,
handle_mvbox: Option<std::thread::JoinHandle<()>>,
handle_sentbox: Option<std::thread::JoinHandle<()>>,
handle_smtp: Option<std::thread::JoinHandle<()>>,
}
macro_rules! while_running {
($code:block) => {
if IS_RUNNING.load(Ordering::Relaxed) {
$code
} else {
break;
}
};
}
fn start_threads(c: Arc<RwLock<Context>>) {
if HANDLE.clone().lock().unwrap().is_some() {
return;
}
println!("Starting threads");
IS_RUNNING.store(true, Ordering::Relaxed);
let ctx = c.clone();
let handle_imap = std::thread::spawn(move || loop {
while_running!({
perform_inbox_jobs(&ctx.read().unwrap());
perform_inbox_fetch(&ctx.read().unwrap());
while_running!({
let context = ctx.read().unwrap();
perform_inbox_idle(&context);
});
});
});
let ctx = c.clone();
let handle_mvbox = std::thread::spawn(move || loop {
while_running!({
perform_mvbox_fetch(&ctx.read().unwrap());
while_running!({
perform_mvbox_idle(&ctx.read().unwrap());
});
});
});
let ctx = c.clone();
let handle_sentbox = std::thread::spawn(move || loop {
while_running!({
perform_sentbox_fetch(&ctx.read().unwrap());
while_running!({
perform_sentbox_idle(&ctx.read().unwrap());
});
});
});
let ctx = c;
let handle_smtp = std::thread::spawn(move || loop {
while_running!({
perform_smtp_jobs(&ctx.read().unwrap());
while_running!({
perform_smtp_idle(&ctx.read().unwrap());
});
});
});
*HANDLE.clone().lock().unwrap() = Some(Handle {
handle_imap: Some(handle_imap),
handle_mvbox: Some(handle_mvbox),
handle_sentbox: Some(handle_sentbox),
handle_smtp: Some(handle_smtp),
});
}
fn stop_threads(context: &Context) {
if let Some(ref mut handle) = *HANDLE.clone().lock().unwrap() {
println!("Stopping threads");
IS_RUNNING.store(false, Ordering::Relaxed);
interrupt_inbox_idle(context);
interrupt_mvbox_idle(context);
interrupt_sentbox_idle(context);
interrupt_smtp_idle(context);
handle.handle_imap.take().unwrap().join().unwrap();
handle.handle_mvbox.take().unwrap().join().unwrap();
handle.handle_sentbox.take().unwrap().join().unwrap();
handle.handle_smtp.take().unwrap().join().unwrap();
}
}
// === The main loop
struct DcHelper {
@@ -308,8 +206,17 @@ const CONTACT_COMMANDS: [&str; 6] = [
"delcontact",
"cleanupcontacts",
];
const MISC_COMMANDS: [&str; 9] = [
"getqr", "getbadqr", "checkqr", "event", "fileinfo", "clear", "exit", "quit", "help",
const MISC_COMMANDS: [&str; 10] = [
"getqr",
"getbadqr",
"checkqr",
"event",
"fileinfo",
"clear",
"exit",
"quit",
"help",
"estimatedeletion",
];
impl Hinter for DcHelper {
@@ -361,21 +268,22 @@ impl Highlighter for DcHelper {
impl Helper for DcHelper {}
fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
async fn start(args: Vec<String>) -> Result<(), Error> {
if args.len() < 2 {
println!("Error: Bad arguments, expected [db-name].");
return Err(format_err!("No db-name specified"));
bail!("No db-name specified");
}
let context = Context::new(
Box::new(receive_event),
"CLI".into(),
Path::new(&args[1]).to_path_buf(),
)?;
let context = Context::new("CLI".into(), Path::new(&args[1]).to_path_buf()).await?;
let events = context.get_event_emitter();
async_std::task::spawn(async move {
while let Some(event) = events.recv().await {
receive_event(event);
}
});
println!("Delta Chat Core is awaiting your commands.");
let ctx = Arc::new(RwLock::new(context));
let config = Config::builder()
.history_ignore_space(true)
.completion_type(CompletionType::List)
@@ -395,6 +303,8 @@ fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
println!("No previous history.");
}
let mut selected_chat = ChatId::default();
loop {
let p = "> ";
let readline = rl.readline(&p);
@@ -402,8 +312,7 @@ fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
Ok(line) => {
// TODO: ignore "set mail_pw"
rl.add_history_entry(line.as_str());
let ctx = ctx.clone();
match handle_cmd(line.trim(), ctx) {
match handle_cmd(line.trim(), context.clone(), &mut selected_chat).await {
Ok(ExitResult::Continue) => {}
Ok(ExitResult::Exit) => break,
Err(err) => println!("Error: {}", err),
@@ -411,6 +320,7 @@ fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
}
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
println!("Exiting...");
context.stop_io().await;
break;
}
Err(err) => {
@@ -419,11 +329,9 @@ fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
}
}
}
rl.save_history(".dc-history.txt")?;
println!("history saved");
{
stop_threads(&ctx.read().unwrap());
}
Ok(())
}
@@ -434,43 +342,29 @@ enum ExitResult {
Exit,
}
fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failure::Error> {
async fn handle_cmd(
line: &str,
ctx: Context,
selected_chat: &mut ChatId,
) -> Result<ExitResult, Error> {
let mut args = line.splitn(2, ' ');
let arg0 = args.next().unwrap_or_default();
let arg1 = args.next().unwrap_or_default();
match arg0 {
"connect" => {
start_threads(ctx);
ctx.start_io().await;
}
"disconnect" => {
stop_threads(&ctx.read().unwrap());
}
"smtp-jobs" => {
if HANDLE.clone().lock().unwrap().is_some() {
println!("smtp-jobs are already running in a thread.",);
} else {
perform_smtp_jobs(&ctx.read().unwrap());
}
}
"imap-jobs" => {
if HANDLE.clone().lock().unwrap().is_some() {
println!("inbox-jobs are already running in a thread.");
} else {
perform_inbox_jobs(&ctx.read().unwrap());
}
ctx.stop_io().await;
}
"configure" => {
start_threads(ctx.clone());
ctx.read().unwrap().configure();
ctx.configure().await?;
}
"oauth2" => {
if let Some(addr) = ctx.read().unwrap().get_config(config::Config::Addr) {
let oauth2_url = dc_get_oauth2_url(
&ctx.read().unwrap(),
&addr,
"chat.delta:/com.b44t.messenger",
);
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() {
println!("OAuth2 not available for {}.", &addr);
} else {
@@ -485,11 +379,10 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
print!("\x1b[1;1H\x1b[2J");
}
"getqr" | "getbadqr" => {
start_threads(ctx.clone());
if let Some(mut qr) = dc_get_securejoin_qr(
&ctx.read().unwrap(),
ChatId::new(arg1.parse().unwrap_or_default()),
) {
ctx.start_io().await;
if let Some(mut qr) =
dc_get_securejoin_qr(&ctx, ChatId::new(arg1.parse().unwrap_or_default())).await
{
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
qr.replace_range(12..22, "0000000000")
@@ -505,23 +398,23 @@ fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failu
}
}
"joinqr" => {
start_threads(ctx.clone());
ctx.start_io().await;
if !arg0.is_empty() {
dc_join_securejoin(&ctx.read().unwrap(), arg1);
dc_join_securejoin(&ctx, arg1).await;
}
}
"exit" | "quit" => return Ok(ExitResult::Exit),
_ => dc_cmdline(&ctx.read().unwrap(), line)?,
_ => cmdline(ctx.clone(), line, selected_chat).await?,
}
Ok(ExitResult::Continue)
}
pub fn main() -> Result<(), failure::Error> {
fn main() -> Result<(), Error> {
let _ = pretty_env_logger::try_init();
let args: Vec<String> = std::env::args().collect();
main_0(args)?;
let args = std::env::args().collect();
async_std::task::block_on(async move { start(args).await })?;
Ok(())
}

View File

@@ -1,7 +1,6 @@
extern crate deltachat;
use std::sync::{Arc, RwLock};
use std::{thread, time};
use std::time;
use tempfile::tempdir;
use deltachat::chat;
@@ -9,13 +8,9 @@ use deltachat::chatlist::*;
use deltachat::config;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::job::{
perform_inbox_fetch, perform_inbox_idle, perform_inbox_jobs, perform_smtp_idle,
perform_smtp_jobs,
};
use deltachat::Event;
fn cb(_ctx: &Context, event: Event) {
fn cb(event: Event) {
print!("[{:?}]", event);
match event {
@@ -31,81 +26,68 @@ fn cb(_ctx: &Context, event: Event) {
}
}
fn main() {
#[async_std::main]
async fn main() {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
println!("creating database {:?}", dbfile);
let ctx =
Context::new(Box::new(cb), "FakeOs".into(), dbfile).expect("Failed to create context");
let running = Arc::new(RwLock::new(true));
let info = ctx.get_info();
let ctx = Context::new("FakeOs".into(), dbfile.into())
.await
.expect("Failed to create context");
let info = ctx.get_info().await;
let duration = time::Duration::from_millis(4000);
println!("info: {:#?}", info);
let ctx = Arc::new(ctx);
let ctx1 = ctx.clone();
let r1 = running.clone();
let t1 = thread::spawn(move || {
while *r1.read().unwrap() {
perform_inbox_jobs(&ctx1);
if *r1.read().unwrap() {
perform_inbox_fetch(&ctx1);
if *r1.read().unwrap() {
perform_inbox_idle(&ctx1);
}
}
}
});
let ctx1 = ctx.clone();
let r1 = running.clone();
let t2 = thread::spawn(move || {
while *r1.read().unwrap() {
perform_smtp_jobs(&ctx1);
if *r1.read().unwrap() {
perform_smtp_idle(&ctx1);
}
let events = ctx.get_event_emitter();
let events_spawn = async_std::task::spawn(async move {
while let Some(event) = events.recv().await {
cb(event);
}
});
println!("configuring");
let args = std::env::args().collect::<Vec<String>>();
assert_eq!(args.len(), 2, "missing password");
let pw = args[1].clone();
ctx.set_config(config::Config::Addr, Some("d@testrun.org"))
assert_eq!(args.len(), 3, "requires email password");
let email = args[1].clone();
let pw = args[2].clone();
ctx.set_config(config::Config::Addr, Some(&email))
.await
.unwrap();
ctx.set_config(config::Config::MailPw, Some(&pw))
.await
.unwrap();
ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap();
ctx.configure();
thread::sleep(duration);
ctx.configure().await.unwrap();
println!("sending a message");
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com").unwrap();
let chat_id = chat::create_by_contact_id(&ctx, contact_id).unwrap();
chat::send_text_msg(&ctx, chat_id, "Hi, here is my first message!".into()).unwrap();
println!("------ RUN ------");
ctx.clone().start_io().await;
println!("--- SENDING A MESSAGE ---");
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
.await
.unwrap();
let chat_id = chat::create_by_contact_id(&ctx, contact_id).await.unwrap();
for i in 0..2 {
chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {}nth message!", i))
.await
.unwrap();
}
println!("fetching chats..");
let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap();
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
for i in 0..chats.len() {
let summary = chats.get_summary(&ctx, 0, None);
let summary = chats.get_summary(&ctx, 0, None).await;
let text1 = summary.get_text1();
let text2 = summary.get_text2();
println!("chat: {} - {:?} - {:?}", i, text1, text2,);
}
thread::sleep(duration);
println!("stopping threads");
*running.write().unwrap() = false;
deltachat::job::interrupt_inbox_idle(&ctx);
deltachat::job::interrupt_smtp_idle(&ctx);
println!("joining");
t1.join().unwrap();
t2.join().unwrap();
async_std::task::sleep(duration).await;
println!("stopping");
ctx.stop_io().await;
println!("closing");
events_spawn.await;
}

View File

@@ -1,3 +1,15 @@
0.900.0 (DRAFT)
---------------
- refactored internals to use plugin-approach
- introduced PerAccount and Global hooks that plugins can implement
- introduced `ac_member_added()` and `ac_member_removed()` plugin events.
- introduced two documented examples for an echo and a group-membership
tracking plugin.
0.800.0
-------

View File

@@ -113,10 +113,10 @@ You may look at `examples <https://py.delta.chat/examples.html>`_.
.. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust
Building manylinux1 based wheels
================================
Building manylinux based wheels
====================================
Building portable manylinux1 wheels which come with libdeltachat.so
Building portable manylinux wheels which come with libdeltachat.so
can be done with docker-tooling.
using docker pull / premade images

View File

@@ -2,10 +2,6 @@
high level API reference
========================
.. note::
This API is work in progress and may change in versions prior to 1.0.
- :class:`deltachat.account.Account` (your main entry point, creates the
other classes)
- :class:`deltachat.contact.Contact`

View File

@@ -1,7 +0,0 @@
C deltachat interface
=====================
See :doc:`lapi` for accessing many of the below functions
through the ``deltachat.capi.lib`` namespace.

View File

@@ -1,37 +1,60 @@
examples
========
Playing around on the commandline
----------------------------------
Once you have :doc:`installed deltachat bindings <install>`
you can start playing from the python interpreter commandline.
For example you can type ``python`` and then::
you need email/password credentials for an IMAP/SMTP account.
Delta Chat developers and the CI system use a special URL to create
temporary e-mail accounts on [testrun.org](https://testrun.org) for testing.
# instantiate and configure deltachat account
import deltachat
ac = deltachat.Account("/tmp/db")
Receiving a Chat message from the command line
----------------------------------------------
# start configuration activity and smtp/imap threads
ac.start_threads()
ac.configure(addr="test2@hq5.merlinux.eu", mail_pw="********")
Here is a simple bot that:
# create a contact and send a message
contact = ac.create_contact("someother@email.address")
chat = ac.create_chat_by_contact(contact)
chat.send_text("hi from the python interpreter command line")
- receives a message and sends back ("echoes") a message
Checkout our :doc:`api` for the various high-level things you can do
to send/receive messages, create contacts and chats.
- terminates the bot if the message `/quit` is sent
.. include:: ../examples/echo_and_quit.py
:literal:
Looking at a real example
With this file in your working directory you can run the bot
by specifying a database path, an e-mail address and password of
a SMTP-IMAP account::
$ cd examples
$ python echo_and_quit.py /tmp/db --email ADDRESS --password PASSWORD
While this process is running you can start sending chat messages
to `ADDRESS`.
Track member additions and removals in a group
----------------------------------------------
Here is a simple bot that:
- echoes messages sent to it
- tracks if configuration completed
- tracks member additions and removals for all chat groups
.. include:: ../examples/group_tracking.py
:literal:
With this file in your working directory you can run the bot
by specifying a database path, an e-mail address and password of
a SMTP-IMAP account::
python group_tracking.py --email ADDRESS --password PASSWORD /tmp/db
When this process is running you can start sending chat messages
to `ADDRESS`.
Writing bots for real
-------------------------
The `deltabot repository <https://github.com/deltachat/deltabot#deltachat-example-bot>`_
contains a real-life example of Python bindings usage.
contains a little framework for writing deltachat bots in Python.

View File

@@ -4,8 +4,9 @@ deltachat python bindings
The ``deltachat`` Python package provides two layers of bindings for the
core Rust-library of the https://delta.chat messaging ecosystem:
- :doc:`api` is a high level interface to deltachat-core which aims
to be memory safe and thoroughly tested through continous tox/pytest runs.
- :doc:`api` is a high level interface to deltachat-core.
- :doc:`plugins` is a brief introduction into implementing plugin hooks.
- :doc:`lapi` is a lowlevel CFFI-binding to the `Rust Core
<https://github.com/deltachat/deltachat-core-rust>`_.
@@ -28,6 +29,7 @@ getting started
changelog
api
lapi
plugins
..
Indices and tables

38
python/doc/plugins.rst Normal file
View File

@@ -0,0 +1,38 @@
Implementing Plugin Hooks
==========================
The Delta Chat Python bindings use `pluggy <https://pluggy.readthedocs.io>`_
for managing global and per-account plugin registration, and performing
hook calls. There are two kinds of plugins:
- Global plugins that are active for all accounts; they can implement
hooks at account-creation and account-shutdown time.
- Account plugins that are only active during the lifetime of a
single Account instance.
Registering a plugin
--------------------
.. autofunction:: deltachat.register_global_plugin
:noindex:
.. automethod:: deltachat.account.Account.add_account_plugin
:noindex:
Per-Account Hook specifications
-------------------------------
.. autoclass:: deltachat.hookspec.PerAccount
:members:
Global Hook specifications
--------------------------
.. autoclass:: deltachat.hookspec.Global
:members:

View File

@@ -0,0 +1,33 @@
# content of echo_and_quit.py
from deltachat import account_hookimpl, run_cmdline
class EchoPlugin:
@account_hookimpl
def ac_incoming_message(self, message):
print("process_incoming message", message)
if message.text.strip() == "/quit":
message.account.shutdown()
else:
# unconditionally accept the chat
message.accept_sender_contact()
addr = message.get_sender_contact().addr
if message.is_system_message():
message.chat.send_text("echoing system message from {}:\n{}".format(addr, message))
else:
text = message.text
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
@account_hookimpl
def ac_message_delivered(self, message):
print("ac_message_delivered", message)
def main(argv=None):
run_cmdline(argv=argv, account_plugins=[EchoPlugin()])
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,52 @@
# content of group_tracking.py
from deltachat import account_hookimpl, run_cmdline
class GroupTrackingPlugin:
@account_hookimpl
def ac_incoming_message(self, message):
print("process_incoming message", message)
if message.text.strip() == "/quit":
message.account.shutdown()
else:
# unconditionally accept the chat
message.accept_sender_contact()
addr = message.get_sender_contact().addr
text = message.text
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
@account_hookimpl
def ac_outgoing_message(self, message):
print("ac_outgoing_message:", message)
@account_hookimpl
def ac_configure_completed(self, success):
print("ac_configure_completed:", success)
@account_hookimpl
def ac_chat_modified(self, chat):
print("ac_chat_modified:", chat.id, chat.get_name())
for member in chat.get_contacts():
print("chat member: {}".format(member.addr))
@account_hookimpl
def ac_member_added(self, chat, contact, message):
print("ac_member_added {} to chat {} from {}".format(
contact.addr, chat.id, message.get_sender_contact().addr))
for member in chat.get_contacts():
print("chat member: {}".format(member.addr))
@account_hookimpl
def ac_member_removed(self, chat, contact, message):
print("ac_member_removed {} from chat {} by {}".format(
contact.addr, chat.id, message.get_sender_contact().addr))
def main(argv=None):
run_cmdline(argv=argv, account_plugins=[GroupTrackingPlugin()])
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,79 @@
import pytest
import py
import echo_and_quit
import group_tracking
from deltachat.events import FFIEventLogger
@pytest.fixture(scope='session')
def datadir():
"""The py.path.local object of the test-data/ directory."""
for path in reversed(py.path.local(__file__).parts()):
datadir = path.join('test-data')
if datadir.isdir():
return datadir
else:
pytest.skip('test-data directory not found')
def test_echo_quit_plugin(acfactory, lp):
lp.sec("creating one echo_and_quit bot")
botproc = acfactory.run_bot_process(echo_and_quit)
lp.sec("creating a temp account to contact the bot")
ac1 = acfactory.get_one_online_account()
lp.sec("sending a message to the bot")
bot_contact = ac1.create_contact(botproc.addr)
ch1 = ac1.create_chat_by_contact(bot_contact)
ch1.send_text("hello")
lp.sec("waiting for the bot-reply to arrive")
reply = ac1._evtracker.wait_next_incoming_message()
assert "hello" in reply.text
assert reply.chat == ch1
lp.sec("send quit sequence")
ch1.send_text("/quit")
botproc.wait()
def test_group_tracking_plugin(acfactory, lp):
lp.sec("creating one group-tracking bot and two temp accounts")
botproc = acfactory.run_bot_process(group_tracking, ffi=False)
ac1, ac2 = acfactory.get_two_online_accounts(quiet=True)
botproc.fnmatch_lines("""
*ac_configure_completed*
""")
ac1.add_account_plugin(FFIEventLogger(ac1, "ac1"))
ac2.add_account_plugin(FFIEventLogger(ac2, "ac2"))
lp.sec("creating bot test group with bot")
bot_contact = ac1.create_contact(botproc.addr)
ch = ac1.create_group_chat("bot test group")
ch.add_contact(bot_contact)
ch.send_text("hello")
botproc.fnmatch_lines("""
*ac_chat_modified*bot test group*
""")
lp.sec("adding third member {}".format(ac2.get_config("addr")))
contact3 = ac1.create_contact(ac2.get_config("addr"))
ch.add_contact(contact3)
reply = ac1._evtracker.wait_next_incoming_message()
assert "hello" in reply.text
lp.sec("now looking at what the bot received")
botproc.fnmatch_lines("""
*ac_member_added {}*
""".format(contact3.addr))
lp.sec("contact successfully added, now removing")
ch.remove_contact(contact3)
botproc.fnmatch_lines("""
*ac_member_removed {}*
""".format(contact3.addr))

7
python/fail_test.py Normal file
View File

@@ -0,0 +1,7 @@
from __future__ import print_function
from deltachat import capi
from deltachat.capi import ffi, lib
if __name__ == "__main__":
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
lib.dc_stop_io(ctx)

View File

@@ -19,6 +19,7 @@ if __name__ == "__main__":
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
if target == 'release':
cmd.append("--release")
print("running:", " ".join(cmd))
subprocess.check_call(cmd)
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)

View File

@@ -22,6 +22,11 @@ def main():
packages=setuptools.find_packages('src'),
package_dir={'': 'src'},
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
entry_points = {
'pytest11': [
'deltachat.testplugin = deltachat.testplugin',
],
},
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',

View File

@@ -1,6 +1,13 @@
from deltachat import capi, const
from deltachat.capi import ffi
from deltachat.account import Account # noqa
import sys
from . import capi, const, hookspec # noqa
from .capi import ffi # noqa
from .account import Account # noqa
from .message import Message # noqa
from .contact import Contact # noqa
from .chat import Chat # noqa
from .hookspec import account_hookimpl, global_hookimpl # noqa
from . import events
from pkg_resources import get_distribution, DistributionNotFound
try:
@@ -10,67 +17,71 @@ except DistributionNotFound:
__version__ = "0.0.0.dev0-unknown"
_DC_CALLBACK_MAP = {}
@capi.ffi.def_extern()
def py_dc_callback(ctx, evt, data1, data2):
"""The global event handler.
CFFI only allows us to set one global event handler, so this one
looks up the correct event handler for the given context.
"""
try:
callback = _DC_CALLBACK_MAP.get(ctx, lambda *a: 0)
except AttributeError:
# we are in a deep in GC-free/interpreter shutdown land
# nothing much better to do here than:
return 0
# the following code relates to the deltachat/_build.py's helper
# function which provides us signature info of an event call
evt_name = get_dc_event_name(evt)
event_sig_types = capi.lib.dc_get_event_signature_types(evt)
if data1 and event_sig_types & 1:
data1 = ffi.string(ffi.cast('char*', data1)).decode("utf8")
if data2 and event_sig_types & 2:
data2 = ffi.string(ffi.cast('char*', data2)).decode("utf8")
try:
if isinstance(data2, bytes):
data2 = data2.decode("utf8")
except UnicodeDecodeError:
# XXX ignoring the decode error is not quite correct but for now
# i don't want to hunt down encoding problems in the c lib
pass
try:
ret = callback(ctx, evt_name, data1, data2)
if ret is None:
ret = 0
assert isinstance(ret, int), repr(ret)
if event_sig_types & 4:
return ffi.cast('uintptr_t', ret)
elif event_sig_types & 8:
return ffi.cast('int', ret)
except: # noqa
raise
ret = 0
return ret
def set_context_callback(dc_context, func):
_DC_CALLBACK_MAP[dc_context] = func
def clear_context_callback(dc_context):
try:
_DC_CALLBACK_MAP.pop(dc_context, None)
except AttributeError:
pass
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
if not _DC_EVENTNAME_MAP:
for name, val in vars(const).items():
if name.startswith("DC_EVENT_"):
_DC_EVENTNAME_MAP[val] = name
return _DC_EVENTNAME_MAP[integer]
def register_global_plugin(plugin):
""" Register a global plugin which implements one or more
of the :class:`deltachat.hookspec.Global` hooks.
"""
gm = hookspec.Global._get_plugin_manager()
gm.register(plugin)
gm.check_pending()
def unregister_global_plugin(plugin):
gm = hookspec.Global._get_plugin_manager()
gm.unregister(plugin)
register_global_plugin(events)
def run_cmdline(argv=None, account_plugins=None):
""" Run a simple default command line app, registering the specified
account plugins. """
import argparse
if argv is None:
argv = sys.argv
parser = argparse.ArgumentParser(prog=argv[0] if argv else None)
parser.add_argument("db", action="store", help="database file")
parser.add_argument("--show-ffi", action="store_true", help="show low level ffi events")
parser.add_argument("--email", action="store", help="email address")
parser.add_argument("--password", action="store", help="password")
args = parser.parse_args(argv[1:])
ac = Account(args.db)
if args.show_ffi:
log = events.FFIEventLogger(ac, "bot")
ac.add_account_plugin(log)
for plugin in account_plugins or []:
print("adding plugin", plugin)
ac.add_account_plugin(plugin)
if not ac.is_configured():
assert args.email and args.password, (
"you must specify --email and --password once to configure this database/account"
)
ac.set_config("addr", args.email)
ac.set_config("mail_pw", args.password)
ac.set_config("mvbox_move", "0")
ac.set_config("mvbox_watch", "0")
ac.set_config("sentbox_watch", "0")
ac.configure()
ac.wait_configure_finish()
# start IO threads and configure if neccessary
ac.start_io()
print("{}: waiting for message".format(ac.get_config("addr")))
ac.wait_shutdown()

View File

@@ -45,22 +45,9 @@ def ffibuilder():
'deltachat.capi',
"""
#include <deltachat.h>
const char * dupstring_helper(const char* string)
int dc_event_has_string_data(int e)
{
return strdup(string);
}
int dc_get_event_signature_types(int e)
{
int result = 0;
if (DC_EVENT_DATA1_IS_STRING(e))
result |= 1;
if (DC_EVENT_DATA2_IS_STRING(e))
result |= 2;
if (DC_EVENT_RETURNS_STRING(e))
result |= 4;
if (DC_EVENT_RETURNS_INT(e))
result |= 8;
return result;
return DC_EVENT_DATA2_IS_STRING(e);
}
""",
include_dirs=incs,
@@ -71,8 +58,7 @@ def ffibuilder():
builder.cdef("""
typedef int... time_t;
void free(void *ptr);
extern const char * dupstring_helper(const char* string);
extern int dc_get_event_signature_types(int);
extern int dc_event_has_string_data(int);
""")
distutils.log.set_verbosity(distutils.log.INFO)
cc = distutils.ccompiler.new_compiler(force=True)
@@ -92,13 +78,6 @@ def ffibuilder():
finally:
shutil.rmtree(tmpdir)
builder.cdef("""
extern "Python" uintptr_t py_dc_callback(
dc_context_t* context,
int event,
uintptr_t data1,
uintptr_t data2);
""")
return builder

View File

@@ -1,22 +1,24 @@
""" Account class implementation. """
from __future__ import print_function
import atexit
import threading
from contextlib import contextmanager
from email.utils import parseaddr
from threading import Event
import os
import time
from array import array
from queue import Queue
import deltachat
from . import const
from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
from .chat import Chat
from .message import Message
from .contact import Contact
from .eventlogger import EventLogger
from .hookspec import get_plugin_manager, hookimpl
from .tracker import ImexTracker, ConfigureTracker
from . import hookspec
from .events import EventThread
class MissingCredentials(ValueError):
""" Account is missing `addr` and `mail_pw` config values. """
class Account(object):
@@ -24,45 +26,53 @@ class Account(object):
by the underlying deltachat core library. All public Account methods are
meant to be memory-safe and return memory-safe objects.
"""
def __init__(self, db_path, logid=None, os_name=None, debug=True):
MissingCredentials = MissingCredentials
def __init__(self, db_path, os_name=None, logging=True):
""" initialize account object.
:param db_path: a path to the account database. The database
will be created if it doesn't exist.
:param logid: an optional logging prefix that should be used with
the default internal logging.
:param os_name: this will be put to the X-Mailer header in outgoing messages
:param debug: turn on debug logging for events.
"""
self._dc_context = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, as_dc_charpointer(os_name)),
_destroy_dc_context,
)
self._evlogger = EventLogger(self, logid, debug)
self._threads = IOThreads(self._dc_context, self._evlogger._log_event)
# initialize per-account plugin system
self._pm = hookspec.PerAccount._make_plugin_manager()
self._logging = logging
# register event call back and initialize plugin system
def _ll_event(ctx, evt_name, data1, data2):
assert ctx == self._dc_context
self.pluggy.hook.process_low_level_event(
account=self, event_name=evt_name, data1=data1, data2=data2
)
self.add_account_plugin(self)
self.pluggy = get_plugin_manager()
self.pluggy.register(self._evlogger)
deltachat.set_context_callback(self._dc_context, _ll_event)
# open database
self.db_path = db_path
if hasattr(db_path, "encode"):
db_path = db_path.encode("utf8")
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
raise ValueError("Could not dc_open: {}".format(db_path))
self._dc_context = ffi.gc(
lib.dc_context_new(as_dc_charpointer(os_name), db_path, ffi.NULL),
lib.dc_context_unref,
)
if self._dc_context == ffi.NULL:
raise ValueError("FAILED dc_context_new: {} {}".format(os_name, db_path))
self._shutdown_event = Event()
self._event_thread = EventThread(self)
self._configkeys = self.get_config("sys.config_keys").split()
atexit.register(self.shutdown)
hook = hookspec.Global._get_plugin_manager().hook
hook.dc_account_init(account=self)
def disable_logging(self):
""" disable logging. """
self._logging = False
def enable_logging(self):
""" re-enable logging. """
self._logging = True
# def __del__(self):
# self.shutdown()
def log(self, msg):
if self._logging:
self._pm.hook.ac_log_line(message=msg)
def _check_config_key(self, name):
if name not in self._configkeys:
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
@@ -134,16 +144,15 @@ class Account(object):
if res == 0:
raise Exception("Failed to set key")
def configure(self, **kwargs):
""" set config values and configure this account.
def update_config(self, kwargs):
""" update config values.
:param kwargs: name=value config settings for this account.
values need to be unicode.
:returns: None
"""
for name, value in kwargs.items():
self.set_config(name, value)
lib.dc_configure(self._dc_context)
for key, value in kwargs.items():
self.set_config(key, str(value))
def is_configured(self):
""" determine if the account is configured already; an initial connection
@@ -151,7 +160,7 @@ class Account(object):
:returns: True if account is configured.
"""
return lib.dc_is_configured(self._dc_context)
return True if lib.dc_is_configured(self._dc_context) else False
def set_avatar(self, img_path):
"""Set self avatar.
@@ -202,8 +211,7 @@ class Account(object):
:returns: :class:`deltachat.contact.Contact`
"""
self.check_is_configured()
return Contact(self._dc_context, const.DC_CONTACT_ID_SELF)
return Contact(self, const.DC_CONTACT_ID_SELF)
def create_contact(self, email, name=None):
""" create a (new) Contact. If there already is a Contact
@@ -214,11 +222,14 @@ class Account(object):
:param name: display name for this contact (optional)
:returns: :class:`deltachat.contact.Contact` instance.
"""
name = as_dc_charpointer(name)
email = as_dc_charpointer(email)
contact_id = lib.dc_create_contact(self._dc_context, name, email)
realname, addr = parseaddr(email)
if name:
realname = name
realname = as_dc_charpointer(realname)
addr = as_dc_charpointer(addr)
contact_id = lib.dc_create_contact(self._dc_context, realname, addr)
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
return Contact(self._dc_context, contact_id)
return Contact(self, contact_id)
def delete_contact(self, contact):
""" delete a Contact.
@@ -227,10 +238,18 @@ class Account(object):
:returns: True if deletion succeeded (contact was deleted)
"""
contact_id = contact.id
assert contact._dc_context == self._dc_context
assert contact.account == self
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
def get_contact_by_addr(self, email):
""" get a contact for the email address or None if it's blocked or doesn't exist. """
_, addr = parseaddr(email)
addr = as_dc_charpointer(addr)
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
if contact_id:
return self.get_contact_by_id(contact_id)
def get_contacts(self, query=None, with_self=False, only_verified=False):
""" get a (filtered) list of contacts.
@@ -250,7 +269,15 @@ class Account(object):
lib.dc_get_contacts(self._dc_context, flags, query),
lib.dc_array_unref
)
return list(iter_array(dc_array, lambda x: Contact(self._dc_context, x)))
return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_fresh_messages(self):
""" yield all fresh messages from all chats. """
dc_array = ffi.gc(
lib.dc_get_fresh_msgs(self._dc_context),
lib.dc_array_unref
)
yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
def create_chat_by_contact(self, contact):
""" create or get an existing 1:1 chat object for the specified contact or contact id.
@@ -259,7 +286,7 @@ class Account(object):
:returns: a :class:`deltachat.chat.Chat` object.
"""
if hasattr(contact, "id"):
if contact._dc_context != self._dc_context:
if contact.account != self:
raise ValueError("Contact belongs to a different Account")
contact_id = contact.id
else:
@@ -272,11 +299,14 @@ class Account(object):
""" create or get an existing chat object for the
the specified message.
If this message is in the deaddrop chat then
the sender will become an accepted contact.
:param message: messsage id or message instance.
:returns: a :class:`deltachat.chat.Chat` object.
"""
if hasattr(message, "id"):
if self._dc_context != message._dc_context:
if message.account != self:
raise ValueError("Message belongs to a different Account")
msg_id = message.id
else:
@@ -324,6 +354,13 @@ class Account(object):
"""
return Message.from_db(self, msg_id)
def get_contact_by_id(self, contact_id):
""" return Contact instance or None.
:param contact_id: integer id of this contact.
:returns: None or :class:`deltachat.contact.Contact` instance.
"""
return Contact(self, contact_id)
def get_chat_by_id(self, chat_id):
""" return Chat instance.
:param chat_id: integer id of this chat.
@@ -368,13 +405,18 @@ class Account(object):
lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids))
def export_self_keys(self, path):
""" export public and private keys to the specified directory. """
""" export public and private keys to the specified directory.
Note that the account does not have to be started.
"""
return self._export(path, imex_cmd=1)
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.
"""
export_files = self._export(path, 11)
if len(export_files) != 1:
@@ -382,16 +424,16 @@ class Account(object):
return export_files[0]
def _export(self, path, imex_cmd):
with ImexTracker(self) as imex_tracker:
with self.temp_plugin(ImexTracker()) as imex_tracker:
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
if not self._threads.is_started():
lib.dc_perform_imap_jobs(self._dc_context)
return imex_tracker.wait_finish()
def import_self_keys(self, path):
""" Import private keys found in the `path` directory.
The last imported key is made the default keys unless its name
contains the string legacy. Public keys are not imported.
Note that the account does not have to be started.
"""
self._import(path, imex_cmd=2)
@@ -404,10 +446,8 @@ class Account(object):
self._import(path, imex_cmd=12)
def _import(self, path, imex_cmd):
with ImexTracker(self) as imex_tracker:
with self.temp_plugin(ImexTracker()) as imex_tracker:
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
if not self._threads.is_started():
lib.dc_perform_imap_jobs(self._dc_context)
imex_tracker.wait_finish()
def initiate_key_transfer(self):
@@ -416,8 +456,8 @@ class Account(object):
If sending out was unsuccessful, a RuntimeError is raised.
"""
self.check_is_configured()
if not self._threads.is_started():
raise RuntimeError("threads not running, can not send out")
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")
@@ -473,46 +513,6 @@ class Account(object):
raise ValueError("could not join group")
return Chat(self, chat_id)
def stop_ongoing(self):
lib.dc_stop_ongoing_process(self._dc_context)
#
# meta API for start/stop and event based processing
#
def wait_next_incoming_message(self):
""" wait for and return next incoming message. """
ev = self._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
return self.get_message_by_id(ev[2])
def start_threads(self, mvbox=False, sentbox=False):
""" start IMAP/SMTP threads (and configure account if it hasn't happened).
:raises: ValueError if 'addr' or 'mail_pw' are not configured.
:returns: None
"""
if not self.is_configured():
self.configure()
self._threads.start(mvbox=mvbox, sentbox=sentbox)
def stop_threads(self, wait=True):
""" stop IMAP/SMTP threads. """
if self._threads.is_started():
self.stop_ongoing()
self._threads.stop(wait=wait)
def shutdown(self, wait=True):
""" stop threads and close and remove underlying dc_context and callbacks. """
if hasattr(self, "_dc_context") and hasattr(self, "_threads"):
# print("SHUTDOWN", self)
self.stop_threads(wait=False)
lib.dc_close(self._dc_context)
self.stop_threads(wait=wait) # to wait for threads
deltachat.clear_context_callback(self._dc_context)
del self._dc_context
atexit.unregister(self.shutdown)
self.pluggy.unregister(self._evlogger)
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
"""set a new location. It effects all chats where we currently
have enabled location streaming.
@@ -527,131 +527,109 @@ class Account(object):
if dc_res == 0:
raise ValueError("no chat is streaming locations")
#
# meta API for start/stop and event based processing
#
class ImexTracker:
def __init__(self, account):
self._imex_events = Queue()
self.account = account
def add_account_plugin(self, plugin, name=None):
""" add an account plugin which implements one or more of
the :class:`deltachat.hookspec.PerAccount` hooks.
"""
self._pm.register(plugin, name=name)
self._pm.check_pending()
return plugin
def __enter__(self):
self.account.pluggy.register(self)
return self
def remove_account_plugin(self, plugin, name=None):
""" remove an account plugin. """
self._pm.unregister(plugin, name=name)
def __exit__(self, *args):
self.account.pluggy.unregister(self)
@contextmanager
def temp_plugin(self, plugin):
""" run a with-block with the given plugin temporarily registered. """
self._pm.register(plugin)
yield plugin
self._pm.unregister(plugin)
@hookimpl
def process_low_level_event(self, account, event_name, data1, data2):
# there could be multiple accounts instantiated
if self.account is not account:
return
if event_name == "DC_EVENT_IMEX_PROGRESS":
self._imex_events.put(data1)
elif event_name == "DC_EVENT_IMEX_FILE_WRITTEN":
self._imex_events.put(data1)
def stop_ongoing(self):
""" Stop ongoing securejoin, configuration or other core jobs. """
lib.dc_stop_ongoing_process(self._dc_context)
def wait_finish(self, progress_timeout=60):
""" Return list of written files, raise ValueError if ExportFailed. """
files_written = []
while True:
ev = self._imex_events.get(timeout=progress_timeout)
if isinstance(ev, str):
files_written.append(ev)
elif ev == 0:
raise ValueError("export failed, exp-files: {}".format(files_written))
elif ev == 1000:
return files_written
def start_io(self):
""" start this account's IO scheduling (Rust-core async scheduler)
If this account is not configured an Exception is raised.
You need to call account.configure() and account.wait_configure_finish()
before.
class IOThreads:
def __init__(self, dc_context, log_event=lambda *args: None):
self._dc_context = dc_context
self._thread_quitflag = False
self._name2thread = {}
self._log_event = log_event
You may call `stop_scheduler`, `wait_shutdown` or `shutdown` after the
account is started.
:raises MissingCredentials: if `addr` and `mail_pw` values are not set.
:raises ConfigureFailed: if the account could not be configured.
:returns: None (account is configured and with io-scheduling running)
"""
if not self.is_configured():
raise ValueError("account not configured, cannot start io")
lib.dc_start_io(self._dc_context)
def configure(self):
assert not self.is_configured()
assert not hasattr(self, "_configtracker")
if not self.get_config("addr") or not self.get_config("mail_pw"):
raise MissingCredentials("addr or mail_pwd not set in config")
if hasattr(self, "_configtracker"):
self.remove_account_plugin(self._configtracker)
self._configtracker = ConfigureTracker()
self.add_account_plugin(self._configtracker)
lib.dc_configure(self._dc_context)
def wait_configure_finish(self):
try:
self._configtracker.wait_finish()
finally:
self.remove_account_plugin(self._configtracker)
del self._configtracker
def is_started(self):
return len(self._name2thread) > 0
return self._event_thread.is_alive() and bool(lib.dc_is_io_running(self._dc_context))
def start(self, imap=True, smtp=True, mvbox=False, sentbox=False):
assert not self.is_started()
if imap:
self._start_one_thread("inbox", self.imap_thread_run)
if mvbox:
self._start_one_thread("mvbox", self.mvbox_thread_run)
if sentbox:
self._start_one_thread("sentbox", self.sentbox_thread_run)
if smtp:
self._start_one_thread("smtp", self.smtp_thread_run)
def wait_shutdown(self):
""" wait until shutdown of this account has completed. """
self._shutdown_event.wait()
def _start_one_thread(self, name, func):
self._name2thread[name] = t = threading.Thread(target=func, name=name)
t.setDaemon(1)
t.start()
def stop_io(self):
""" stop core IO scheduler if it is running. """
self.log("stop_ongoing")
self.stop_ongoing()
def stop(self, wait=False):
self._thread_quitflag = True
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")
# Workaround for a race condition. Make sure that thread is
# not in between checking for quitflag and entering idle.
time.sleep(0.5)
def shutdown(self):
""" shutdown and destroy account (stop callback thread, close and remove
underlying dc_context)."""
if self._dc_context is None:
return
lib.dc_interrupt_imap_idle(self._dc_context)
lib.dc_interrupt_smtp_idle(self._dc_context)
lib.dc_interrupt_mvbox_idle(self._dc_context)
lib.dc_interrupt_sentbox_idle(self._dc_context)
if wait:
for name, thread in self._name2thread.items():
thread.join()
self.stop_io()
def imap_thread_run(self):
self._log_event("py-bindings-info", 0, "INBOX THREAD START")
while not self._thread_quitflag:
lib.dc_perform_imap_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_imap_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_imap_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "INBOX THREAD FINISHED")
self.log("remove dc_context references")
# the dc_context_unref triggers get_next_event to return ffi.NULL
# which in turns makes the event thread finish execution
self._dc_context = None
def mvbox_thread_run(self):
self._log_event("py-bindings-info", 0, "MVBOX THREAD START")
while not self._thread_quitflag:
lib.dc_perform_mvbox_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_mvbox_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_mvbox_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "MVBOX THREAD FINISHED")
self.log("wait for event thread to finish")
self._event_thread.wait()
def sentbox_thread_run(self):
self._log_event("py-bindings-info", 0, "SENTBOX THREAD START")
while not self._thread_quitflag:
lib.dc_perform_sentbox_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_sentbox_fetch(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_sentbox_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "SENTBOX THREAD FINISHED")
self._shutdown_event.set()
def smtp_thread_run(self):
self._log_event("py-bindings-info", 0, "SMTP THREAD START")
while not self._thread_quitflag:
lib.dc_perform_smtp_jobs(self._dc_context)
if not self._thread_quitflag:
lib.dc_perform_smtp_idle(self._dc_context)
self._log_event("py-bindings-info", 0, "SMTP THREAD FINISHED")
def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):
# destructor for dc_context
dc_context_unref(dc_context)
try:
deltachat.clear_context_callback(dc_context)
except (TypeError, AttributeError):
# we are deep into Python Interpreter shutdown,
# so no need to clear the callback context mapping.
pass
hook = hookspec.Global._get_plugin_manager().hook
hook.dc_account_after_shutdown(account=self)
self.log("shutdown finished")
class ScannedQRCode:

View File

@@ -19,23 +19,22 @@ class Chat(object):
def __init__(self, account, id):
self.account = account
self._dc_context = account._dc_context
self.id = id
def __eq__(self, other):
return self.id == getattr(other, "id", None) and \
self._dc_context == getattr(other, "_dc_context", None)
self.account._dc_context == other.account._dc_context
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return "<Chat id={} name={} dc_context={}>".format(self.id, self.get_name(), self._dc_context)
return "<Chat id={} name={}>".format(self.id, self.get_name())
@property
def _dc_chat(self):
return ffi.gc(
lib.dc_get_chat(self._dc_context, self.id),
lib.dc_get_chat(self.account._dc_context, self.id),
lib.dc_chat_unref
)
@@ -47,10 +46,20 @@ class Chat(object):
- does not delete messages on server
- the chat or contact is not blocked, new message will arrive
"""
lib.dc_delete_chat(self._dc_context, self.id)
lib.dc_delete_chat(self.account._dc_context, self.id)
# ------ chat status/metadata API ------------------------------
def is_group(self):
""" return true if this chat is a group chat.
: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
)
def is_deaddrop(self):
""" return true if this chat is a deaddrop chat.
@@ -95,7 +104,7 @@ class Chat(object):
:returns: None
"""
name = as_dc_charpointer(name)
return lib.dc_set_chat_name(self._dc_context, self.id, name)
return lib.dc_set_chat_name(self.account._dc_context, self.id, name)
def mute(self, duration=None):
""" mutes the chat
@@ -107,7 +116,7 @@ class Chat(object):
mute_duration = -1
else:
mute_duration = duration
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, mute_duration)
ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, mute_duration)
if not bool(ret):
raise ValueError("Call to dc_set_chat_mute_duration failed")
@@ -116,7 +125,7 @@ class Chat(object):
:returns: None
"""
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, 0)
ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, 0)
if not bool(ret):
raise ValueError("Failed to unmute chat")
@@ -129,7 +138,7 @@ class Chat(object):
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
def get_type(self):
""" return type of this chat.
""" (deprecated) return type of this chat.
:returns: one of const.DC_CHAT_TYPE_*
"""
@@ -142,7 +151,7 @@ class Chat(object):
in a second channel (typically used by mobiles with QRcode-show + scan UX)
where account.join_with_qrcode(qr) needs to be called.
"""
res = lib.dc_get_securejoin_qr(self._dc_context, self.id)
res = lib.dc_get_securejoin_qr(self.account._dc_context, self.id)
return from_dc_charpointer(res)
# ------ chat messaging API ------------------------------
@@ -164,7 +173,7 @@ class Chat(object):
assert msg.id != 0
# get a fresh copy of dc_msg, the core needs it
msg = Message.from_db(self.account, msg.id)
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
if sent_id == 0:
raise ValueError("message could not be sent")
# modify message in place to avoid bad state for the caller
@@ -179,7 +188,7 @@ class Chat(object):
:returns: the resulting :class:`deltachat.message.Message` instance
"""
msg = as_dc_charpointer(text)
msg_id = lib.dc_send_text_msg(self._dc_context, self.id, msg)
msg_id = lib.dc_send_text_msg(self.account._dc_context, self.id, msg)
if msg_id == 0:
raise ValueError("message could not be send, does chat exist?")
return Message.from_db(self.account, msg_id)
@@ -194,7 +203,7 @@ class Chat(object):
"""
msg = Message.new_empty(self.account, view_type="file")
msg.set_file(path, mime_type)
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
if sent_id == 0:
raise ValueError("message could not be sent")
return Message.from_db(self.account, sent_id)
@@ -209,7 +218,7 @@ class Chat(object):
mime_type = mimetypes.guess_type(path)[0]
msg = Message.new_empty(self.account, view_type="image")
msg.set_file(path, mime_type)
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
if sent_id == 0:
raise ValueError("message could not be sent")
return Message.from_db(self.account, sent_id)
@@ -220,7 +229,7 @@ class Chat(object):
:param msg: the message to be prepared.
:returns: :class:`deltachat.message.Message` instance.
"""
msg_id = lib.dc_prepare_msg(self._dc_context, self.id, msg._dc_msg)
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
@@ -256,7 +265,7 @@ class Chat(object):
msg = Message.from_db(self.account, message.id)
# pass 0 as chat-id because core-docs say it's ok when out-preparing
sent_id = lib.dc_send_msg(self._dc_context, 0, msg._dc_msg)
sent_id = lib.dc_send_msg(self.account._dc_context, 0, msg._dc_msg)
if sent_id == 0:
raise ValueError("message could not be sent")
assert sent_id == msg.id
@@ -270,9 +279,9 @@ class Chat(object):
:returns: None
"""
if message is None:
lib.dc_set_draft(self._dc_context, self.id, ffi.NULL)
lib.dc_set_draft(self.account._dc_context, self.id, ffi.NULL)
else:
lib.dc_set_draft(self._dc_context, self.id, message._dc_msg)
lib.dc_set_draft(self.account._dc_context, self.id, message._dc_msg)
def get_draft(self):
""" get draft message for this chat.
@@ -280,7 +289,7 @@ class Chat(object):
:param message: a :class:`Message` instance
:returns: Message object or None (if no draft available)
"""
x = lib.dc_get_draft(self._dc_context, self.id)
x = lib.dc_get_draft(self.account._dc_context, self.id)
if x == ffi.NULL:
return None
dc_msg = ffi.gc(x, lib.dc_msg_unref)
@@ -292,7 +301,7 @@ class Chat(object):
:returns: list of :class:`deltachat.message.Message` objects for this chat.
"""
dc_array = ffi.gc(
lib.dc_get_chat_msgs(self._dc_context, self.id, 0, 0),
lib.dc_get_chat_msgs(self.account._dc_context, self.id, 0, 0),
lib.dc_array_unref
)
return list(iter_array(dc_array, lambda x: Message.from_db(self.account, x)))
@@ -302,18 +311,18 @@ class Chat(object):
:returns: number of fresh messages
"""
return lib.dc_get_fresh_msg_cnt(self._dc_context, self.id)
return lib.dc_get_fresh_msg_cnt(self.account._dc_context, self.id)
def mark_noticed(self):
""" mark all messages in this chat as noticed.
Noticed messages are no longer fresh.
"""
return lib.dc_marknoticed_chat(self._dc_context, self.id)
return lib.dc_marknoticed_chat(self.account._dc_context, self.id)
def get_summary(self):
""" return dictionary with summary information. """
dc_res = lib.dc_chat_get_info_json(self._dc_context, self.id)
dc_res = lib.dc_chat_get_info_json(self.account._dc_context, self.id)
s = from_dc_charpointer(dc_res)
return json.loads(s)
@@ -326,7 +335,7 @@ class Chat(object):
:raises ValueError: if contact could not be added
:returns: None
"""
ret = lib.dc_add_contact_to_chat(self._dc_context, self.id, contact.id)
ret = lib.dc_add_contact_to_chat(self.account._dc_context, self.id, contact.id)
if ret != 1:
raise ValueError("could not add contact {!r} to chat".format(contact))
@@ -337,7 +346,7 @@ class Chat(object):
:raises ValueError: if contact could not be removed
:returns: None
"""
ret = lib.dc_remove_contact_from_chat(self._dc_context, self.id, contact.id)
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))
@@ -349,11 +358,11 @@ class Chat(object):
"""
from .contact import Contact
dc_array = ffi.gc(
lib.dc_get_chat_contacts(self._dc_context, self.id),
lib.dc_get_chat_contacts(self.account._dc_context, self.id),
lib.dc_array_unref
)
return list(iter_array(
dc_array, lambda id: Contact(self._dc_context, id))
dc_array, lambda id: Contact(self.account, id))
)
def set_profile_image(self, img_path):
@@ -368,7 +377,7 @@ class Chat(object):
"""
assert os.path.exists(img_path), img_path
p = as_dc_charpointer(img_path)
res = lib.dc_set_chat_profile_image(self._dc_context, self.id, p)
res = lib.dc_set_chat_profile_image(self.account._dc_context, self.id, p)
if res != 1:
raise ValueError("Setting Profile Image {!r} failed".format(p))
@@ -381,7 +390,7 @@ class Chat(object):
:raises ValueError: if profile image could not be reset
:returns: None
"""
res = lib.dc_set_chat_profile_image(self._dc_context, self.id, ffi.NULL)
res = lib.dc_set_chat_profile_image(self.account._dc_context, self.id, ffi.NULL)
if res != 1:
raise ValueError("Removing Profile Image failed")
@@ -405,19 +414,13 @@ class Chat(object):
"""
return lib.dc_chat_get_color(self._dc_chat)
def get_subtitle(self):
"""return the subtitle of the chat
:returns: the subtitle
"""
return from_dc_charpointer(lib.dc_chat_get_subtitle(self._dc_chat))
# ------ location streaming API ------------------------------
def is_sending_locations(self):
"""return True if this chat has location-sending enabled currently.
:returns: True if location sending is enabled.
"""
return lib.dc_is_sending_locations_to_chat(self._dc_context, self.id)
return lib.dc_is_sending_locations_to_chat(self.account._dc_context, self.id)
def is_archived(self):
"""return True if this chat is archived.
@@ -430,7 +433,7 @@ class Chat(object):
all subsequent messages will carry a location with them.
"""
lib.dc_send_locations_to_chat(self._dc_context, self.id, seconds)
lib.dc_send_locations_to_chat(self.account._dc_context, self.id, seconds)
def get_locations(self, contact=None, timestamp_from=None, timestamp_to=None):
"""return list of locations for the given contact in the given timespan.
@@ -454,7 +457,7 @@ class Chat(object):
else:
contact_id = contact.id
dc_array = lib.dc_get_locations(self._dc_context, self.id, contact_id, time_from, time_to)
dc_array = lib.dc_get_locations(self.account._dc_context, self.id, contact_id, time_from, time_to)
return [
Location(
latitude=lib.dc_array_get_latitude(dc_array, i),

View File

@@ -11,6 +11,7 @@ from os.path import join as joinpath
DC_GCL_ARCHIVED_ONLY = 0x01
DC_GCL_NO_SPECIALS = 0x02
DC_GCL_ADD_ALLDONE_HINT = 0x04
DC_GCL_FOR_FORWARDING = 0x08
DC_GCL_VERIFIED_ONLY = 0x01
DC_GCL_ADD_SELF = 0x02
DC_QR_ASK_VERIFYCONTACT = 200
@@ -98,7 +99,6 @@ DC_EVENT_IMEX_PROGRESS = 2051
DC_EVENT_IMEX_FILE_WRITTEN = 2052
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
DC_EVENT_SECUREJOIN_MEMBER_ADDED = 2062
DC_EVENT_FILE_COPIED = 2055
DC_EVENT_IS_OFFLINE = 2081
DC_EVENT_GET_STRING = 2091
@@ -115,8 +115,6 @@ DC_CHAT_VISIBILITY_PINNED = 2
DC_STR_NOMESSAGES = 1
DC_STR_SELF = 2
DC_STR_DRAFT = 3
DC_STR_MEMBER = 4
DC_STR_CONTACT = 6
DC_STR_VOICEMESSAGE = 7
DC_STR_DEADDROP = 8
DC_STR_IMAGE = 9
@@ -148,7 +146,6 @@ DC_STR_ARCHIVEDCHATS = 40
DC_STR_STARREDMSGS = 41
DC_STR_AC_SETUP_MSG_SUBJECT = 42
DC_STR_AC_SETUP_MSG_BODY = 43
DC_STR_SELFTALK_SUBTITLE = 50
DC_STR_CANNOT_LOGIN = 60
DC_STR_SERVER_RESPONSE = 61
DC_STR_MSGACTIONBYUSER = 62

View File

@@ -10,23 +10,23 @@ class Contact(object):
You obtain instances of it through :class:`deltachat.account.Account`.
"""
def __init__(self, dc_context, id):
self._dc_context = dc_context
def __init__(self, account, id):
self.account = account
self.id = id
def __eq__(self, other):
return self._dc_context == other._dc_context and self.id == other.id
return self.account._dc_context == other.account._dc_context and self.id == other.id
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self._dc_context)
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self.account._dc_context)
@property
def _dc_contact(self):
return ffi.gc(
lib.dc_get_contact(self._dc_context, self.id),
lib.dc_get_contact(self.account._dc_context, self.id),
lib.dc_contact_unref
)
@@ -57,3 +57,7 @@ class Contact(object):
if dc_res == ffi.NULL:
return None
return from_dc_charpointer(dc_res)
def get_chat(self):
"""return 1:1 chat for this contact. """
return self.account.create_chat_by_contact(self)

View File

@@ -1,81 +0,0 @@
import threading
import re
import time
from queue import Queue, Empty
from .hookspec import hookimpl
class EventLogger:
_loglock = threading.RLock()
def __init__(self, account, logid=None, debug=True):
self.account = account
self._event_queue = Queue()
self._debug = debug
if logid is None:
logid = str(self.account._dc_context).strip(">").split()[-1]
self.logid = logid
self._timeout = None
self.init_time = time.time()
@hookimpl
def process_low_level_event(self, account, event_name, data1, data2):
if self.account == account:
self._log_event(event_name, data1, data2)
self._event_queue.put((event_name, data1, data2))
def set_timeout(self, timeout):
self._timeout = timeout
def consume_events(self, check_error=True):
while not self._event_queue.empty():
self.get(check_error=check_error)
def get(self, timeout=None, check_error=True):
timeout = timeout or self._timeout
ev = self._event_queue.get(timeout=timeout)
if check_error and ev[0] == "DC_EVENT_ERROR":
raise ValueError("{}({!r},{!r})".format(*ev))
return ev
def ensure_event_not_queued(self, event_name_regex):
__tracebackhide__ = True
rex = re.compile("(?:{}).*".format(event_name_regex))
while 1:
try:
ev = self._event_queue.get(False)
except Empty:
break
else:
assert not rex.match(ev[0]), "event found {}".format(ev)
def get_matching(self, event_name_regex, check_error=True, timeout=None):
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
rex = re.compile("(?:{}).*".format(event_name_regex))
while 1:
ev = self.get(timeout=timeout, check_error=check_error)
if rex.match(ev[0]):
return ev
def get_info_matching(self, regex):
rex = re.compile("(?:{}).*".format(regex))
while 1:
ev = self.get_matching("DC_EVENT_INFO")
if rex.match(ev[2]):
return ev
def _log_event(self, evt_name, data1, data2):
# don't show events that are anyway empty impls now
if evt_name == "DC_EVENT_GET_STRING":
return
if self._debug:
evpart = "{}({!r},{!r})".format(evt_name, data1, data2)
self._log(evpart)
def _log(self, msg):
t = threading.currentThread()
tname = getattr(t, "name", t)
if tname == "MainThread":
tname = "MAIN"
with self._loglock:
print("{:2.2f} [{}-{}] {}".format(time.time() - self.init_time, tname, self.logid, msg))

View File

@@ -0,0 +1,217 @@
import threading
import time
import re
from queue import Queue, Empty
import deltachat
from .hookspec import account_hookimpl
from contextlib import contextmanager
from .capi import ffi, lib
from .message import map_system_message
from .cutil import from_dc_charpointer
class FFIEvent:
def __init__(self, name, data1, data2):
self.name = name
self.data1 = data1
self.data2 = data2
def __str__(self):
return "{name} data1={data1} data2={data2}".format(**self.__dict__)
class FFIEventLogger:
""" If you register an instance of this logger with an Account
you'll get all ffi-events printed.
"""
# to prevent garbled logging
_loglock = threading.RLock()
def __init__(self, account, logid):
"""
:param logid: an optional logging prefix that should be used with
the default internal logging.
"""
self.account = account
self.logid = logid
self.init_time = time.time()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
self.account.log(str(ffi_event))
@account_hookimpl
def ac_log_line(self, message):
t = threading.currentThread()
tname = getattr(t, "name", t)
if tname == "MainThread":
tname = "MAIN"
elapsed = time.time() - self.init_time
locname = tname
if self.logid:
locname += "-" + self.logid
s = "{:2.2f} [{}] {}".format(elapsed, locname, message)
with self._loglock:
print(s, flush=True)
class FFIEventTracker:
def __init__(self, account, timeout=None):
self.account = account
self._timeout = timeout
self._event_queue = Queue()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
self._event_queue.put(ffi_event)
def set_timeout(self, timeout):
self._timeout = timeout
def consume_events(self, check_error=True):
while not self._event_queue.empty():
self.get(check_error=check_error)
def get(self, timeout=None, check_error=True):
timeout = timeout if timeout is not None else self._timeout
ev = self._event_queue.get(timeout=timeout)
if check_error and ev.name == "DC_EVENT_ERROR":
raise ValueError("unexpected event: {}".format(ev))
return ev
def iter_events(self, timeout=None, check_error=True):
while 1:
yield self.get(timeout=timeout, check_error=check_error)
def get_matching(self, event_name_regex, check_error=True, timeout=None):
rex = re.compile("(?:{}).*".format(event_name_regex))
for ev in self.iter_events(timeout=timeout, check_error=check_error):
if rex.match(ev.name):
return ev
def get_info_matching(self, regex):
rex = re.compile("(?:{}).*".format(regex))
while 1:
ev = self.get_matching("DC_EVENT_INFO")
if rex.match(ev.data2):
return ev
def ensure_event_not_queued(self, event_name_regex):
__tracebackhide__ = True
rex = re.compile("(?:{}).*".format(event_name_regex))
while 1:
try:
ev = self._event_queue.get(False)
except Empty:
break
else:
assert not rex.match(ev.name), "event found {}".format(ev)
def wait_securejoin_inviter_progress(self, target):
while 1:
event = self.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
if event.data2 >= target:
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), self.account)
break
def wait_next_incoming_message(self):
""" wait for and return next incoming message. """
ev = self.get_matching("DC_EVENT_INCOMING_MSG")
return self.account.get_message_by_id(ev.data2)
def wait_next_messages_changed(self):
""" wait for and return next message-changed message or None
if the event contains no msgid"""
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
if ev.data2 > 0:
return self.account.get_message_by_id(ev.data2)
class EventThread(threading.Thread):
""" Event Thread for an account.
With each Account init this callback thread is started.
"""
def __init__(self, account):
self.account = account
super(EventThread, self).__init__(name="events")
self.setDaemon(True)
self.start()
@contextmanager
def log_execution(self, message):
self.account.log(message + " START")
yield
self.account.log(message + " FINISHED")
def wait(self):
if self == threading.current_thread():
# we are in the callback thread and thus cannot
# wait for the thread-loop to finish.
return
self.join()
def run(self):
""" get and run events until shutdown. """
with self.log_execution("EVENT THREAD"):
self._inner_run()
def _inner_run(self):
event_emitter = ffi.gc(
lib.dc_get_event_emitter(self.account._dc_context),
lib.dc_event_emitter_unref,
)
while 1:
event = lib.dc_get_next_event(event_emitter)
if event == ffi.NULL:
break
evt = lib.dc_event_get_id(event)
data1 = lib.dc_event_get_data1_int(event)
# the following code relates to the deltachat/_build.py's helper
# function which provides us signature info of an event call
evt_name = deltachat.get_dc_event_name(evt)
if lib.dc_event_has_string_data(evt):
data2 = from_dc_charpointer(lib.dc_event_get_data3_str(event))
else:
data2 = lib.dc_event_get_data2_int(event)
lib.dc_event_unref(event)
ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2)
try:
self.account._pm.hook.ac_process_ffi_event(account=self, ffi_event=ffi_event)
for name, kwargs in self._map_ffi_event(ffi_event):
self.account.log("calling hook name={} kwargs={}".format(name, kwargs))
hook = getattr(self.account._pm.hook, name)
hook(**kwargs)
except Exception:
if self.account._dc_context is not None:
raise
def _map_ffi_event(self, ffi_event):
name = ffi_event.name
account = self.account
if name == "DC_EVENT_CONFIGURE_PROGRESS":
data1 = ffi_event.data1
if data1 == 0 or data1 == 1000:
success = data1 == 1000
yield "ac_configure_completed", dict(success=success)
elif name == "DC_EVENT_INCOMING_MSG":
msg = account.get_message_by_id(ffi_event.data2)
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
elif name == "DC_EVENT_MSGS_CHANGED":
if ffi_event.data2 != 0:
msg = account.get_message_by_id(ffi_event.data2)
if msg.is_outgoing():
res = map_system_message(msg)
if res and res[0].startswith("ac_member"):
yield res
yield "ac_outgoing_message", dict(message=msg)
elif msg.is_in_fresh():
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
elif name == "DC_EVENT_MSG_DELIVERED":
msg = account.get_message_by_id(ffi_event.data2)
yield "ac_message_delivered", dict(message=msg)
elif name == "DC_EVENT_CHAT_MODIFIED":
chat = account.get_chat_by_id(ffi_event.data1)
yield "ac_chat_modified", dict(chat=chat)

View File

@@ -1,25 +1,93 @@
""" Hooks for python bindings """
""" Hooks for Python bindings to Delta Chat Core Rust CFFI"""
import pluggy
name = "deltachat"
hookspec = pluggy.HookspecMarker(name)
hookimpl = pluggy.HookimplMarker(name)
_plugin_manager = None
account_spec_name = "deltachat-account"
account_hookspec = pluggy.HookspecMarker(account_spec_name)
account_hookimpl = pluggy.HookimplMarker(account_spec_name)
global_spec_name = "deltachat-global"
global_hookspec = pluggy.HookspecMarker(global_spec_name)
global_hookimpl = pluggy.HookimplMarker(global_spec_name)
def get_plugin_manager():
global _plugin_manager
if _plugin_manager is None:
_plugin_manager = pluggy.PluginManager(name)
_plugin_manager.add_hookspecs(DeltaChatHookSpecs)
return _plugin_manager
class PerAccount:
""" per-Account-instance hook specifications.
All hooks are executed in a dedicated Event thread.
Hooks are not allowed to block/last long as this
blocks overall event processing on the python side.
"""
@classmethod
def _make_plugin_manager(cls):
pm = pluggy.PluginManager(account_spec_name)
pm.add_hookspecs(cls)
return pm
@account_hookspec
def ac_process_ffi_event(self, ffi_event):
""" process a CFFI low level events for a given account.
ffi_event has "name", "data1", "data2" values as specified
with `DC_EVENT_* <https://c.delta.chat/group__DC__EVENT.html>`_.
DANGER: this hook is executed from the callback invoked by core.
Hook implementations need to be short running and can typically
not call back into core because this would easily cause recursion issues.
"""
@account_hookspec
def ac_log_line(self, message):
""" log a message related to the account. """
@account_hookspec
def ac_configure_completed(self, success):
""" Called when a configure process completed. """
@account_hookspec
def ac_incoming_message(self, message):
""" Called on any incoming message (to deaddrop or chat). """
@account_hookspec
def ac_outgoing_message(self, message):
""" Called on each outgoing message (both system and "normal")."""
@account_hookspec
def ac_message_delivered(self, message):
""" Called when an outgoing message has been delivered to SMTP. """
@account_hookspec
def ac_chat_modified(self, chat):
""" Chat was created or modified regarding membership, avatar, title. """
@account_hookspec
def ac_member_added(self, chat, contact, message):
""" Called for each contact added to an accepted chat. """
@account_hookspec
def ac_member_removed(self, chat, contact, message):
""" Called for each contact removed from a chat. """
class DeltaChatHookSpecs:
""" Plugin Hook specifications for Python bindings to Delta Chat CFFI. """
class Global:
""" global hook specifications using a per-process singleton
plugin manager instance.
@hookspec
def process_low_level_event(self, account, event_name, data1, data2):
""" process a CFFI low level events for a given account. """
"""
_plugin_manager = None
@classmethod
def _get_plugin_manager(cls):
if cls._plugin_manager is None:
cls._plugin_manager = pm = pluggy.PluginManager(global_spec_name)
pm.add_hookspecs(cls)
return cls._plugin_manager
@global_hookspec
def dc_account_init(self, account):
""" called when `Account::__init__()` function starts executing. """
@global_hookspec
def dc_account_after_shutdown(self, account):
""" Called after the account has been shutdown. """

View File

@@ -16,8 +16,7 @@ class Message(object):
"""
def __init__(self, account, dc_msg):
self.account = account
self._dc_context = account._dc_context
assert isinstance(self._dc_context, ffi.CData)
assert isinstance(self.account._dc_context, ffi.CData)
assert isinstance(dc_msg, ffi.CData)
assert dc_msg != ffi.NULL
self._dc_msg = dc_msg
@@ -28,7 +27,11 @@ class Message(object):
return self.account == other.account and self.id == other.id
def __repr__(self):
return "<Message id={} dc_context={}>".format(self.id, self._dc_context)
c = self.get_sender_contact()
typ = "outgoing" if self.is_outgoing() else "incoming"
return "<Message {} sys={} {} id={} sender={}/{} chat={}/{}>".format(
typ, self.is_system_message(), repr(self.text[:10]),
self.id, c.id, c.addr, self.chat.id, self.chat.get_name())
@classmethod
def from_db(cls, account, id):
@@ -50,6 +53,16 @@ class Message(object):
lib.dc_msg_unref
))
def accept_sender_contact(self):
""" ensure that the sender is an accepted contact
and that the message has a non-deaddrop chat object.
"""
self.account.create_chat_by_message(self)
self._dc_msg = ffi.gc(
lib.dc_get_msg(self.account._dc_context, self.id),
lib.dc_msg_unref
)
@props.with_doc
def text(self):
"""unicode text of this messages (might be empty if not a text message). """
@@ -81,6 +94,10 @@ class Message(object):
"""mime type of the file (if it exists)"""
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
def is_system_message(self):
""" return True if this message is a system/info message. """
return bool(lib.dc_msg_is_info(self._dc_msg))
def is_setup_message(self):
""" return True if this message is a setup message. """
return lib.dc_msg_is_setupmessage(self._dc_msg)
@@ -102,12 +119,12 @@ class Message(object):
The text is multiline and may contain eg. the raw text of the message.
"""
return from_dc_charpointer(lib.dc_get_msg_info(self._dc_context, self.id))
return from_dc_charpointer(lib.dc_get_msg_info(self.account._dc_context, self.id))
def continue_key_transfer(self, setup_code):
""" extract key and use it as primary key for this account. """
res = lib.dc_continue_key_transfer(
self._dc_context,
self.account._dc_context,
self.id,
as_dc_charpointer(setup_code)
)
@@ -142,7 +159,7 @@ class Message(object):
:returns: email-mime message object (with headers only, no body).
"""
import email.parser
mime_headers = lib.dc_get_mime_headers(self._dc_context, self.id)
mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id)
if mime_headers:
s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref))
if isinstance(s, bytes):
@@ -159,6 +176,13 @@ class Message(object):
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
return Chat(self.account, chat_id)
def get_sender_chat(self):
"""return the 1:1 chat with the sender of this message.
:returns: :class:`deltachat.chat.Chat` instance
"""
return self.get_sender_contact().get_chat()
def get_sender_contact(self):
"""return the contact of who wrote the message.
@@ -166,7 +190,7 @@ class Message(object):
"""
from .contact import Contact
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
return Contact(self._dc_context, contact_id)
return Contact(self.account, contact_id)
#
# Message State query methods
@@ -178,7 +202,7 @@ class Message(object):
else:
# load message from db to get a fresh/current state
dc_msg = ffi.gc(
lib.dc_get_msg(self._dc_context, self.id),
lib.dc_get_msg(self.account._dc_context, self.id),
lib.dc_msg_unref
)
return lib.dc_msg_get_state(dc_msg)
@@ -207,6 +231,13 @@ class Message(object):
"""
return self._msgstate == const.DC_STATE_IN_SEEN
def is_outgoing(self):
"""Return True if Message is outgoing. """
return self._msgstate in (
const.DC_STATE_OUT_PREPARING, const.DC_STATE_OUT_PENDING,
const.DC_STATE_OUT_FAILED, const.DC_STATE_OUT_MDN_RCVD,
const.DC_STATE_OUT_DELIVERED)
def is_out_preparing(self):
"""Return True if Message is outgoing, but its file is being prepared.
"""
@@ -269,6 +300,10 @@ class Message(object):
""" return True if it's a file message. """
return self._view_type == const.DC_MSG_FILE
def mark_seen(self):
""" mark this message as seen. """
self.account.mark_seen_messages([self.id])
# some code for handling DC_MSG_* view types
@@ -288,3 +323,29 @@ def get_viewtype_code_from_name(view_type_name):
return code
raise ValueError("message typecode not found for {!r}, "
"available {!r}".format(view_type_name, list(_view_type_mapping.values())))
#
# some helper code for turning system messages into hook events
#
def map_system_message(msg):
if msg.is_system_message():
res = parse_system_add_remove(msg.text)
if res:
contact = msg.account.get_contact_by_addr(res[1])
if contact:
d = dict(chat=msg.chat, contact=contact, message=msg)
return "ac_member_" + res[0], d
def parse_system_add_remove(text):
# Member Me (x@y) removed by a@b.
# Member x@y removed by a@b
text = text.lower()
parts = text.split()
if parts[0] == "member":
if parts[2] in ("removed", "added"):
return parts[2], parts[1]
if parts[3] in ("removed", "added"):
return parts[3], parts[2].strip("()")

View File

@@ -0,0 +1,448 @@
from __future__ import print_function
import os
import sys
import subprocess
import queue
import threading
import fnmatch
import time
import weakref
import tempfile
import pytest
import requests
from . import Account, const
from .capi import lib
from .events import FFIEventLogger, FFIEventTracker
from _pytest._code import Source
import deltachat
def pytest_addoption(parser):
parser.addoption(
"--liveconfig", action="store", default=None,
help="a file with >=2 lines where each line "
"contains NAME=VALUE config settings for one account"
)
parser.addoption(
"--ignored", action="store_true",
help="Also run tests marked with the ignored marker",
)
def pytest_configure(config):
config.addinivalue_line(
"markers", "ignored: Mark test as bing slow, skipped unless --ignored is used."
)
cfg = config.getoption('--liveconfig')
if not cfg:
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
if cfg:
config.option.liveconfig = cfg
# Make sure we don't get garbled output because threads keep running
# collect all ever created accounts in a weakref-set (so we don't
# keep objects unneccessarily alive) and enable/disable logging
# for each pytest test phase # (setup/call/teardown).
# Additionally make the acfactory use a logging/no-logging default.
class LoggingAspect:
def __init__(self):
self._accounts = weakref.WeakSet()
@deltachat.global_hookimpl
def dc_account_init(self, account):
self._accounts.add(account)
def disable_logging(self, item):
for acc in self._accounts:
acc.disable_logging()
acfactory = item.funcargs.get("acfactory")
if acfactory:
acfactory.set_logging_default(False)
def enable_logging(self, item):
for acc in self._accounts:
acc.enable_logging()
acfactory = item.funcargs.get("acfactory")
if acfactory:
acfactory.set_logging_default(True)
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_setup(self, item):
if item.get_closest_marker("ignored"):
if not item.config.getvalue("ignored"):
pytest.skip("use --ignored to run this test")
self.enable_logging(item)
yield
self.disable_logging(item)
@pytest.hookimpl(hookwrapper=True)
def pytest_pyfunc_call(self, pyfuncitem):
self.enable_logging(pyfuncitem)
yield
self.disable_logging(pyfuncitem)
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_teardown(self, item):
self.enable_logging(item)
yield
self.disable_logging(item)
la = LoggingAspect()
config.pluginmanager.register(la)
deltachat.register_global_plugin(la)
def pytest_report_header(config, startdir):
summary = []
t = tempfile.mktemp()
try:
ac = Account(t)
info = ac.get_info()
ac.shutdown()
finally:
os.remove(t)
summary.extend(['Deltachat core={} sqlite={} journal_mode={}'.format(
info['deltachat_core_version'],
info['sqlite_version'],
info['journal_mode'],
)])
cfg = config.option.liveconfig
if cfg:
if "?" in cfg:
url, token = cfg.split("?", 1)
summary.append('Liveconfig provider: {}?<token ommitted>'.format(url))
else:
summary.append('Liveconfig file: {}'.format(cfg))
return summary
class SessionLiveConfigFromFile:
def __init__(self, fn):
self.fn = fn
self.configlist = []
for line in open(fn):
if line.strip() and not line.strip().startswith('#'):
d = {}
for part in line.split():
name, value = part.split("=")
d[name] = value
self.configlist.append(d)
def get(self, index):
return self.configlist[index]
def exists(self):
return bool(self.configlist)
class SessionLiveConfigFromURL:
def __init__(self, url):
self.configlist = []
self.url = url
def get(self, index):
try:
return self.configlist[index]
except IndexError:
assert index == len(self.configlist), index
res = requests.post(self.url)
if res.status_code != 200:
pytest.skip("creating newtmpuser failed {!r}".format(res))
d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"])
self.configlist.append(config)
return config
def exists(self):
return bool(self.configlist)
@pytest.fixture(scope="session")
def session_liveconfig(request):
liveconfig_opt = request.config.option.liveconfig
if liveconfig_opt:
if liveconfig_opt.startswith("http"):
return SessionLiveConfigFromURL(liveconfig_opt)
else:
return SessionLiveConfigFromFile(liveconfig_opt)
@pytest.fixture
def data(request):
class Data:
def __init__(self):
# trying to find test data heuristically
# because we are run from a dev-setup with pytest direct,
# through tox, and then maybe also from deltachat-binding
# users like "deltabot".
self.paths = [os.path.normpath(x) for x in [
os.path.join(os.path.dirname(request.fspath.strpath), "data"),
os.path.join(os.path.dirname(__file__), "..", "..", "..", "test-data")
]]
def get_path(self, bn):
""" return path of file or None if it doesn't exist. """
for path in self.paths:
fn = os.path.join(path, *bn.split("/"))
if os.path.exists(fn):
return fn
print("WARNING: path does not exist: {!r}".format(fn))
def read_path(self, bn, mode="r"):
fn = self.get_path(bn)
if fn is not None:
with open(fn, mode) as f:
return f.read()
return Data()
@pytest.fixture
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
class AccountMaker:
def __init__(self):
self.live_count = 0
self.offline_count = 0
self._finalizers = []
self._accounts = []
self.init_time = time.time()
self._generated_keys = ["alice", "bob", "charlie",
"dom", "elena", "fiona"]
self.set_logging_default(False)
def finalize(self):
while self._finalizers:
fin = self._finalizers.pop()
fin()
while self._accounts:
acc = self._accounts.pop()
acc.shutdown()
acc.disable_logging()
def make_account(self, path, logid, quiet=False):
ac = Account(path, logging=self._logging)
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
ac.addr = ac.get_self_contact().addr
if not quiet:
ac.add_account_plugin(FFIEventLogger(ac, logid=logid))
self._accounts.append(ac)
return ac
def set_logging_default(self, logging):
self._logging = bool(logging)
def get_unconfigured_account(self):
self.offline_count += 1
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
ac._evtracker.init_time = self.init_time
ac._evtracker.set_timeout(2)
return ac
def _preconfigure_key(self, account, addr):
# Only set a key if we haven't used it yet for another account.
if self._generated_keys:
keyname = self._generated_keys.pop(0)
fname_pub = data.read_path("key/{name}-public.asc".format(name=keyname))
fname_sec = data.read_path("key/{name}-secret.asc".format(name=keyname))
if fname_pub and fname_sec:
account._preconfigure_keypair(addr, fname_pub, fname_sec)
return True
else:
print("WARN: could not use preconfigured keys for {!r}".format(addr))
def get_configured_offline_account(self):
ac = self.get_unconfigured_account()
# do a pseudo-configured account
addr = "addr{}@offline.org".format(self.offline_count)
ac.set_config("addr", addr)
self._preconfigure_key(ac, addr)
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
ac.set_config("mail_pw", "123")
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
lib.dc_set_config(ac._dc_context, b"configured", b"1")
return ac
def get_online_config(self, pre_generated_key=True, quiet=False):
if not session_liveconfig:
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig")
configdict = session_liveconfig.get(self.live_count)
self.live_count += 1
if "e2ee_enabled" not in configdict:
configdict["e2ee_enabled"] = "1"
# Enable strict certificate checks for online accounts
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count), quiet=quiet)
if pre_generated_key:
self._preconfigure_key(ac, configdict['addr'])
ac._evtracker.init_time = self.init_time
ac._evtracker.set_timeout(30)
return ac, dict(configdict)
def get_online_configuring_account(self, mvbox=False, sentbox=False, move=False,
pre_generated_key=True, quiet=False, config={}):
ac, configdict = self.get_online_config(
pre_generated_key=pre_generated_key, quiet=quiet)
configdict.update(config)
configdict["mvbox_watch"] = str(int(mvbox))
configdict["mvbox_move"] = str(int(move))
configdict["sentbox_watch"] = str(int(sentbox))
ac.update_config(configdict)
ac.configure()
return ac
def get_one_online_account(self, pre_generated_key=True, mvbox=False, move=False):
ac1 = self.get_online_configuring_account(
pre_generated_key=pre_generated_key, mvbox=mvbox, move=move)
ac1.wait_configure_finish()
ac1.start_io()
return ac1
def get_two_online_accounts(self, move=False, quiet=False):
ac1 = self.get_online_configuring_account(move=True, quiet=quiet)
ac2 = self.get_online_configuring_account(quiet=quiet)
ac1.wait_configure_finish()
ac1.start_io()
ac2.wait_configure_finish()
ac2.start_io()
return ac1, ac2
def get_many_online_accounts(self, num, move=True, quiet=True):
accounts = [self.get_online_configuring_account(move=move, quiet=quiet)
for i in range(num)]
for acc in accounts:
acc._configtracker.wait_finish()
acc.start_io()
return accounts
def clone_online_account(self, account, pre_generated_key=True):
self.live_count += 1
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
if pre_generated_key:
self._preconfigure_key(ac, account.get_config("addr"))
ac._evtracker.init_time = self.init_time
ac._evtracker.set_timeout(30)
ac.update_config(dict(
addr=account.get_config("addr"),
mail_pw=account.get_config("mail_pw"),
mvbox_watch=account.get_config("mvbox_watch"),
mvbox_move=account.get_config("mvbox_move"),
sentbox_watch=account.get_config("sentbox_watch"),
))
ac.configure()
return ac
def run_bot_process(self, module, ffi=True):
fn = module.__file__
bot_ac, bot_cfg = self.get_online_config()
args = [
sys.executable,
"-u",
fn,
"--email", bot_cfg["addr"],
"--password", bot_cfg["mail_pw"],
bot_ac.db_path,
]
if ffi:
args.insert(-1, "--show-ffi")
print("$", " ".join(args))
popen = subprocess.Popen(
args=args,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # combine stdout/stderr in one stream
bufsize=0, # line buffering
close_fds=True, # close all FDs other than 0/1/2
universal_newlines=True # give back text
)
bot = BotProcess(popen, bot_cfg)
self._finalizers.append(bot.kill)
return bot
am = AccountMaker()
request.addfinalizer(am.finalize)
return am
class BotProcess:
def __init__(self, popen, bot_cfg):
self.popen = popen
self.addr = bot_cfg["addr"]
# we read stdout as quickly as we can in a thread and make
# the (unicode) lines available for readers through a queue.
self.stdout_queue = queue.Queue()
self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread")
t.setDaemon(1)
t.start()
def _run_stdout_thread(self):
try:
while 1:
line = self.popen.stdout.readline()
if not line:
break
line = line.strip()
self.stdout_queue.put(line)
print("bot-stdout: ", line)
finally:
self.stdout_queue.put(None)
def kill(self):
self.popen.kill()
def wait(self, timeout=30):
self.popen.wait(timeout=timeout)
def fnmatch_lines(self, pattern_lines):
patterns = [x.strip() for x in Source(pattern_lines.rstrip()).lines if x.strip()]
for next_pattern in patterns:
print("+++FNMATCH:", next_pattern)
ignored = []
while 1:
line = self.stdout_queue.get(timeout=15)
if line is None:
if ignored:
print("BOT stdout terminated after these lines")
for line in ignored:
print(line)
raise IOError("BOT stdout-thread terminated")
if fnmatch.fnmatch(line, next_pattern):
print("+++MATCHED:", line)
break
else:
print("+++IGN:", line)
ignored.append(line)
@pytest.fixture
def tmp_db_path(tmpdir):
return tmpdir.join("test.db").strpath
@pytest.fixture
def lp():
class Printer:
def sec(self, msg):
print()
print("=" * 10, msg, "=" * 10)
def step(self, msg):
print("-" * 5, "step " + msg, "-" * 5)
return Printer()

View File

@@ -0,0 +1,85 @@
from queue import Queue
from threading import Event
from .hookspec import account_hookimpl
class ImexFailed(RuntimeError):
""" Exception for signalling that import/export operations failed."""
class ImexTracker:
def __init__(self):
self._imex_events = Queue()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
if ffi_event.name == "DC_EVENT_IMEX_PROGRESS":
self._imex_events.put(ffi_event.data1)
elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN":
self._imex_events.put(ffi_event.data2)
def wait_finish(self, progress_timeout=60):
""" Return list of written files, raise ValueError if ExportFailed. """
files_written = []
while True:
ev = self._imex_events.get(timeout=progress_timeout)
if isinstance(ev, str):
files_written.append(ev)
elif ev == 0:
raise ImexFailed("export failed, exp-files: {}".format(files_written))
elif ev == 1000:
return files_written
class ConfigureFailed(RuntimeError):
""" Exception for signalling that configuration failed."""
class ConfigureTracker:
ConfigureFailed = ConfigureFailed
def __init__(self):
self._configure_events = Queue()
self._smtp_finished = Event()
self._imap_finished = Event()
self._ffi_events = []
self._progress = Queue()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event):
self._ffi_events.append(ffi_event)
if ffi_event.name == "DC_EVENT_SMTP_CONNECTED":
self._smtp_finished.set()
elif ffi_event.name == "DC_EVENT_IMAP_CONNECTED":
self._imap_finished.set()
elif ffi_event.name == "DC_EVENT_CONFIGURE_PROGRESS":
self._progress.put(ffi_event.data1)
@account_hookimpl
def ac_configure_completed(self, success):
self._configure_events.put(success)
def wait_smtp_connected(self):
""" wait until smtp is configured. """
self._smtp_finished.wait()
def wait_imap_connected(self):
""" wait until smtp is configured. """
self._imap_finished.wait()
def wait_progress(self, data1=None):
while 1:
evdata = self._progress.get()
if data1 is None or evdata == data1:
break
def wait_finish(self):
""" wait until configure is completed.
Raise Exception if Configure failed
"""
if not self._configure_events.get():
content = "\n".join(map(str, self._ffi_events))
raise ConfigureFailed(content)

View File

@@ -10,4 +10,6 @@ if __name__ == "__main__":
for relpath in os.listdir(workspacedir):
if relpath.startswith("deltachat"):
p = os.path.join(workspacedir, relpath)
subprocess.check_call(["auditwheel", "repair", p, "-w", workspacedir])
subprocess.check_call(
["auditwheel", "repair", p, "-w", workspacedir,
"--plat", "manylinux2014_x86_64"])

View File

@@ -1,317 +0,0 @@
from __future__ import print_function
import os
import sys
import py
import pytest
import requests
import time
from deltachat import Account
from deltachat import const
from deltachat.capi import lib
from _pytest.monkeypatch import MonkeyPatch
import tempfile
def pytest_addoption(parser):
parser.addoption(
"--liveconfig", action="store", default=None,
help="a file with >=2 lines where each line "
"contains NAME=VALUE config settings for one account"
)
parser.addoption(
"--ignored", action="store_true",
help="Also run tests marked with the ignored marker",
)
def pytest_configure(config):
config.addinivalue_line(
"markers", "ignored: Mark test as bing slow, skipped unless --ignored is used."
)
cfg = config.getoption('--liveconfig')
if not cfg:
cfg = os.getenv('DCC_NEW_TMP_EMAIL')
if cfg:
config.option.liveconfig = cfg
def pytest_runtest_setup(item):
if (list(item.iter_markers(name="ignored"))
and not item.config.getoption("ignored")):
pytest.skip("Ignored tests not requested, use --ignored")
def pytest_report_header(config, startdir):
summary = []
t = tempfile.mktemp()
m = MonkeyPatch()
try:
m.setattr(sys.stdout, "write", lambda x: len(x))
ac = Account(t)
info = ac.get_info()
ac.shutdown()
finally:
m.undo()
os.remove(t)
summary.extend(['Deltachat core={} sqlite={}'.format(
info['deltachat_core_version'],
info['sqlite_version'],
)])
cfg = config.option.liveconfig
if cfg:
if "#" in cfg:
url, token = cfg.split("#", 1)
summary.append('Liveconfig provider: {}#<token ommitted>'.format(url))
else:
summary.append('Liveconfig file: {}'.format(cfg))
return summary
@pytest.fixture(scope="session")
def data():
class Data:
def __init__(self):
self.path = os.path.join(os.path.dirname(__file__), "data")
def get_path(self, bn):
fn = os.path.join(self.path, bn)
assert os.path.exists(fn)
return fn
return Data()
class SessionLiveConfigFromFile:
def __init__(self, fn):
self.fn = fn
self.configlist = []
for line in open(fn):
if line.strip() and not line.strip().startswith('#'):
d = {}
for part in line.split():
name, value = part.split("=")
d[name] = value
self.configlist.append(d)
def get(self, index):
return self.configlist[index]
def exists(self):
return bool(self.configlist)
class SessionLiveConfigFromURL:
def __init__(self, url):
self.configlist = []
self.url = url
def get(self, index):
try:
return self.configlist[index]
except IndexError:
assert index == len(self.configlist), index
res = requests.post(self.url)
if res.status_code != 200:
pytest.skip("creating newtmpuser failed {!r}".format(res))
d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"])
self.configlist.append(config)
return config
def exists(self):
return bool(self.configlist)
@pytest.fixture(scope="session")
def session_liveconfig(request):
liveconfig_opt = request.config.option.liveconfig
if liveconfig_opt:
if liveconfig_opt.startswith("http"):
return SessionLiveConfigFromURL(liveconfig_opt)
else:
return SessionLiveConfigFromFile(liveconfig_opt)
@pytest.fixture(scope='session')
def datadir():
"""The py.path.local object of the test-data/ directory."""
for path in reversed(py.path.local(__file__).parts()):
datadir = path.join('test-data')
if datadir.isdir():
return datadir
else:
pytest.skip('test-data directory not found')
@pytest.fixture
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, datadir):
class AccountMaker:
def __init__(self):
self.live_count = 0
self.offline_count = 0
self._finalizers = []
self.init_time = time.time()
self._generated_keys = ["alice", "bob", "charlie",
"dom", "elena", "fiona"]
def finalize(self):
while self._finalizers:
fin = self._finalizers.pop()
fin()
def make_account(self, path, logid):
ac = Account(path, logid=logid)
self._finalizers.append(ac.shutdown)
return ac
def get_unconfigured_account(self):
self.offline_count += 1
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(2)
return ac
def _preconfigure_key(self, account, addr):
# Only set a key if we haven't used it yet for another account.
if self._generated_keys:
keyname = self._generated_keys.pop(0)
fname_pub = "key/{name}-public.asc".format(name=keyname)
fname_sec = "key/{name}-secret.asc".format(name=keyname)
account._preconfigure_keypair(addr,
datadir.join(fname_pub).read(),
datadir.join(fname_sec).read())
def get_configured_offline_account(self):
ac = self.get_unconfigured_account()
# do a pseudo-configured account
addr = "addr{}@offline.org".format(self.offline_count)
ac.set_config("addr", addr)
self._preconfigure_key(ac, addr)
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
ac.set_config("mail_pw", "123")
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
lib.dc_set_config(ac._dc_context, b"configured", b"1")
return ac
def get_online_config(self, pre_generated_key=True):
if not session_liveconfig:
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig")
configdict = session_liveconfig.get(self.live_count)
self.live_count += 1
if "e2ee_enabled" not in configdict:
configdict["e2ee_enabled"] = "1"
# Enable strict certificate checks for online accounts
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
if pre_generated_key:
self._preconfigure_key(ac, configdict['addr'])
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(30)
return ac, dict(configdict)
def get_online_configuring_account(self, mvbox=False, sentbox=False,
pre_generated_key=True, config={}):
ac, configdict = self.get_online_config(
pre_generated_key=pre_generated_key)
configdict.update(config)
ac.configure(**configdict)
ac.start_threads(mvbox=mvbox, sentbox=sentbox)
return ac
def get_one_online_account(self, pre_generated_key=True):
ac1 = self.get_online_configuring_account(
pre_generated_key=pre_generated_key)
wait_successful_IMAP_SMTP_connection(ac1)
wait_configuration_progress(ac1, 1000)
return ac1
def get_two_online_accounts(self):
ac1 = self.get_online_configuring_account()
ac2 = self.get_online_configuring_account()
wait_successful_IMAP_SMTP_connection(ac1)
wait_configuration_progress(ac1, 1000)
wait_successful_IMAP_SMTP_connection(ac2)
wait_configuration_progress(ac2, 1000)
return ac1, ac2
def clone_online_account(self, account, pre_generated_key=True):
self.live_count += 1
tmpdb = tmpdir.join("livedb%d" % self.live_count)
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
if pre_generated_key:
self._preconfigure_key(ac, account.get_config("addr"))
ac._evlogger.init_time = self.init_time
ac._evlogger.set_timeout(30)
ac.configure(addr=account.get_config("addr"), mail_pw=account.get_config("mail_pw"))
ac.start_threads()
return ac
am = AccountMaker()
request.addfinalizer(am.finalize)
return am
@pytest.fixture
def tmp_db_path(tmpdir):
return tmpdir.join("test.db").strpath
@pytest.fixture
def lp():
class Printer:
def sec(self, msg):
print()
print("=" * 10, msg, "=" * 10)
def step(self, msg):
print("-" * 5, "step " + msg, "-" * 5)
return Printer()
def wait_configuration_progress(account, min_target, max_target=1001):
min_target = min(min_target, max_target)
while 1:
evt_name, data1, data2 = \
account._evlogger.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
if data1 >= min_target and data1 <= max_target:
print("** CONFIG PROGRESS {}".format(min_target), account)
break
def wait_securejoin_inviter_progress(account, target):
while 1:
evt_name, data1, data2 = \
account._evlogger.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
if data2 >= target:
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), account)
break
def wait_successful_IMAP_SMTP_connection(account):
imap_ok = smtp_ok = False
while not imap_ok or not smtp_ok:
evt_name, data1, data2 = \
account._evlogger.get_matching("DC_EVENT_(IMAP|SMTP)_CONNECTED")
if evt_name == "DC_EVENT_IMAP_CONNECTED":
imap_ok = True
print("** IMAP OK", account)
if evt_name == "DC_EVENT_SMTP_CONNECTED":
smtp_ok = True
print("** SMTP OK", account)
print("** IMAP and SMTP logins successful", account)
def wait_msgs_changed(account, chat_id, msg_id=None):
ev = account._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev[1] == chat_id
if msg_id is not None:
assert ev[2] == msg_id
return ev[2]

1
python/tests/data/key Symbolic link
View File

@@ -0,0 +1 @@
../../../test-data/key

View File

@@ -0,0 +1,134 @@
import time
import threading
import pytest
import os
from queue import Queue, Empty
import deltachat
def test_db_busy_error(acfactory, tmpdir):
starttime = time.time()
log_lock = threading.RLock()
def log(string):
with log_lock:
print("%3.2f %s" % (time.time() - starttime, string))
# make a number of accounts
accounts = acfactory.get_many_online_accounts(5, quiet=True)
log("created %s accounts" % len(accounts))
# put a bigfile into each account
for acc in accounts:
acc.bigfile = os.path.join(acc.get_blobdir(), "bigfile")
with open(acc.bigfile, "wb") as f:
f.write(b"01234567890"*1000_000)
log("created %s bigfiles" % len(accounts))
contact_addrs = [acc.get_self_contact().addr for acc in accounts]
chat = accounts[0].create_group_chat("stress-group")
for addr in contact_addrs[1:]:
chat.add_contact(chat.account.create_contact(addr))
# setup auto-responder bots which report back failures/actions
report_queue = Queue()
def report_func(replier, report_type, *report_args):
report_queue.put((replier, report_type, report_args))
# each replier receives all events and sends report events to receive_queue
repliers = []
for acc in accounts:
replier = AutoReplier(acc, log=log, num_send=1000, num_bigfiles=0, report_func=report_func)
acc.add_account_plugin(replier)
repliers.append(replier)
# kick off message sending
# after which repliers will reply to each other
chat.send_text("hello")
alive_count = len(accounts)
while alive_count > 0:
try:
replier, report_type, report_args = report_queue.get(timeout=10)
except Empty:
log("timeout waiting for next event")
pytest.fail("timeout exceeded")
if report_type == ReportType.exit:
replier.log("EXIT".format(alive_count))
elif report_type == ReportType.ffi_error:
replier.log("ERROR: {}".format(addr, report_args[0]))
elif report_type == ReportType.message_echo:
continue
else:
raise ValueError("{} unknown report type {}, args={}".format(
addr, report_type, report_args
))
alive_count -= 1
replier.log("shutting down")
replier.account.shutdown()
replier.log("shut down complete, remaining={}".format(alive_count))
class ReportType:
exit = "exit"
ffi_error = "ffi-error"
message_echo = "message-echo"
class AutoReplier:
def __init__(self, account, log, num_send, num_bigfiles, report_func):
self.account = account
self._log = log
self.report_func = report_func
self.num_send = num_send
self.num_bigfiles = num_bigfiles
self.current_sent = 0
self.addr = self.account.get_self_contact().addr
self._thread = threading.Thread(
name="Stats{}".format(self.account),
target=self.thread_stats
)
self._thread.setDaemon(True)
self._thread.start()
def log(self, message):
self._log("{} {}".format(self.addr, message))
def thread_stats(self):
# XXX later use, for now we just quit
return
while 1:
time.sleep(1.0)
break
@deltachat.account_hookimpl
def ac_incoming_message(self, message):
if self.current_sent >= self.num_send:
return
message.accept_sender_contact()
message.mark_seen()
self.log("incoming message: {}".format(message))
self.current_sent += 1
# we are still alive, let's send a reply
if self.num_bigfiles and self.current_sent % self.num_bigfiles == 0:
message.chat.send_text("send big file as reply to: {}".format(message.text))
msg = message.chat.send_file(self.account.bigfile)
else:
msg = message.chat.send_text("got message id {}, small text reply".format(message.id))
assert msg.text
self.log("message-sent: {}".format(msg))
self.report_func(self, ReportType.message_echo)
if self.current_sent >= self.num_send:
self.report_func(self, ReportType.exit)
return
@deltachat.account_hookimpl
def ac_process_ffi_event(self, ffi_event):
self.log(ffi_event)
if ffi_event.name == "DC_EVENT_ERROR":
self.report_func(self, ReportType.ffi_error, ffi_event)

File diff suppressed because it is too large Load Diff

View File

@@ -6,16 +6,36 @@ import shutil
import pytest
from filecmp import cmp
from conftest import wait_configuration_progress, wait_msgs_changed
from deltachat import const
def wait_msg_delivered(account, msg_list):
""" wait for one or more MSG_DELIVERED events to match msg_list contents. """
msg_list = list(msg_list)
while msg_list:
ev = account._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
msg_list.remove((ev.data1, ev.data2))
def wait_msgs_changed(account, msgs_list):
""" wait for one or more MSGS_CHANGED events to match msgs_list contents. """
account.log("waiting for msgs_list={}".format(msgs_list))
msgs_list = list(msgs_list)
while msgs_list:
ev = account._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
for i, (data1, data2) in enumerate(msgs_list):
if ev.data1 == data1:
if data2 is None or ev.data2 == data2:
del msgs_list[i]
break
else:
account.log("waiting mismatch data1={} data2={}".format(data1, data2))
return ev.data1, ev.data2
class TestOnlineInCreation:
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
ac1, ac2 = acfactory.get_two_online_accounts()
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
@@ -27,10 +47,7 @@ class TestOnlineInCreation:
chat.prepare_message_file(src.strpath)
def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
ac1, ac2 = acfactory.get_two_online_accounts()
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
@@ -45,15 +62,12 @@ class TestOnlineInCreation:
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
def test_forward_increation(self, acfactory, data, lp):
ac1 = acfactory.get_online_configuring_account()
ac2 = acfactory.get_online_configuring_account()
wait_configuration_progress(ac1, 1000)
wait_configuration_progress(ac2, 1000)
ac1, ac2 = acfactory.get_two_online_accounts()
c2 = ac1.create_contact(email=ac2.get_config("addr"))
chat = ac1.create_chat_by_contact(c2)
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
wait_msgs_changed(ac1, 0, 0) # why no chat id?
wait_msgs_changed(ac1, [(0, 0)]) # why no chat id?
lp.sec("create a message with a file in creation")
orig = data.get_path("d.png")
@@ -62,19 +76,16 @@ class TestOnlineInCreation:
fp.write("preparing")
prepared_original = chat.prepare_message_file(path)
assert prepared_original.is_out_preparing()
wait_msgs_changed(ac1, chat.id, prepared_original.id)
wait_msgs_changed(ac1, [(chat.id, prepared_original.id)])
lp.sec("forward the message while still in creation")
chat2 = ac1.create_group_chat("newgroup")
chat2.add_contact(c2)
wait_msgs_changed(ac1, 0, 0) # why not chat id?
wait_msgs_changed(ac1, [(0, 0)]) # why not chat id?
ac1.forward_messages([prepared_original], chat2)
# XXX there might be two EVENT_MSGS_CHANGED and only one of them
# is the one caused by forwarding
forwarded_id = wait_msgs_changed(ac1, chat2.id)
if forwarded_id == 0:
forwarded_id = wait_msgs_changed(ac1, chat2.id)
assert forwarded_id
_, forwarded_id = wait_msgs_changed(ac1, [(chat2.id, None)])
forwarded_msg = ac1.get_message_by_id(forwarded_id)
assert forwarded_msg.is_out_preparing()
@@ -83,30 +94,28 @@ class TestOnlineInCreation:
shutil.copyfile(orig, path)
chat.send_prepared(prepared_original)
assert prepared_original.is_out_pending() or prepared_original.is_out_delivered()
wait_msgs_changed(ac1, chat.id, prepared_original.id)
lp.sec("expect the forwarded message to be sent now too")
wait_msgs_changed(ac1, chat2.id, forwarded_id)
lp.sec("check that both forwarded and original message are proper.")
wait_msgs_changed(ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)])
fwd_msg = ac1.get_message_by_id(forwarded_id)
assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered()
lp.sec("wait for the messages to be delivered to SMTP")
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
assert ev[1] == chat.id
assert ev[2] == prepared_original.id
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
assert ev[1] == chat2.id
assert ev[2] == forwarded_id
lp.sec("wait for both messages to be delivered to SMTP")
wait_msg_delivered(ac1, [
(chat2.id, forwarded_id),
(chat.id, prepared_original.id)
])
lp.sec("wait1 for original or forwarded messages to arrive")
ev1 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev1[1] > const.DC_CHAT_ID_LAST_SPECIAL
received_original = ac2.get_message_by_id(ev1[2])
ev1 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev1.data1 > const.DC_CHAT_ID_LAST_SPECIAL
received_original = ac2.get_message_by_id(ev1.data2)
assert cmp(received_original.filename, orig, shallow=False)
lp.sec("wait2 for original or forwarded messages to arrive")
ev2 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev2[1] > const.DC_CHAT_ID_LAST_SPECIAL
assert ev2[1] != ev1[1]
received_copy = ac2.get_message_by_id(ev2[2])
ev2 = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
assert ev2.data1 > const.DC_CHAT_ID_LAST_SPECIAL
assert ev2.data1 != ev1.data1
received_copy = ac2.get_message_by_id(ev2.data2)
assert cmp(received_copy.filename, orig, shallow=False)

View File

@@ -1,63 +1,52 @@
from __future__ import print_function
from deltachat import capi, cutil, const, set_context_callback, clear_context_callback
from queue import Queue
from deltachat import capi, cutil, const
from deltachat import register_global_plugin
from deltachat.hookspec import global_hookimpl
from deltachat.capi import ffi
from deltachat.capi import lib
# from deltachat.account import EventLogger
def test_empty_context():
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL, capi.ffi.NULL)
capi.lib.dc_close(ctx)
capi.lib.dc_context_unref(ctx)
def test_callback_None2int():
ctx = capi.lib.dc_context_new(capi.lib.py_dc_callback, ffi.NULL, ffi.NULL)
set_context_callback(ctx, lambda *args: None)
capi.lib.dc_close(ctx)
clear_context_callback(ctx)
def test_dc_close_events(tmpdir, acfactory):
ac1 = acfactory.get_unconfigured_account()
# register after_shutdown function
shutdowns = Queue()
def test_dc_close_events(tmpdir):
from deltachat.account import Account
p = tmpdir.join("hello.db")
ac1 = Account(p.strpath)
class ShutdownPlugin:
@global_hookimpl
def dc_account_after_shutdown(self, account):
assert account._dc_context is None
shutdowns.put(account)
register_global_plugin(ShutdownPlugin())
assert hasattr(ac1, "_dc_context")
ac1.shutdown()
def find(info_string):
evlog = ac1._evlogger
while 1:
ev = evlog.get_matching("DC_EVENT_INFO", check_error=False)
data2 = ev[2]
if info_string in data2:
return
else:
print("skipping event", *ev)
find("disconnecting inbox-thread")
find("disconnecting sentbox-thread")
find("disconnecting mvbox-thread")
find("disconnecting SMTP")
find("Database closed")
shutdowns.get(timeout=2)
def test_wrong_db(tmpdir):
dc_context = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
p = tmpdir.join("hello.db")
# write an invalid database file
p.write("x123" * 10)
assert not lib.dc_open(dc_context, p.strpath.encode("ascii"), ffi.NULL)
assert ffi.NULL == lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
def test_empty_blobdir(tmpdir):
db_fname = tmpdir.join("hello.db")
# Apparently some client code expects this to be the same as passing NULL.
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), b""),
lib.dc_context_unref,
)
db_fname = tmpdir.join("hello.db")
assert lib.dc_open(ctx, db_fname.strpath.encode("ascii"), b"")
assert ctx != ffi.NULL
def test_event_defines():
@@ -66,24 +55,27 @@ def test_event_defines():
def test_sig():
sig = capi.lib.dc_get_event_signature_types
assert sig(const.DC_EVENT_INFO) == 2
assert sig(const.DC_EVENT_WARNING) == 2
assert sig(const.DC_EVENT_ERROR) == 2
assert sig(const.DC_EVENT_SMTP_CONNECTED) == 2
assert sig(const.DC_EVENT_IMAP_CONNECTED) == 2
assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT) == 2
sig = capi.lib.dc_event_has_string_data
assert not sig(const.DC_EVENT_MSGS_CHANGED)
assert sig(const.DC_EVENT_INFO)
assert sig(const.DC_EVENT_WARNING)
assert sig(const.DC_EVENT_ERROR)
assert sig(const.DC_EVENT_SMTP_CONNECTED)
assert sig(const.DC_EVENT_IMAP_CONNECTED)
assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT)
assert sig(const.DC_EVENT_IMEX_FILE_WRITTEN)
def test_markseen_invalid_message_ids(acfactory):
ac1 = acfactory.get_configured_offline_account()
contact1 = ac1.create_contact(email="some1@example.com", name="some1")
chat = ac1.create_chat_by_contact(contact1)
chat.send_text("one messae")
ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
ac1._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
msg_ids = [9]
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))
ac1._evlogger.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
ac1._evtracker.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
def test_get_special_message_id_returns_empty_message(acfactory):
@@ -95,47 +87,18 @@ def test_get_special_message_id_returns_empty_message(acfactory):
def test_provider_info_none():
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_new(ffi.NULL, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
def test_get_info_closed():
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
assert 'deltachat_core_version' in info
assert 'database_dir' not in info
def test_get_info_open(tmpdir):
db_fname = tmpdir.join("test.db")
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), ffi.NULL),
lib.dc_context_unref,
)
db_fname = tmpdir.join("test.db")
lib.dc_open(ctx, db_fname.strpath.encode("ascii"), ffi.NULL)
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
assert 'deltachat_core_version' in info
assert 'database_dir' in info
def test_is_open_closed():
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
assert lib.dc_is_open(ctx) == 0
def test_is_open_actually_open(tmpdir):
ctx = ffi.gc(
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
lib.dc_context_unref,
)
db_fname = tmpdir.join("test.db")
lib.dc_open(ctx, db_fname.strpath.encode("ascii"), ffi.NULL)
assert lib.dc_is_open(ctx) == 1

View File

@@ -7,13 +7,14 @@ envlist =
[testenv]
commands =
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs:tests}
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored {posargs: tests examples}
python tests/package_wheels.py {toxworkdir}/wheelhouse
passenv =
TRAVIS
DCC_RS_DEV
DCC_RS_TARGET
DCC_PY_LIVECONFIG
DCC_NEW_TMP_EMAIL
CARGO_TARGET_DIR
RUSTC_WRAPPER
deps =
@@ -40,13 +41,13 @@ deps =
restructuredtext_lint
commands =
flake8 src/deltachat
flake8 tests/
flake8 tests/ examples/
rst-lint --encoding 'utf-8' README.rst
[testenv:doc]
changedir=doc
deps =
sphinx==2.2.0
sphinx
breathe
commands =
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
@@ -66,7 +67,6 @@ commands =
[pytest]
addopts = -v -ra --strict-markers
python_files = tests/test_*.py
norecursedirs = .tox
xfail_strict=true
timeout = 90

View File

@@ -1 +1 @@
nightly-2019-11-06
1.43.1

View File

@@ -75,7 +75,7 @@ impl Aheader {
wanted_from: &str,
headers: &[mailparse::MailHeader<'_>],
) -> Option<Self> {
if let Ok(Some(value)) = headers.get_header_value(HeaderDef::Autocrypt) {
if let Some(value) = headers.get_header_value(HeaderDef::Autocrypt) {
match Self::from_str(&value) {
Ok(header) => {
if addr_cmp(&header.addr, wanted_from) {
@@ -127,14 +127,10 @@ impl str::FromStr for Aheader {
.split(';')
.filter_map(|a| {
let attribute: Vec<&str> = a.trim().splitn(2, '=').collect();
if attribute.len() < 2 {
return None;
match &attribute[..] {
[key, value] => Some((key.trim().to_string(), value.trim().to_string())),
_ => None,
}
Some((
attribute[0].trim().to_string(),
attribute[1].trim().to_string(),
))
})
.collect();

View File

@@ -2,17 +2,18 @@
use std::ffi::OsStr;
use std::fmt;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use self::image::GenericImageView;
use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use async_std::{fs, io};
use image::GenericImageView;
use thiserror::Error;
use crate::constants::AVATAR_SIZE;
use crate::context::Context;
use crate::events::Event;
extern crate image;
/// Represents a file in the blob directory.
///
/// The object has a name, which will always be valid UTF-8. Having a
@@ -43,31 +44,35 @@ impl<'a> BlobObject<'a> {
/// [BlobError::WriteFailure] is used when the file could not
/// be written to. You can expect [BlobError.cause] to contain an
/// underlying error.
pub fn create(
pub async fn create(
context: &'a Context,
suggested_name: impl AsRef<str>,
data: &[u8],
) -> 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)?;
let (name, mut file) = BlobObject::create_new_file(&blobdir, &stem, &ext).await?;
file.write_all(data)
.await
.map_err(|err| BlobError::WriteFailure {
blobdir: blobdir.to_path_buf(),
blobname: name.clone(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
let blob = BlobObject {
blobdir,
name: format!("$BLOBDIR/{}", name),
};
context.call_cb(Event::NewBlobFile(blob.as_name().to_string()));
context.emit_event(Event::NewBlobFile(blob.as_name().to_string()));
Ok(blob)
}
// Creates a new file, returning a tuple of the name and the handle.
fn create_new_file(dir: &Path, stem: &str, ext: &str) -> Result<(String, fs::File), BlobError> {
async fn create_new_file(
dir: &Path,
stem: &str,
ext: &str,
) -> Result<(String, fs::File), BlobError> {
let max_attempt = 15;
let mut name = format!("{}{}", stem, ext);
for attempt in 0..max_attempt {
@@ -76,6 +81,7 @@ impl<'a> BlobObject<'a> {
.create_new(true)
.write(true)
.open(&path)
.await
{
Ok(file) => return Ok((name, file)),
Err(err) => {
@@ -84,7 +90,6 @@ impl<'a> BlobObject<'a> {
blobdir: dir.to_path_buf(),
blobname: name,
cause: err,
backtrace: failure::Backtrace::new(),
});
} else {
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
@@ -97,7 +102,6 @@ impl<'a> BlobObject<'a> {
blobdir: dir.to_path_buf(),
blobname: name,
cause: std::io::Error::new(std::io::ErrorKind::Other, "supposedly unreachable"),
backtrace: failure::Backtrace::new(),
})
}
@@ -113,39 +117,41 @@ impl<'a> BlobObject<'a> {
/// In addition to the errors in [BlobObject::create] the
/// [BlobError::CopyFailure] is used when the data can not be
/// copied.
pub fn create_and_copy(
pub async fn create_and_copy(
context: &'a Context,
src: impl AsRef<Path>,
) -> std::result::Result<BlobObject<'a>, BlobError> {
let mut src_file = fs::File::open(src.as_ref()).map_err(|err| BlobError::CopyFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: String::from(""),
src: src.as_ref().to_path_buf(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
let mut src_file =
fs::File::open(src.as_ref())
.await
.map_err(|err| BlobError::CopyFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: String::from(""),
src: src.as_ref().to_path_buf(),
cause: err,
})?;
let (stem, ext) = BlobObject::sanitise_name(&src.as_ref().to_string_lossy());
let (name, mut dst_file) = BlobObject::create_new_file(context.get_blobdir(), &stem, &ext)?;
let (name, mut dst_file) =
BlobObject::create_new_file(context.get_blobdir(), &stem, &ext).await?;
let name_for_err = name.clone();
std::io::copy(&mut src_file, &mut dst_file).map_err(|err| {
if let Err(err) = io::copy(&mut src_file, &mut dst_file).await {
{
// Attempt to remove the failed file, swallow errors resulting from that.
let path = context.get_blobdir().join(&name_for_err);
fs::remove_file(path).ok();
fs::remove_file(path).await.ok();
}
BlobError::CopyFailure {
return Err(BlobError::CopyFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: name_for_err,
src: src.as_ref().to_path_buf(),
cause: err,
backtrace: failure::Backtrace::new(),
}
})?;
});
}
let blob = BlobObject {
blobdir: context.get_blobdir(),
name: format!("$BLOBDIR/{}", name),
};
context.call_cb(Event::NewBlobFile(blob.as_name().to_string()));
context.emit_event(Event::NewBlobFile(blob.as_name().to_string()));
Ok(blob)
}
@@ -163,14 +169,14 @@ impl<'a> BlobObject<'a> {
/// This merely delegates to the [BlobObject::create_and_copy] and
/// the [BlobObject::from_path] methods. See those for possible
/// errors.
pub fn new_from_path(
pub async fn new_from_path(
context: &Context,
src: impl AsRef<Path>,
) -> std::result::Result<BlobObject, BlobError> {
) -> std::result::Result<BlobObject<'_>, BlobError> {
if src.as_ref().starts_with(context.get_blobdir()) {
BlobObject::from_path(context, src)
} else {
BlobObject::create_and_copy(context, src)
BlobObject::create_and_copy(context, src).await
}
}
@@ -198,17 +204,14 @@ impl<'a> BlobObject<'a> {
.map_err(|_| BlobError::WrongBlobdir {
blobdir: context.get_blobdir().to_path_buf(),
src: path.as_ref().to_path_buf(),
backtrace: failure::Backtrace::new(),
})?;
if !BlobObject::is_acceptible_blob_name(&rel_path) {
return Err(BlobError::WrongName {
blobname: path.as_ref().to_path_buf(),
backtrace: failure::Backtrace::new(),
});
}
let name = rel_path.to_str().ok_or_else(|| BlobError::WrongName {
blobname: path.as_ref().to_path_buf(),
backtrace: failure::Backtrace::new(),
})?;
BlobObject::from_name(context, name.to_string())
}
@@ -236,7 +239,6 @@ impl<'a> BlobObject<'a> {
if !BlobObject::is_acceptible_blob_name(&name) {
return Err(BlobError::WrongName {
blobname: PathBuf::from(name),
backtrace: failure::Backtrace::new(),
});
}
Ok(BlobObject {
@@ -359,7 +361,6 @@ impl<'a> BlobObject<'a> {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
if img.width() <= AVATAR_SIZE && img.height() <= AVATAR_SIZE {
@@ -372,7 +373,6 @@ impl<'a> BlobObject<'a> {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
backtrace: failure::Backtrace::new(),
})?;
Ok(())
@@ -386,98 +386,41 @@ impl<'a> fmt::Display for BlobObject<'a> {
}
/// Errors for the [BlobObject].
#[derive(Fail, Debug)]
#[derive(Debug, Error)]
pub enum BlobError {
#[error("Failed to create blob {blobname} in {}", .blobdir.display())]
CreateFailure {
blobdir: PathBuf,
blobname: String,
#[cause]
#[source]
cause: std::io::Error,
backtrace: failure::Backtrace,
},
#[error("Failed to write data to blob {blobname} in {}", .blobdir.display())]
WriteFailure {
blobdir: PathBuf,
blobname: String,
#[cause]
#[source]
cause: std::io::Error,
backtrace: failure::Backtrace,
},
#[error("Failed to copy data from {} to blob {blobname} in {}", .src.display(), .blobdir.display())]
CopyFailure {
blobdir: PathBuf,
blobname: String,
src: PathBuf,
#[cause]
#[source]
cause: std::io::Error,
backtrace: failure::Backtrace,
},
#[error("Failed to recode to blob {blobname} in {}", .blobdir.display())]
RecodeFailure {
blobdir: PathBuf,
blobname: String,
#[cause]
#[source]
cause: image::ImageError,
backtrace: failure::Backtrace,
},
WrongBlobdir {
blobdir: PathBuf,
src: PathBuf,
backtrace: failure::Backtrace,
},
WrongName {
blobname: PathBuf,
backtrace: failure::Backtrace,
},
}
// Implementing Display is done by hand because the failure
// #[fail(display = "...")] syntax does not allow using
// `blobdir.display()`.
impl fmt::Display for BlobError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Match on the data rather than kind, they are equivalent for
// identifying purposes but contain the actual data we need.
match &self {
BlobError::CreateFailure {
blobdir, blobname, ..
} => write!(
f,
"Failed to create blob {} in {}",
blobname,
blobdir.display()
),
BlobError::WriteFailure {
blobdir, blobname, ..
} => write!(
f,
"Failed to write data to blob {} in {}",
blobname,
blobdir.display()
),
BlobError::CopyFailure {
blobdir,
blobname,
src,
..
} => write!(
f,
"Failed to copy data from {} to blob {} in {}",
src.display(),
blobname,
blobdir.display(),
),
BlobError::RecodeFailure {
blobdir, blobname, ..
} => write!(f, "Failed to recode {} in {}", blobname, blobdir.display(),),
BlobError::WrongBlobdir { blobdir, src, .. } => write!(
f,
"File path {} is not in blobdir {}",
src.display(),
blobdir.display(),
),
BlobError::WrongName { blobname, .. } => {
write!(f, "Blob has a bad name: {}", blobname.display(),)
}
}
}
#[error("File path {} is not in {}", .src.display(), .blobdir.display())]
WrongBlobdir { blobdir: PathBuf, src: PathBuf },
#[error("Blob has a badname {}", .blobname.display())]
WrongName { blobname: PathBuf },
}
#[cfg(test)]
@@ -486,58 +429,71 @@ mod tests {
use crate::test_utils::*;
#[test]
fn test_create() {
let t = dummy_context();
let blob = BlobObject::create(&t.ctx, "foo", b"hello").unwrap();
#[async_std::test]
async fn test_create() {
let t = dummy_context().await;
let blob = BlobObject::create(&t.ctx, "foo", b"hello").await.unwrap();
let fname = t.ctx.get_blobdir().join("foo");
let data = fs::read(fname).unwrap();
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"));
}
#[test]
fn test_lowercase_ext() {
let t = dummy_context();
let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello").unwrap();
#[async_std::test]
async fn test_lowercase_ext() {
let t = dummy_context().await;
let blob = BlobObject::create(&t.ctx, "foo.TXT", b"hello")
.await
.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/foo.txt");
}
#[test]
fn test_as_file_name() {
let t = dummy_context();
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
#[async_std::test]
async fn test_as_file_name() {
let t = dummy_context().await;
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
assert_eq!(blob.as_file_name(), "foo.txt");
}
#[test]
fn test_as_rel_path() {
let t = dummy_context();
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
#[async_std::test]
async fn test_as_rel_path() {
let t = dummy_context().await;
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
assert_eq!(blob.as_rel_path(), Path::new("foo.txt"));
}
#[test]
fn test_suffix() {
let t = dummy_context();
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
#[async_std::test]
async fn test_suffix() {
let t = dummy_context().await;
let blob = BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
assert_eq!(blob.suffix(), Some("txt"));
let blob = BlobObject::create(&t.ctx, "bar", b"world").unwrap();
let blob = BlobObject::create(&t.ctx, "bar", b"world").await.unwrap();
assert_eq!(blob.suffix(), None);
}
#[test]
fn test_create_dup() {
let t = dummy_context();
BlobObject::create(&t.ctx, "foo.txt", b"hello").unwrap();
#[async_std::test]
async fn test_create_dup() {
let t = dummy_context().await;
BlobObject::create(&t.ctx, "foo.txt", b"hello")
.await
.unwrap();
let foo_path = t.ctx.get_blobdir().join("foo.txt");
assert!(foo_path.exists());
BlobObject::create(&t.ctx, "foo.txt", b"world").unwrap();
for dirent in fs::read_dir(t.ctx.get_blobdir()).unwrap() {
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();
while let Some(dirent) = dir.next().await {
let fname = dirent.unwrap().file_name();
if fname == foo_path.file_name().unwrap() {
assert_eq!(fs::read(&foo_path).unwrap(), b"hello");
assert_eq!(fs::read(&foo_path).await.unwrap(), b"hello");
} else {
let name = fname.to_str().unwrap();
assert!(name.starts_with("foo"));
@@ -546,17 +502,22 @@ mod tests {
}
}
#[test]
fn test_double_ext_preserved() {
let t = dummy_context();
BlobObject::create(&t.ctx, "foo.tar.gz", b"hello").unwrap();
#[async_std::test]
async fn test_double_ext_preserved() {
let t = dummy_context().await;
BlobObject::create(&t.ctx, "foo.tar.gz", b"hello")
.await
.unwrap();
let foo_path = t.ctx.get_blobdir().join("foo.tar.gz");
assert!(foo_path.exists());
BlobObject::create(&t.ctx, "foo.tar.gz", b"world").unwrap();
for dirent in fs::read_dir(t.ctx.get_blobdir()).unwrap() {
assert!(foo_path.exists().await);
BlobObject::create(&t.ctx, "foo.tar.gz", b"world")
.await
.unwrap();
let mut dir = fs::read_dir(t.ctx.get_blobdir()).await.unwrap();
while let Some(dirent) = dir.next().await {
let fname = dirent.unwrap().file_name();
if fname == foo_path.file_name().unwrap() {
assert_eq!(fs::read(&foo_path).unwrap(), b"hello");
assert_eq!(fs::read(&foo_path).await.unwrap(), b"hello");
} else {
let name = fname.to_str().unwrap();
println!("{}", name);
@@ -566,55 +527,55 @@ mod tests {
}
}
#[test]
fn test_create_long_names() {
let t = dummy_context();
#[async_std::test]
async fn test_create_long_names() {
let t = dummy_context().await;
let s = "1".repeat(150);
let blob = BlobObject::create(&t.ctx, &s, b"data").unwrap();
let blob = BlobObject::create(&t.ctx, &s, b"data").await.unwrap();
let blobname = blob.as_name().split('/').last().unwrap();
assert!(blobname.len() < 128);
}
#[test]
fn test_create_and_copy() {
let t = dummy_context();
#[async_std::test]
async fn test_create_and_copy() {
let t = dummy_context().await;
let src = t.dir.path().join("src");
fs::write(&src, b"boo").unwrap();
let blob = BlobObject::create_and_copy(&t.ctx, &src).unwrap();
fs::write(&src, b"boo").await.unwrap();
let blob = BlobObject::create_and_copy(&t.ctx, &src).await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/src");
let data = fs::read(blob.to_abs_path()).unwrap();
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).is_err());
assert!(BlobObject::create_and_copy(&t.ctx, &whoops).await.is_err());
let whoops = t.ctx.get_blobdir().join("whoops");
assert!(!whoops.exists());
assert!(!whoops.exists().await);
}
#[test]
fn test_create_from_path() {
let t = dummy_context();
#[async_std::test]
async fn test_create_from_path() {
let t = dummy_context().await;
let src_ext = t.dir.path().join("external");
fs::write(&src_ext, b"boo").unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap();
fs::write(&src_ext, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/external");
let data = fs::read(blob.to_abs_path()).unwrap();
let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo");
let src_int = t.ctx.get_blobdir().join("internal");
fs::write(&src_int, b"boo").unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_int).unwrap();
fs::write(&src_int, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_int).await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
let data = fs::read(blob.to_abs_path()).unwrap();
let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo");
}
#[test]
fn test_create_from_name_long() {
let t = dummy_context();
#[async_std::test]
async fn test_create_from_name_long() {
let t = dummy_context().await;
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
fs::write(&src_ext, b"boo").unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).unwrap();
fs::write(&src_ext, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t.ctx, &src_ext).await.unwrap();
assert_eq!(
blob.as_name(),
"$BLOBDIR/autocrypt-setup-message-4137848473.html"

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
//! # Chat list module
use crate::chat;
use crate::chat::*;
use crate::constants::*;
use crate::contact::*;
use crate::context::*;
use crate::error::Result;
use crate::error::{bail, ensure, Result};
use crate::lot::Lot;
use crate::message::{Message, MessageState, MsgId};
use crate::stock::StockMessage;
@@ -73,6 +74,9 @@ impl Chatlist {
/// if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
/// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
/// chats
/// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
/// and hides the device-chat,
// typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
/// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added
/// to the list (may be used eg. for selecting chats on forwarding, the flag is
/// not needed when DC_GCL_ARCHIVED_ONLY is already set)
@@ -82,12 +86,23 @@ impl Chatlist {
/// are returned.
/// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID
/// are returned.
pub fn try_load(
pub async fn try_load(
context: &Context,
listflags: usize,
query: Option<&str>,
query_contact_id: Option<u32>,
) -> Result<Self> {
let flag_archived_only = 0 != listflags & DC_GCL_ARCHIVED_ONLY;
let flag_for_forwarding = 0 != listflags & DC_GCL_FOR_FORWARDING;
let flag_no_specials = 0 != listflags & DC_GCL_NO_SPECIALS;
let flag_add_alldone_hint = 0 != listflags & DC_GCL_ADD_ALLDONE_HINT;
// Note that we do not emit DC_EVENT_MSGS_MODIFIED here even if some
// messages get deleted to avoid reloading the same chatlist.
if let Err(err) = delete_device_expired_messages(context).await {
warn!(context, "Failed to hide expired messages: {}", err);
}
let mut add_archived_link_item = false;
let process_row = |row: &rusqlite::Row| {
@@ -101,6 +116,15 @@ impl Chatlist {
.map_err(Into::into)
};
let skip_id = if flag_for_forwarding {
chat::lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE)
.await
.unwrap_or_default()
.0
} else {
ChatId::new(0)
};
// select with left join and minimum:
//
// - the inner select must use `hidden` and _not_ `m.hidden`
@@ -133,14 +157,19 @@ impl Chatlist {
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;",
params![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned],
process_row,
process_rows,
)?
} else if 0 != listflags & DC_GCL_ARCHIVED_ONLY {
).await?
} else if flag_archived_only {
// show archived chats
context.sql.query_map(
"SELECT c.id, m.id
// (this includes the archived device-chat; we could skip it,
// however, then the number of archived chats do not match, which might be even more irritating.
// and adapting the number requires larger refactorings and seems not to be worth the effort)
context
.sql
.query_map(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
@@ -154,23 +183,26 @@ impl Chatlist {
AND c.archived=1
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft],
process_row,
process_rows,
)?
paramsv![MessageState::OutDraft],
process_row,
process_rows,
)
.await?
} else if let Some(query) = query {
let query = query.trim().to_string();
ensure!(!query.is_empty(), "missing query");
// allow searching over special names that may change at any time
// when the ui calls set_stock_translation()
if let Err(err) = update_special_chat_names(context) {
if let Err(err) = update_special_chat_names(context).await {
warn!(context, "cannot update special chat names: {:?}", err)
}
let str_like_cmd = format!("%{}%", query);
context.sql.query_map(
"SELECT c.id, m.id
context
.sql
.query_map(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
@@ -178,18 +210,27 @@ impl Chatlist {
SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?))
WHERE c.id>9
AND (hidden=0 OR state=?1))
WHERE c.id>9 AND c.id!=?2
AND c.blocked=0
AND c.name LIKE ?
AND c.name LIKE ?3
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, str_like_cmd],
process_row,
process_rows,
)?
paramsv![MessageState::OutDraft, skip_id, str_like_cmd],
process_row,
process_rows,
)
.await?
} else {
// show normal chatlist
let sort_id_up = if flag_for_forwarding {
chat::lookup_by_contact_id(context, DC_CONTACT_ID_SELF)
.await
.unwrap_or_default()
.0
} else {
ChatId::new(0)
};
let mut ids = context.sql.query_map(
"SELECT c.id, m.id
FROM chats c
@@ -200,29 +241,32 @@ impl Chatlist {
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?1))
WHERE c.id>9
WHERE c.id>9 AND c.id!=?2
AND c.blocked=0
AND NOT c.archived=?2
AND NOT c.archived=?3
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
params![MessageState::OutDraft, ChatVisibility::Archived, ChatVisibility::Pinned],
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,
)?;
if 0 == listflags & DC_GCL_NO_SPECIALS {
if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context) {
ids.insert(
0,
(ChatId::new(DC_CHAT_ID_DEADDROP), last_deaddrop_fresh_msg_id),
);
).await?;
if !flag_no_specials {
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),
);
}
}
add_archived_link_item = true;
}
ids
};
if add_archived_link_item && dc_get_archived_cnt(context) > 0 {
if ids.is_empty() && 0 != listflags & DC_GCL_ADD_ALLDONE_HINT {
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((ChatId::new(DC_CHAT_ID_ARCHIVED_LINK), MsgId::new(0)));
@@ -245,18 +289,20 @@ impl Chatlist {
///
/// To get the message object from the message ID, use dc_get_chat().
pub fn get_chat_id(&self, index: usize) -> ChatId {
if index >= self.ids.len() {
return ChatId::new(0);
match self.ids.get(index) {
Some((chat_id, _msg_id)) => *chat_id,
None => ChatId::new(0),
}
self.ids[index].0
}
/// Get a single message ID of a chatlist.
///
/// To get the message object from the message ID, use dc_get_msg().
pub fn get_msg_id(&self, index: usize) -> Result<MsgId> {
ensure!(index < self.ids.len(), "Chatlist index out of range");
Ok(self.ids[index].1)
match self.ids.get(index) {
Some((_chat_id, msg_id)) => Ok(*msg_id),
None => bail!("Chatlist index out of range"),
}
}
/// Get a summary for a chatlist index.
@@ -273,36 +319,38 @@ impl Chatlist {
/// - dc_lot_t::timestamp: the timestamp of the message. 0 if not applicable.
/// - dc_lot_t::state: The state of the message as one of the DC_STATE_* constants (see #dc_msg_get_state()).
// 0 if not applicable.
pub fn get_summary(&self, context: &Context, index: usize, chat: Option<&Chat>) -> Lot {
pub async fn get_summary(&self, context: &Context, index: usize, chat: Option<&Chat>) -> Lot {
// The summary is created by the chat, not by the last message.
// This is because we may want to display drafts here or stuff as
// "is typing".
// Also, sth. as "No messages" would not work if the summary comes from a message.
let mut ret = Lot::new();
if index >= self.ids.len() {
ret.text2 = Some("ErrBadChatlistIndex".to_string());
return ret;
}
let (chat_id, lastmsg_id) = match self.ids.get(index) {
Some(ids) => ids,
None => {
ret.text2 = Some("ErrBadChatlistIndex".to_string());
return ret;
}
};
let chat_loaded: Chat;
let chat = if let Some(chat) = chat {
chat
} else if let Ok(chat) = Chat::load_from_db(context, self.ids[index].0) {
} else if let Ok(chat) = Chat::load_from_db(context, *chat_id).await {
chat_loaded = chat;
&chat_loaded
} else {
return ret;
};
let lastmsg_id = self.ids[index].1;
let mut lastcontact = None;
let lastmsg = if let Ok(lastmsg) = Message::load_from_db(context, lastmsg_id) {
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).ok();
lastcontact = Contact::load_from_db(context, lastmsg.from_id).await.ok();
}
Some(lastmsg)
@@ -314,9 +362,15 @@ impl Chatlist {
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).to_string());
ret.text2 = Some(
context
.stock_str(StockMessage::NoMessages)
.await
.to_string(),
);
} else {
ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context);
ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context)
.await;
}
ret
@@ -328,34 +382,38 @@ impl Chatlist {
}
/// Returns the number of archived chats
pub fn dc_get_archived_cnt(context: &Context) -> u32 {
pub async fn dc_get_archived_cnt(context: &Context) -> u32 {
context
.sql
.query_get_value(
context,
"SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;",
params![],
paramsv![],
)
.await
.unwrap_or_default()
}
fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
async fn get_last_deaddrop_fresh_msg(context: &Context) -> Option<MsgId> {
// We have an index over the state-column, this should be
// sufficient as there are typically only few fresh messages.
context.sql.query_get_value(
context,
concat!(
"SELECT m.id",
" FROM msgs m",
" LEFT JOIN chats c",
" ON c.id=m.chat_id",
" WHERE m.state=10",
" AND m.hidden=0",
" AND c.blocked=2",
" ORDER BY m.timestamp DESC, m.id DESC;"
),
params![],
)
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
}
#[cfg(test)]
@@ -364,15 +422,21 @@ mod tests {
use crate::test_utils::*;
#[test]
fn test_try_load() {
let t = dummy_context();
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap();
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat").unwrap();
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat").unwrap();
#[async_std::test]
async fn test_try_load() {
let t = dummy_context().await;
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();
let chat_id2 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "b chat")
.await
.unwrap();
let chat_id3 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "c chat")
.await
.unwrap();
// check that the chatlist starts with the most recent message
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
let chats = Chatlist::try_load(&t.ctx, 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);
@@ -381,44 +445,102 @@ 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));
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
chat_id2.set_draft(&t.ctx, Some(&mut msg)).await;
let chats = Chatlist::try_load(&t.ctx, 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).unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("b"), None)
.await
.unwrap();
assert_eq!(chats.len(), 1);
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None)
.await
.unwrap();
assert_eq!(chats.len(), 0);
chat_id1
.set_visibility(&t.ctx, ChatVisibility::Archived)
.await
.ok();
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None).unwrap();
let chats = Chatlist::try_load(&t.ctx, DC_GCL_ARCHIVED_ONLY, None, None)
.await
.unwrap();
assert_eq!(chats.len(), 1);
}
#[test]
fn test_search_special_chat_names() {
let t = dummy_context();
t.ctx.update_device_chats().unwrap();
#[async_std::test]
async fn test_sort_self_talk_up_on_forward() {
let t = dummy_context().await;
t.ctx.update_device_chats().await.unwrap();
create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat")
.await
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None).unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert!(chats.len() == 3);
assert!(!Chat::load_from_db(&t.ctx, chats.get_chat_id(0))
.await
.unwrap()
.is_self_talk());
let chats = Chatlist::try_load(&t.ctx, 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))
.await
.unwrap()
.is_self_talk());
}
#[async_std::test]
async fn test_search_special_chat_names() {
let t = dummy_context().await;
t.ctx.update_device_chats().await.unwrap();
let chats = Chatlist::try_load(&t.ctx, 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).unwrap();
let chats = Chatlist::try_load(&t.ctx, 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())
.await
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None)
.await
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-1234-s"), None).unwrap();
assert_eq!(chats.len(), 1);
t.ctx
.set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string())
.await
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None)
.await
.unwrap();
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None).unwrap();
assert_eq!(chats.len(), 1);
}
#[async_std::test]
async fn test_get_summary_unwrap() {
let t = dummy_context().await;
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "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;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
let summary = chats.get_summary(&t.ctx, 0, None).await;
assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary
}
}

View File

@@ -4,13 +4,14 @@ use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
use crate::blob::BlobObject;
use crate::chat::ChatId;
use crate::constants::DC_VERSION_STR;
use crate::context::Context;
use crate::dc_tools::*;
use crate::job::*;
use crate::events::Event;
use crate::message::MsgId;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::stock::StockMessage;
use rusqlite::NO_PARAMS;
/// The available configuration keys.
#[derive(
@@ -62,9 +63,31 @@ pub enum Config {
#[strum(props(default = "0"))] // also change ShowEmails.default() on changes
ShowEmails,
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
MediaQuality,
#[strum(props(default = "0"))]
KeyGenType,
/// Timer in seconds after which the message is deleted from the
/// server.
///
/// Equals to 0 by default, which means the message is never
/// deleted.
///
/// Value 1 is treated as "delete at once": messages are deleted
/// immediately, without moving to DeltaChat folder.
#[strum(props(default = "0"))]
DeleteServerAfter,
/// Timer in seconds after which the message is deleted from the
/// device.
///
/// Equals to 0 by default, which means the message is never
/// deleted.
#[strum(props(default = "0"))]
DeleteDeviceAfter,
SaveMimeHeaders,
ConfiguredAddr,
ConfiguredMailServer,
@@ -95,16 +118,16 @@ pub enum Config {
impl Context {
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
pub fn get_config(&self, key: Config) -> Option<String> {
pub async fn get_config(&self, key: Config) -> Option<String> {
let value = match key {
Config::Selfavatar => {
let rel_path = self.sql.get_raw_config(self, key);
let rel_path = self.sql.get_raw_config(self, 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),
_ => self.sql.get_raw_config(self, key).await,
};
if value.is_some() {
@@ -113,65 +136,102 @@ impl Context {
// Default values
match key {
Config::Selfstatus => Some(self.stock_str(StockMessage::StatusLine).into_owned()),
Config::Selfstatus => Some(self.stock_str(StockMessage::StatusLine).await.into_owned()),
_ => key.get_str("default").map(|s| s.to_string()),
}
}
pub fn get_config_int(&self, key: Config) -> i32 {
pub async fn get_config_int(&self, key: Config) -> i32 {
self.get_config(key)
.await
.and_then(|s| s.parse().ok())
.unwrap_or_default()
}
pub fn get_config_bool(&self, key: Config) -> bool {
self.get_config_int(key) != 0
pub async fn get_config_bool(&self, key: Config) -> bool {
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),
}
}
/// 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),
}
}
/// 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 fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
pub async fn set_config(&self, key: Config, value: Option<&str>) -> crate::sql::Result<()> {
match key {
Config::Selfavatar => {
self.sql
.execute("UPDATE contacts SET selfavatar_sent=0;", NO_PARAMS)?;
.execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![])
.await?;
self.sql
.set_raw_config_bool(self, "attach_selfavatar", true)?;
.set_raw_config_bool(self, "attach_selfavatar", true)
.await?;
match value {
Some(value) => {
let blob = BlobObject::new_from_path(&self, 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()))
self.sql
.set_raw_config(self, key, Some(blob.as_name()))
.await
}
None => self.sql.set_raw_config(self, key, None),
None => self.sql.set_raw_config(self, key, None).await,
}
}
Config::InboxWatch => {
let ret = self.sql.set_raw_config(self, key, value);
interrupt_inbox_idle(self);
let ret = self.sql.set_raw_config(self, key, value).await;
self.interrupt_inbox().await;
ret
}
Config::SentboxWatch => {
let ret = self.sql.set_raw_config(self, key, value);
interrupt_sentbox_idle(self);
let ret = self.sql.set_raw_config(self, key, value).await;
self.interrupt_sentbox().await;
ret
}
Config::MvboxWatch => {
let ret = self.sql.set_raw_config(self, key, value);
interrupt_mvbox_idle(self);
let ret = self.sql.set_raw_config(self, key, value).await;
self.interrupt_mvbox().await;
ret
}
Config::Selfstatus => {
let def = self.stock_str(StockMessage::StatusLine);
let def = self.stock_str(StockMessage::StatusLine).await;
let val = if value.is_none() || value.unwrap() == def {
None
} else {
value
};
self.sql.set_raw_config(self, key, val)
self.sql.set_raw_config(self, key, val).await
}
_ => self.sql.set_raw_config(self, key, value),
Config::DeleteDeviceAfter => {
let ret = self.sql.set_raw_config(self, key, value).await;
// Force chatlist reload to delete old messages immediately.
self.emit_event(Event::MsgsChanged {
msg_id: MsgId::new(0),
chat_id: ChatId::new(0),
});
ret
}
_ => self.sql.set_raw_config(self, key, value).await,
}
}
}
@@ -194,9 +254,11 @@ mod tests {
use std::str::FromStr;
use std::string::ToString;
use crate::constants;
use crate::constants::AVATAR_SIZE;
use crate::test_utils::*;
use image::GenericImageView;
use num_traits::FromPrimitive;
use std::fs::File;
use std::io::Write;
@@ -217,9 +279,9 @@ mod tests {
assert_eq!(Config::ImapFolder.get_str("default"), Some("INBOX"));
}
#[test]
fn test_selfavatar_outside_blobdir() {
let t = dummy_context();
#[async_std::test]
async fn test_selfavatar_outside_blobdir() {
let t = dummy_context().await;
let avatar_src = t.dir.path().join("avatar.jpg");
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
File::create(&avatar_src)
@@ -227,13 +289,14 @@ mod tests {
.write_all(avatar_bytes)
.unwrap();
let avatar_blob = t.ctx.get_blobdir().join("avatar.jpg");
assert!(!avatar_blob.exists());
assert!(!avatar_blob.exists().await);
t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
.await
.unwrap();
assert!(avatar_blob.exists());
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);
let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await;
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
let img = image::open(avatar_src).unwrap();
@@ -245,9 +308,9 @@ mod tests {
assert_eq!(img.height(), AVATAR_SIZE);
}
#[test]
fn test_selfavatar_in_blobdir() {
let t = dummy_context();
#[async_std::test]
async fn test_selfavatar_in_blobdir() {
let t = dummy_context().await;
let avatar_src = t.ctx.get_blobdir().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar900x900.png");
File::create(&avatar_src)
@@ -261,12 +324,57 @@ mod tests {
t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
.await
.unwrap();
let avatar_cfg = t.ctx.get_config(Config::Selfavatar);
let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await;
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);
}
#[async_std::test]
async fn test_selfavatar_copy_without_recode() {
let t = dummy_context().await;
let avatar_src = t.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
File::create(&avatar_src)
.unwrap()
.write_all(avatar_bytes)
.unwrap();
let avatar_blob = t.ctx.get_blobdir().join("avatar.png");
assert!(!avatar_blob.exists().await);
t.ctx
.set_config(Config::Selfavatar, Some(&avatar_src.to_str().unwrap()))
.await
.unwrap();
assert!(avatar_blob.exists().await);
assert_eq!(
std::fs::metadata(&avatar_blob).unwrap().len(),
avatar_bytes.len() as u64
);
let avatar_cfg = t.ctx.get_config(Config::Selfavatar).await;
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 = dummy_context().await;
let media_quality = t.ctx.get_config_int(Config::MediaQuality).await;
assert_eq!(media_quality, 0);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
assert_eq!(media_quality, constants::MediaQuality::Balanced);
t.ctx
.set_config(Config::MediaQuality, Some("1"))
.await
.unwrap();
let media_quality = t.ctx.get_config_int(Config::MediaQuality).await;
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();
assert_eq!(media_quality, constants::MediaQuality::Worse);
}
}

View File

@@ -1,7 +1,6 @@
//! # Thunderbird's Autoconfiguration implementation
//!
//! Documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration */
use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::constants::*;
@@ -9,33 +8,7 @@ use crate::context::Context;
use crate::login_param::LoginParam;
use super::read_url::read_url;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "Invalid email address: {:?}", _0)]
InvalidEmailAddress(String),
#[fail(display = "XML error at position {}", position)]
InvalidXml {
position: usize,
#[cause]
error: quick_xml::Error,
},
#[fail(display = "Bad or incomplete autoconfig")]
IncompleteAutoconfig(LoginParam),
#[fail(display = "Failed to get URL {}", _0)]
ReadUrlError(#[cause] super::read_url::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
impl From<super::read_url::Error> for Error {
fn from(err: super::read_url::Error) -> Error {
Error::ReadUrlError(err)
}
}
use super::Error;
#[derive(Debug)]
struct MozAutoconfigure<'a> {
@@ -65,16 +38,16 @@ enum MozConfigTag {
Username,
}
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam> {
fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam, Error> {
let mut reader = quick_xml::Reader::from_str(xml_raw);
reader.trim_text(true);
// Split address into local part and domain part.
let p = in_emailaddr
.find('@')
.ok_or_else(|| Error::InvalidEmailAddress(in_emailaddr.to_string()))?;
let (in_emaillocalpart, in_emaildomain) = in_emailaddr.split_at(p);
let in_emaildomain = &in_emaildomain[1..];
let parts: Vec<&str> = in_emailaddr.rsplitn(2, '@').collect();
let (in_emaillocalpart, in_emaildomain) = match &parts[..] {
[domain, local] => (local, domain),
_ => return Err(Error::InvalidEmailAddress(in_emailaddr.to_string())),
};
let mut moz_ac = MozAutoconfigure {
in_emailaddr,
@@ -121,12 +94,12 @@ fn parse_xml(in_emailaddr: &str, xml_raw: &str) -> Result<LoginParam> {
}
}
pub fn moz_autoconfigure(
pub async fn moz_autoconfigure(
context: &Context,
url: &str,
param_in: &LoginParam,
) -> Result<LoginParam> {
let xml_raw = read_url(context, url)?;
) -> Result<LoginParam, Error> {
let xml_raw = read_url(context, url).await?;
let res = parse_xml(&param_in.addr, &xml_raw);
if let Err(err) = &res {

View File

@@ -1,6 +1,5 @@
//! Outlook's Autodiscover
use quick_xml;
use quick_xml::events::BytesEnd;
use crate::constants::*;
@@ -8,33 +7,7 @@ use crate::context::Context;
use crate::login_param::LoginParam;
use super::read_url::read_url;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "XML error at position {}", position)]
InvalidXml {
position: usize,
#[cause]
error: quick_xml::Error,
},
#[fail(display = "Bad or incomplete autoconfig")]
IncompleteAutoconfig(LoginParam),
#[fail(display = "Failed to get URL {}", _0)]
ReadUrlError(#[cause] super::read_url::Error),
#[fail(display = "Number of redirection is exceeded")]
RedirectionError,
}
pub type Result<T> = std::result::Result<T, Error>;
impl From<super::read_url::Error> for Error {
fn from(err: super::read_url::Error) -> Error {
Error::ReadUrlError(err)
}
}
use super::Error;
struct OutlookAutodiscover {
pub out: LoginParam,
@@ -52,7 +25,7 @@ enum ParsingResult {
RedirectUrl(String),
}
fn parse_xml(xml_raw: &str) -> Result<ParsingResult> {
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
let mut outlk_ad = OutlookAutodiscover {
out: LoginParam::new(),
out_imap_set: false,
@@ -139,15 +112,15 @@ fn parse_xml(xml_raw: &str) -> Result<ParsingResult> {
Ok(res)
}
pub fn outlk_autodiscover(
pub async fn outlk_autodiscover(
context: &Context,
url: &str,
_param_in: &LoginParam,
) -> Result<LoginParam> {
) -> Result<LoginParam, 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)?;
let xml_raw = read_url(context, &url).await?;
let res = parse_xml(&xml_raw);
if let Err(err) = &res {
warn!(context, "{}", err);

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,15 @@
use crate::context::Context;
#[derive(Debug, Fail)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[fail(display = "URL request error")]
GetError(#[cause] reqwest::Error),
#[error("URL request error")]
GetError(surf::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
pub fn read_url(context: &Context, url: &str) -> Result<String> {
pub async fn read_url(context: &Context, url: &str) -> Result<String, Error> {
info!(context, "Requesting URL {}", url);
match reqwest::blocking::Client::new()
.get(url)
.send()
.and_then(|res| res.text())
{
match surf::get(url).recv_string().await {
Ok(res) => Ok(res),
Err(err) => {
info!(context, "Can\'t read URL {}", url);

View File

@@ -57,6 +57,19 @@ impl Default for ShowEmails {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(u8)]
pub enum MediaQuality {
Balanced = 0,
Worse = 1,
}
impl Default for MediaQuality {
fn default() -> Self {
MediaQuality::Balanced // also change Config.MediaQuality props(default) on changes
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(u8)]
pub enum KeyGenType {
@@ -80,6 +93,7 @@ pub(crate) const DC_FROM_HANDSHAKE: i32 = 0x01;
pub const DC_GCL_ARCHIVED_ONLY: usize = 0x01;
pub const DC_GCL_NO_SPECIALS: usize = 0x02;
pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
pub const DC_GCL_FOR_FORWARDING: usize = 0x08;
pub const DC_GCM_ADDDAYMARKER: u32 = 0x01;
@@ -196,13 +210,13 @@ pub const DC_LP_SMTP_SOCKET_SSL: usize = 0x20000;
pub const DC_LP_SMTP_SOCKET_PLAIN: usize = 0x40000;
/// 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);
pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
/// if none of these flags are set, the default is chosen
pub const DC_LP_IMAP_SOCKET_FLAGS: i32 =
(DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_SSL | DC_LP_IMAP_SOCKET_PLAIN);
DC_LP_IMAP_SOCKET_STARTTLS | DC_LP_IMAP_SOCKET_SSL | DC_LP_IMAP_SOCKET_PLAIN;
/// if none of these flags are set, the default is chosen
pub const DC_LP_SMTP_SOCKET_FLAGS: usize =
(DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN);
DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN;
// QR code scanning (view from Bob, the joiner)
pub const DC_VC_AUTH_REQUIRED: i32 = 2;
@@ -213,6 +227,9 @@ pub const DC_BOB_SUCCESS: i32 = 1;
// max. width/height of an avatar
pub const AVATAR_SIZE: u32 = 192;
// 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;
#[derive(
Debug,
Display,
@@ -310,8 +327,6 @@ const DC_STR_SELFNOTINGRP: usize = 21; // deprecated;
const DC_STR_NOMESSAGES: usize = 1;
const DC_STR_SELF: usize = 2;
const DC_STR_DRAFT: usize = 3;
const DC_STR_MEMBER: usize = 4;
const DC_STR_CONTACT: usize = 6;
const DC_STR_VOICEMESSAGE: usize = 7;
const DC_STR_DEADDROP: usize = 8;
const DC_STR_IMAGE: usize = 9;
@@ -343,7 +358,6 @@ const DC_STR_ARCHIVEDCHATS: usize = 40;
const DC_STR_STARREDMSGS: usize = 41;
const DC_STR_AC_SETUP_MSG_SUBJECT: usize = 42;
const DC_STR_AC_SETUP_MSG_BODY: usize = 43;
const DC_STR_SELFTALK_SUBTITLE: usize = 50;
const DC_STR_CANNOT_LOGIN: usize = 60;
const DC_STR_SERVER_RESPONSE: usize = 61;
const DC_STR_MSGACTIONBYUSER: usize = 62;

File diff suppressed because it is too large Load Diff

View File

@@ -1,68 +1,70 @@
//! Context module
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Condvar, Mutex, RwLock};
use std::ops::Deref;
use async_std::path::{Path, PathBuf};
use async_std::sync::{channel, Arc, Mutex, Receiver, RwLock, Sender};
use crate::chat::*;
use crate::config::Config;
use crate::constants::*;
use crate::contact::*;
use crate::dc_tools::duration_to_str;
use crate::error::*;
use crate::events::Event;
use crate::imap::*;
use crate::job::*;
use crate::job_thread::JobThread;
use crate::key::Key;
use crate::events::{Event, EventEmitter, Events};
use crate::job::{self, Action};
use crate::key::{DcKey, Key, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::lot::Lot;
use crate::message::{self, Message, MessengerMessage, MsgId};
use crate::param::Params;
use crate::smtp::Smtp;
use crate::scheduler::Scheduler;
use crate::sql::Sql;
use std::time::SystemTime;
/// Callback function type for [Context]
///
/// # Parameters
///
/// * `context` - The context object as returned by [Context::new].
/// * `event` - One of the [Event] items.
/// * `data1` - Depends on the event parameter, see [Event].
/// * `data2` - Depends on the event parameter, see [Event].
pub type ContextCallback = dyn Fn(&Context, Event) -> () + Send + Sync;
#[derive(DebugStub)]
#[derive(Clone, Debug)]
pub struct Context {
/// Database file path
dbfile: PathBuf,
/// Blob directory path
blobdir: PathBuf,
pub sql: Sql,
pub perform_inbox_jobs_needed: Arc<RwLock<bool>>,
pub probe_imap_network: Arc<RwLock<bool>>,
pub inbox_thread: Arc<RwLock<JobThread>>,
pub sentbox_thread: Arc<RwLock<JobThread>>,
pub mvbox_thread: Arc<RwLock<JobThread>>,
pub smtp: Arc<Mutex<Smtp>>,
pub smtp_state: Arc<(Mutex<SmtpState>, Condvar)>,
pub oauth2_critical: Arc<Mutex<()>>,
#[debug_stub = "Callback"]
cb: Box<ContextCallback>,
pub os_name: Option<String>,
pub cmdline_sel_chat_id: Arc<RwLock<ChatId>>,
pub(crate) bob: Arc<RwLock<BobStatus>>,
pub last_smeared_timestamp: RwLock<i64>,
pub running_state: Arc<RwLock<RunningState>>,
/// Mutex to avoid generating the key for the user more than once.
pub generating_key_mutex: Mutex<()>,
pub translated_stockstrings: RwLock<HashMap<usize, String>>,
pub(crate) inner: Arc<InnerContext>,
}
#[derive(Debug, PartialEq, Eq)]
impl Deref for Context {
type Target = InnerContext;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
#[derive(Debug)]
pub struct InnerContext {
/// Database file path
pub(crate) dbfile: PathBuf,
/// Blob directory path
pub(crate) blobdir: PathBuf,
pub(crate) sql: Sql,
pub(crate) os_name: Option<String>,
pub(crate) bob: RwLock<BobStatus>,
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.
pub(crate) generating_key_mutex: Mutex<()>,
/// Mutex to enforce only a single running oauth2 is running.
pub(crate) oauth2_mutex: Mutex<()>,
pub(crate) translated_stockstrings: RwLock<HashMap<usize, String>>,
pub(crate) events: Events,
pub(crate) scheduler: RwLock<Scheduler>,
creation_time: SystemTime,
}
#[derive(Debug)]
pub struct RunningState {
pub ongoing_running: bool,
shall_stop_ongoing: bool,
cancel_sender: Option<Sender<()>>,
}
/// Return some info about deltachat-core
@@ -71,8 +73,8 @@ pub struct RunningState {
/// actual keys and their values which will be present are not
/// guaranteed. Calling [Context::get_info] also includes information
/// about the context on top of the information here.
pub fn get_info() -> HashMap<&'static str, String> {
let mut res = HashMap::new();
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("arch", (std::mem::size_of::<usize>() * 8).to_string());
@@ -82,72 +84,85 @@ pub fn get_info() -> HashMap<&'static str, String> {
impl Context {
/// Creates new context.
pub fn new(cb: Box<ContextCallback>, os_name: String, dbfile: PathBuf) -> Result<Context> {
pretty_env_logger::try_init_timed().ok();
pub async fn new(os_name: String, dbfile: PathBuf) -> Result<Context> {
// pretty_env_logger::try_init_timed().ok();
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
let blobdir = dbfile.with_file_name(blob_fname);
if !blobdir.exists() {
std::fs::create_dir_all(&blobdir)?;
if !blobdir.exists().await {
async_std::fs::create_dir_all(&blobdir).await?;
}
Context::with_blobdir(cb, os_name, dbfile, blobdir)
Context::with_blobdir(os_name, dbfile, blobdir).await
}
pub fn with_blobdir(
cb: Box<ContextCallback>,
pub async fn with_blobdir(
os_name: String,
dbfile: PathBuf,
blobdir: PathBuf,
) -> Result<Context> {
ensure!(
blobdir.is_dir(),
blobdir.is_dir().await,
"Blobdir does not exist: {}",
blobdir.display()
);
let ctx = Context {
let inner = InnerContext {
blobdir,
dbfile,
cb,
os_name: Some(os_name),
running_state: Arc::new(RwLock::new(Default::default())),
running_state: RwLock::new(Default::default()),
sql: Sql::new(),
smtp: Arc::new(Mutex::new(Smtp::new())),
smtp_state: Arc::new((Mutex::new(Default::default()), Condvar::new())),
oauth2_critical: Arc::new(Mutex::new(())),
bob: Arc::new(RwLock::new(Default::default())),
bob: RwLock::new(Default::default()),
last_smeared_timestamp: RwLock::new(0),
cmdline_sel_chat_id: Arc::new(RwLock::new(ChatId::new(0))),
inbox_thread: Arc::new(RwLock::new(JobThread::new(
"INBOX",
"configured_inbox_folder",
Imap::new(),
))),
sentbox_thread: Arc::new(RwLock::new(JobThread::new(
"SENTBOX",
"configured_sentbox_folder",
Imap::new(),
))),
mvbox_thread: Arc::new(RwLock::new(JobThread::new(
"MVBOX",
"configured_mvbox_folder",
Imap::new(),
))),
probe_imap_network: Arc::new(RwLock::new(false)),
perform_inbox_jobs_needed: Arc::new(RwLock::new(false)),
generating_key_mutex: Mutex::new(()),
oauth2_mutex: Mutex::new(()),
translated_stockstrings: RwLock::new(HashMap::new()),
events: Events::default(),
scheduler: RwLock::new(Scheduler::Stopped),
creation_time: std::time::SystemTime::now(),
};
let ctx = Context {
inner: Arc::new(inner),
};
ensure!(
ctx.sql.open(&ctx, &ctx.dbfile, false),
ctx.sql.open(&ctx, &ctx.dbfile, false).await,
"Failed opening sqlite database"
);
Ok(ctx)
}
/// Starts the IO scheduler.
pub async fn start_io(&self) {
info!(self, "starting IO");
assert!(!self.is_io_running().await, "context is already running");
let l = &mut *self.inner.scheduler.write().await;
l.start(self.clone()).await;
}
/// 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");
self.inner.stop_io().await;
}
/// Returns a reference to the underlying SQL instance.
///
/// Warning: this is only here for testing, not part of the public API.
#[cfg(feature = "internals")]
pub fn sql(&self) -> &Sql {
&self.inner.sql
}
/// Returns database file path.
pub fn get_dbfile(&self) -> &Path {
self.dbfile.as_path()
@@ -158,49 +173,57 @@ impl Context {
self.blobdir.as_path()
}
pub fn call_cb(&self, event: Event) {
(*self.cb)(self, event);
/// Emits a single event.
pub fn emit_event(&self, event: Event) {
self.events.emit(event);
}
/*******************************************************************************
* Ongoing process allocation/free/check
******************************************************************************/
/// Get the next queued event.
pub fn get_event_emitter(&self) -> EventEmitter {
self.events.get_emitter()
}
pub fn alloc_ongoing(&self) -> bool {
if self.has_ongoing() {
warn!(self, "There is already another ongoing process running.",);
// Ongoing process allocation/free/check
false
} else {
let s_a = self.running_state.clone();
let mut s = s_a.write().unwrap();
s.ongoing_running = true;
s.shall_stop_ongoing = false;
true
pub async fn alloc_ongoing(&self) -> Result<Receiver<()>> {
if self.has_ongoing().await {
bail!("There is already another ongoing process running.");
}
let s_a = &self.running_state;
let mut s = s_a.write().await;
s.ongoing_running = true;
s.shall_stop_ongoing = false;
let (sender, receiver) = channel(1);
s.cancel_sender = Some(sender);
Ok(receiver)
}
pub fn free_ongoing(&self) {
let s_a = self.running_state.clone();
let mut s = s_a.write().unwrap();
pub async fn free_ongoing(&self) {
let s_a = &self.running_state;
let mut s = s_a.write().await;
s.ongoing_running = false;
s.shall_stop_ongoing = true;
s.cancel_sender.take();
}
pub fn has_ongoing(&self) -> bool {
let s_a = self.running_state.clone();
let s = s_a.read().unwrap();
pub async fn has_ongoing(&self) -> bool {
let s_a = &self.running_state;
let s = s_a.read().await;
s.ongoing_running || !s.shall_stop_ongoing
}
/// Signal an ongoing process to stop.
pub fn stop_ongoing(&self) {
let s_a = self.running_state.clone();
let mut s = s_a.write().unwrap();
pub async fn stop_ongoing(&self) {
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 s.ongoing_running && !s.shall_stop_ongoing {
info!(self, "Signaling the ongoing process to stop ASAP.",);
@@ -210,69 +233,71 @@ impl Context {
};
}
pub fn shall_stop_ongoing(&self) -> bool {
self.running_state
.clone()
.read()
.unwrap()
.shall_stop_ongoing
pub async fn shall_stop_ongoing(&self) -> bool {
self.running_state.read().await.shall_stop_ongoing
}
/*******************************************************************************
* UI chat/message related API
******************************************************************************/
pub fn get_info(&self) -> HashMap<&'static str, String> {
pub async fn get_info(&self) -> BTreeMap<&'static str, String> {
let unset = "0";
let l = LoginParam::from_database(self, "");
let l2 = LoginParam::from_database(self, "configured_");
let displayname = self.get_config(Config::Displayname);
let chats = get_chat_cnt(self) as usize;
let real_msgs = message::get_real_msg_cnt(self) as usize;
let deaddrop_msgs = message::get_deaddrop_msg_cnt(self) as usize;
let contacts = Contact::get_real_cnt(self) as usize;
let is_configured = self.get_config_int(Config::Configured);
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 dbversion = self
.sql
.get_raw_config_int(self, "dbversion")
.await
.unwrap_or_default();
let journal_mode = self
.sql
.query_get_value(self, "PRAGMA journal_mode;", paramsv![])
.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);
let mdns_enabled = self.get_config_int(Config::MdnsEnabled);
let bcc_self = self.get_config_int(Config::BccSelf);
let prv_key_cnt: Option<isize> = self
.sql
.query_get_value(self, "SELECT COUNT(*) FROM keypairs;", paramsv![])
.await;
let prv_key_cnt: Option<isize> =
self.sql
.query_get_value(self, "SELECT COUNT(*) FROM keypairs;", rusqlite::NO_PARAMS);
let pub_key_cnt: Option<isize> = self.sql.query_get_value(
self,
"SELECT COUNT(*) FROM acpeerstates;",
rusqlite::NO_PARAMS,
);
let fingerprint_str = if let Some(key) = Key::from_self_public(self, &l2.addr, &self.sql) {
key.fingerprint()
} else {
"<Not yet calculated>".into()
let pub_key_cnt: Option<isize> = self
.sql
.query_get_value(self, "SELECT COUNT(*) FROM acpeerstates;", paramsv![])
.await;
let fingerprint_str = match SignedPublicKey::load_self(self).await {
Ok(key) => Key::from(key).fingerprint(),
Err(err) => format!("<key failure: {}>", err),
};
let inbox_watch = self.get_config_int(Config::InboxWatch);
let sentbox_watch = self.get_config_int(Config::SentboxWatch);
let mvbox_watch = self.get_config_int(Config::MvboxWatch);
let mvbox_move = self.get_config_int(Config::MvboxMove);
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 folders_configured = self
.sql
.get_raw_config_int(self, "folders_configured")
.await
.unwrap_or_default();
let configured_sentbox_folder = self
.sql
.get_raw_config(self, "configured_sentbox_folder")
.await
.unwrap_or_else(|| "<unset>".to_string());
let configured_mvbox_folder = self
.sql
.get_raw_config(self, "configured_mvbox_folder")
.await
.unwrap_or_else(|| "<unset>".to_string());
let mut res = get_info();
@@ -282,11 +307,13 @@ impl Context {
res.insert("number_of_contacts", contacts.to_string());
res.insert("database_dir", self.get_dbfile().display().to_string());
res.insert("database_version", dbversion.to_string());
res.insert("journal_mode", journal_mode);
res.insert("blobdir", self.get_blobdir().display().to_string());
res.insert("display_name", displayname.unwrap_or_else(|| unset.into()));
res.insert(
"selfavatar",
self.get_config(Config::Selfavatar)
.await
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert("is_configured", is_configured.to_string());
@@ -312,11 +339,14 @@ impl Context {
);
res.insert("fingerprint", fingerprint_str);
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
res
}
pub fn get_fresh_msgs(&self) -> Vec<MsgId> {
let show_deaddrop = 0;
pub async fn get_fresh_msgs(&self) -> Vec<MsgId> {
let show_deaddrop: i32 = 0;
self.sql
.query_map(
concat!(
@@ -333,7 +363,7 @@ impl Context {
" AND (c.blocked=0 OR c.blocked=?)",
" ORDER BY m.timestamp DESC,m.id DESC;"
),
&[10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
paramsv![10, 9, if 0 != show_deaddrop { 2 } else { 0 }],
|row| row.get::<_, MsgId>(0),
|rows| {
let mut ret = Vec::new();
@@ -343,11 +373,12 @@ impl Context {
Ok(ret)
},
)
.await
.unwrap_or_default()
}
#[allow(non_snake_case)]
pub fn search_msgs(&self, chat_id: ChatId, query: impl AsRef<str>) -> Vec<MsgId> {
pub async fn search_msgs(&self, chat_id: ChatId, query: impl AsRef<str>) -> Vec<MsgId> {
let real_query = query.as_ref().trim();
if real_query.is_empty() {
return Vec::new();
@@ -387,7 +418,7 @@ impl Context {
self.sql
.query_map(
query,
params![chat_id, &strLikeInText, &strLikeBeg],
paramsv![chat_id, strLikeInText, strLikeBeg],
|row| row.get::<_, MsgId>("id"),
|rows| {
let mut ret = Vec::new();
@@ -397,6 +428,7 @@ impl Context {
Ok(ret)
},
)
.await
.unwrap_or_default()
}
@@ -404,8 +436,11 @@ impl Context {
folder_name.as_ref() == "INBOX"
}
pub fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
let sentbox_name = self.sql.get_raw_config(self, "configured_sentbox_folder");
pub async fn is_sentbox(&self, folder_name: impl AsRef<str>) -> bool {
let sentbox_name = self
.sql
.get_raw_config(self, "configured_sentbox_folder")
.await;
if let Some(name) = sentbox_name {
name == folder_name.as_ref()
} else {
@@ -413,8 +448,11 @@ impl Context {
}
}
pub fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
let mvbox_name = self.sql.get_raw_config(self, "configured_mvbox_folder");
pub async fn is_mvbox(&self, folder_name: impl AsRef<str>) -> bool {
let mvbox_name = self
.sql
.get_raw_config(self, "configured_mvbox_folder")
.await;
if let Some(name) = mvbox_name {
name == folder_name.as_ref()
@@ -423,15 +461,15 @@ impl Context {
}
}
pub fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) {
if !self.get_config_bool(Config::MvboxMove) {
pub async fn do_heuristics_moves(&self, folder: &str, msg_id: MsgId) {
if !self.get_config_bool(Config::MvboxMove).await {
return;
}
if self.is_mvbox(folder) {
if self.is_mvbox(folder).await {
return;
}
if let Ok(msg) = Message::load_from_db(self, msg_id) {
if let Ok(msg) = Message::load_from_db(self, msg_id).await {
if msg.is_setupmessage() {
// do not move setup messages;
// there may be a non-delta device that wants to handle it
@@ -441,30 +479,32 @@ impl Context {
match msg.is_dc_message {
MessengerMessage::No => {}
MessengerMessage::Yes | MessengerMessage::Reply => {
job_add(
job::add(
self,
Action::MoveMsg,
msg.id.to_u32() as i32,
Params::new(),
0,
);
job::Job::new(Action::MoveMsg, msg.id.to_u32(), Params::new(), 0),
)
.await;
}
}
}
}
}
impl Drop for Context {
fn drop(&mut self) {
info!(self, "disconnecting inbox-thread",);
self.inbox_thread.read().unwrap().imap.disconnect(self);
info!(self, "disconnecting sentbox-thread",);
self.sentbox_thread.read().unwrap().imap.disconnect(self);
info!(self, "disconnecting mvbox-thread",);
self.mvbox_thread.read().unwrap().imap.disconnect(self);
info!(self, "disconnecting SMTP");
self.smtp.clone().lock().unwrap().disconnect();
self.sql.close(self);
impl InnerContext {
async fn is_io_running(&self) -> bool {
self.scheduler.read().await.is_running()
}
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;
}
}
}
@@ -473,6 +513,7 @@ impl Default for RunningState {
RunningState {
ongoing_running: false,
shall_stop_ongoing: true,
cancel_sender: None,
}
}
}
@@ -484,28 +525,6 @@ pub(crate) struct BobStatus {
pub qr_scan: Option<Lot>,
}
#[derive(Debug, PartialEq)]
pub(crate) enum PerformJobsNeeded {
Not,
AtOnce,
AvoidDos,
}
impl Default for PerformJobsNeeded {
fn default() -> Self {
Self::Not
}
}
#[derive(Default, Debug)]
pub struct SmtpState {
pub idle: bool,
pub suspended: bool,
pub doing_jobs: bool,
pub(crate) perform_jobs_needed: PerformJobsNeeded,
pub probe_network: bool,
}
pub fn get_version_str() -> &'static str {
&DC_VERSION_STR
}
@@ -516,81 +535,81 @@ mod tests {
use crate::test_utils::*;
#[test]
fn test_wrong_db() {
#[async_std::test]
async fn test_wrong_db() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
std::fs::write(&dbfile, b"123").unwrap();
let res = Context::new(Box::new(|_, _| ()), "FakeOs".into(), dbfile);
let res = Context::new("FakeOs".into(), dbfile.into()).await;
assert!(res.is_err());
}
#[test]
fn test_get_fresh_msgs() {
let t = dummy_context();
let fresh = t.ctx.get_fresh_msgs();
#[async_std::test]
async fn test_get_fresh_msgs() {
let t = dummy_context().await;
let fresh = t.ctx.get_fresh_msgs().await;
assert!(fresh.is_empty())
}
#[test]
fn test_blobdir_exists() {
#[async_std::test]
async fn test_blobdir_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile).unwrap();
Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
let blobdir = tmp.path().join("db.sqlite-blobs");
assert!(blobdir.is_dir());
}
#[test]
fn test_wrong_blogdir() {
#[async_std::test]
async fn test_wrong_blogdir() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("db.sqlite-blobs");
std::fs::write(&blobdir, b"123").unwrap();
let res = Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile);
let res = Context::new("FakeOS".into(), dbfile.into()).await;
assert!(res.is_err());
}
#[test]
fn test_sqlite_parent_not_exists() {
#[async_std::test]
async fn test_sqlite_parent_not_exists() {
let tmp = tempfile::tempdir().unwrap();
let subdir = tmp.path().join("subdir");
let dbfile = subdir.join("db.sqlite");
let dbfile2 = dbfile.clone();
Context::new(Box::new(|_, _| ()), "FakeOS".into(), dbfile).unwrap();
Context::new("FakeOS".into(), dbfile.into()).await.unwrap();
assert!(subdir.is_dir());
assert!(dbfile2.is_file());
}
#[test]
fn test_with_empty_blobdir() {
#[async_std::test]
async fn test_with_empty_blobdir() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir(Box::new(|_, _| ()), "FakeOS".into(), dbfile, blobdir);
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into()).await;
assert!(res.is_err());
}
#[test]
fn test_with_blobdir_not_exists() {
#[async_std::test]
async fn test_with_blobdir_not_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs");
let res = Context::with_blobdir(Box::new(|_, _| ()), "FakeOS".into(), dbfile, blobdir);
let res = Context::with_blobdir("FakeOS".into(), dbfile.into(), blobdir.into()).await;
assert!(res.is_err());
}
#[test]
fn no_crashes_on_context_deref() {
let t = dummy_context();
#[async_std::test]
async fn no_crashes_on_context_deref() {
let t = dummy_context().await;
std::mem::drop(t.ctx);
}
#[test]
fn test_get_info() {
let t = dummy_context();
#[async_std::test]
async fn test_get_info() {
let t = dummy_context().await;
let info = t.ctx.get_info();
let info = t.ctx.get_info().await;
assert!(info.get("database_dir").is_some());
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,26 +3,27 @@
use core::cmp::{max, min};
use std::borrow::Cow;
use std::path::{Path, PathBuf};
use std::fmt;
use std::str::FromStr;
use std::time::SystemTime;
use std::{fmt, fs};
use std::time::{Duration, SystemTime};
use async_std::path::{Path, PathBuf};
use async_std::{fs, io};
use chrono::{Local, TimeZone};
use rand::{thread_rng, Rng};
use crate::context::Context;
use crate::error::Error;
use crate::error::{bail, Error};
use crate::events::Event;
pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
0 != v && 0 == v & (v - 1)
}
/// Shortens a string to a specified length and adds "..." or "[...]" to the end of
/// the shortened string.
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize, do_unwrap: bool) -> Cow<str> {
let ellipse = if do_unwrap { "..." } else { "[...]" };
/// Shortens a string to a specified length and adds "[...]" to the
/// end of the shortened string.
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() {
@@ -75,6 +76,14 @@ pub fn dc_timestamp_to_str(wanted: i64) -> String {
ts.format("%Y.%m.%d %H:%M:%S").to_string()
}
pub fn duration_to_str(duration: Duration) -> String {
let secs = duration.as_secs();
let h = secs / 3600;
let m = (secs % 3600) / 60;
let s = (secs % 3600) % 60;
format!("{}h {}m {}s", h, m, s)
}
pub(crate) fn dc_gm2local_offset() -> i64 {
/* returns the offset that must be _added_ to an UTC/GMT-time to create the localtime.
the function may return negative values. */
@@ -99,9 +108,9 @@ const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5;
// returns the currently smeared timestamp,
// may be used to check if call to dc_create_smeared_timestamp() is needed or not.
// the returned timestamp MUST NOT be used to be sent out or saved in the database!
pub(crate) fn dc_smeared_time(context: &Context) -> i64 {
pub(crate) async fn dc_smeared_time(context: &Context) -> i64 {
let mut now = time();
let ts = *context.last_smeared_timestamp.read().unwrap();
let ts = *context.last_smeared_timestamp.read().await;
if ts >= now {
now = ts + 1;
}
@@ -110,11 +119,11 @@ pub(crate) fn dc_smeared_time(context: &Context) -> i64 {
}
// returns a timestamp that is guaranteed to be unique.
pub(crate) fn dc_create_smeared_timestamp(context: &Context) -> i64 {
pub(crate) async fn dc_create_smeared_timestamp(context: &Context) -> i64 {
let now = time();
let mut ret = now;
let mut last_smeared_timestamp = context.last_smeared_timestamp.write().unwrap();
let mut last_smeared_timestamp = context.last_smeared_timestamp.write().await;
if ret <= *last_smeared_timestamp {
ret = *last_smeared_timestamp + 1;
if ret - now > MAX_SECONDS_TO_LEND_FROM_FUTURE {
@@ -129,12 +138,12 @@ pub(crate) fn dc_create_smeared_timestamp(context: &Context) -> i64 {
// creates `count` timestamps that are guaranteed to be unique.
// the frist created timestamps is returned directly,
// get the other timestamps just by adding 1..count-1
pub(crate) fn dc_create_smeared_timestamps(context: &Context, count: usize) -> i64 {
pub(crate) async fn dc_create_smeared_timestamps(context: &Context, count: usize) -> i64 {
let now = time();
let count = count as i64;
let mut start = now + min(count, MAX_SECONDS_TO_LEND_FROM_FUTURE) - count;
let mut last_smeared_timestamp = context.last_smeared_timestamp.write().unwrap();
let mut last_smeared_timestamp = context.last_smeared_timestamp.write().await;
start = max(*last_smeared_timestamp + 1, start);
*last_smeared_timestamp = start + count - 1;
@@ -240,11 +249,8 @@ pub fn dc_get_filemeta(buf: &[u8]) -> Result<(u32, u32), Error> {
///
/// If `path` starts with "$BLOBDIR", replaces it with the blobdir path.
/// Otherwise, returns path as is.
pub(crate) fn dc_get_abs_path<P: AsRef<std::path::Path>>(
context: &Context,
path: P,
) -> std::path::PathBuf {
let p: &std::path::Path = path.as_ref();
pub(crate) fn dc_get_abs_path<P: AsRef<Path>>(context: &Context, path: P) -> PathBuf {
let p: &Path = path.as_ref();
if let Ok(p) = p.strip_prefix("$BLOBDIR") {
context.get_blobdir().join(p)
} else {
@@ -252,20 +258,20 @@ pub(crate) fn dc_get_abs_path<P: AsRef<std::path::Path>>(
}
}
pub(crate) fn dc_get_filebytes(context: &Context, path: impl AsRef<std::path::Path>) -> u64 {
pub(crate) async fn dc_get_filebytes(context: &Context, path: impl AsRef<Path>) -> u64 {
let path_abs = dc_get_abs_path(context, &path);
match fs::metadata(&path_abs) {
match fs::metadata(&path_abs).await {
Ok(meta) => meta.len() as u64,
Err(_err) => 0,
}
}
pub(crate) fn dc_delete_file(context: &Context, path: impl AsRef<std::path::Path>) -> bool {
pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) -> bool {
let path_abs = dc_get_abs_path(context, &path);
if !path_abs.exists() {
if !path_abs.exists().await {
return false;
}
if !path_abs.is_file() {
if !path_abs.is_file().await {
warn!(
context,
"refusing to delete non-file \"{}\".",
@@ -275,9 +281,9 @@ pub(crate) fn dc_delete_file(context: &Context, path: impl AsRef<std::path::Path
}
let dpath = format!("{}", path.as_ref().to_string_lossy());
match fs::remove_file(path_abs) {
match fs::remove_file(path_abs).await {
Ok(_) => {
context.call_cb(Event::DeletedBlobFile(dpath));
context.emit_event(Event::DeletedBlobFile(dpath));
true
}
Err(err) => {
@@ -287,13 +293,13 @@ pub(crate) fn dc_delete_file(context: &Context, path: impl AsRef<std::path::Path
}
}
pub(crate) fn dc_copy_file(
pub(crate) async fn dc_copy_file(
context: &Context,
src_path: impl AsRef<std::path::Path>,
dest_path: impl AsRef<std::path::Path>,
src_path: impl AsRef<Path>,
dest_path: impl AsRef<Path>,
) -> bool {
let src_abs = dc_get_abs_path(context, &src_path);
let mut src_file = match fs::File::open(&src_abs) {
let mut src_file = match fs::File::open(&src_abs).await {
Ok(file) => file,
Err(err) => {
warn!(
@@ -311,6 +317,7 @@ pub(crate) fn dc_copy_file(
.create_new(true)
.write(true)
.open(&dest_abs)
.await
{
Ok(file) => file,
Err(err) => {
@@ -324,7 +331,7 @@ pub(crate) fn dc_copy_file(
}
};
match std::io::copy(&mut src_file, &mut dest_file) {
match io::copy(&mut src_file, &mut dest_file).await {
Ok(_) => true,
Err(err) => {
error!(
@@ -336,20 +343,20 @@ pub(crate) fn dc_copy_file(
);
{
// Attempt to remove the failed file, swallow errors resulting from that.
fs::remove_file(dest_abs).ok();
fs::remove_file(dest_abs).await.ok();
}
false
}
}
}
pub(crate) fn dc_create_folder(
pub(crate) async fn dc_create_folder(
context: &Context,
path: impl AsRef<std::path::Path>,
) -> Result<(), std::io::Error> {
path: impl AsRef<Path>,
) -> Result<(), io::Error> {
let path_abs = dc_get_abs_path(context, &path);
if !path_abs.exists() {
match fs::create_dir_all(path_abs) {
if !path_abs.exists().await {
match fs::create_dir_all(path_abs).await {
Ok(_) => Ok(()),
Err(err) => {
warn!(
@@ -367,13 +374,13 @@ pub(crate) fn dc_create_folder(
}
/// Write a the given content to provied file path.
pub(crate) fn dc_write_file(
pub(crate) async fn dc_write_file(
context: &Context,
path: impl AsRef<Path>,
buf: &[u8],
) -> Result<(), std::io::Error> {
) -> Result<(), io::Error> {
let path_abs = dc_get_abs_path(context, &path);
fs::write(&path_abs, buf).map_err(|err| {
fs::write(&path_abs, buf).await.map_err(|err| {
warn!(
context,
"Cannot write {} bytes to \"{}\": {}",
@@ -385,13 +392,10 @@ pub(crate) fn dc_write_file(
})
}
pub fn dc_read_file<P: AsRef<std::path::Path>>(
context: &Context,
path: P,
) -> Result<Vec<u8>, Error> {
pub async fn dc_read_file<P: AsRef<Path>>(context: &Context, path: P) -> Result<Vec<u8>, Error> {
let path_abs = dc_get_abs_path(context, &path);
match fs::read(&path_abs) {
match fs::read(&path_abs).await {
Ok(bytes) => Ok(bytes),
Err(err) => {
warn!(
@@ -405,13 +409,31 @@ pub fn dc_read_file<P: AsRef<std::path::Path>>(
}
}
pub fn dc_open_file<P: AsRef<std::path::Path>>(
pub async fn dc_open_file<P: AsRef<Path>>(context: &Context, path: P) -> Result<fs::File, Error> {
let path_abs = dc_get_abs_path(context, &path);
match fs::File::open(&path_abs).await {
Ok(bytes) => Ok(bytes),
Err(err) => {
warn!(
context,
"Cannot read \"{}\" or file is empty: {}",
path.as_ref().display(),
err
);
Err(err.into())
}
}
}
pub fn dc_open_file_std<P: AsRef<std::path::Path>>(
context: &Context,
path: P,
) -> Result<std::fs::File, Error> {
let path_abs = dc_get_abs_path(context, &path);
let p: PathBuf = path.as_ref().into();
let path_abs = dc_get_abs_path(context, p);
match fs::File::open(&path_abs) {
match std::fs::File::open(&path_abs) {
Ok(bytes) => Ok(bytes),
Err(err) => {
warn!(
@@ -425,7 +447,7 @@ pub fn dc_open_file<P: AsRef<std::path::Path>>(
}
}
pub(crate) fn dc_get_next_backup_path(
pub(crate) async fn dc_get_next_backup_path(
folder: impl AsRef<Path>,
backup_time: i64,
) -> Result<PathBuf, Error> {
@@ -438,7 +460,7 @@ pub(crate) fn dc_get_next_backup_path(
for i in 0..64 {
let mut path = folder.clone();
path.push(format!("{}-{}.bak", stem, i));
if !path.exists() {
if !path.exists().await {
return Ok(path);
}
}
@@ -452,6 +474,23 @@ pub(crate) fn time() -> i64 {
.as_secs() as i64
}
/// An invalid email address was encountered
#[derive(Debug, thiserror::Error)]
#[error("Invalid email address: {message} ({addr})")]
pub struct InvalidEmailError {
message: String,
addr: String,
}
impl InvalidEmailError {
fn new(msg: impl Into<String>, addr: impl Into<String>) -> InvalidEmailError {
InvalidEmailError {
message: msg.into(),
addr: addr.into(),
}
}
}
/// Very simple email address wrapper.
///
/// Represents an email address, right now just the `name@domain` portion.
@@ -475,7 +514,7 @@ pub struct EmailAddress {
}
impl EmailAddress {
pub fn new(input: &str) -> Result<Self, Error> {
pub fn new(input: &str) -> Result<Self, InvalidEmailError> {
input.parse::<EmailAddress>()
}
}
@@ -487,31 +526,55 @@ impl fmt::Display for EmailAddress {
}
impl FromStr for EmailAddress {
type Err = Error;
type Err = InvalidEmailError;
/// Performs a dead-simple parse of an email address.
fn from_str(input: &str) -> Result<EmailAddress, Error> {
ensure!(!input.is_empty(), "empty string is not valid");
fn from_str(input: &str) -> Result<EmailAddress, InvalidEmailError> {
if input.is_empty() {
return Err(InvalidEmailError::new("empty string is not valid", input));
}
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
ensure!(parts.len() > 1, "missing '@' character");
let local = parts[1];
let domain = parts[0];
let err = |msg: &str| {
Err(InvalidEmailError {
message: msg.to_string(),
addr: input.to_string(),
})
};
match &parts[..] {
[domain, local] => {
if local.is_empty() {
return err("empty string is not valid for local part");
}
if domain.len() <= 3 {
return err("domain is too short");
}
let dot = domain.find('.');
match dot {
None => {
return err("invalid domain");
}
Some(dot_idx) => {
if dot_idx >= domain.len() - 2 {
return err("invalid domain");
}
}
}
Ok(EmailAddress {
local: (*local).to_string(),
domain: (*domain).to_string(),
})
}
_ => err("missing '@' character"),
}
}
}
ensure!(
!local.is_empty(),
"empty string is not valid for local part"
);
ensure!(domain.len() > 3, "domain is too short");
let dot = domain.find('.');
ensure!(dot.is_some(), "invalid domain");
ensure!(dot.unwrap() < domain.len() - 2, "invalid domain");
Ok(EmailAddress {
local: local.to_string(),
domain: domain.to_string(),
})
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)
}
}
@@ -538,54 +601,42 @@ mod tests {
#[test]
fn test_dc_truncate_1() {
let s = "this is a little test string";
assert_eq!(dc_truncate(s, 16, false), "this is a [...]");
assert_eq!(dc_truncate(s, 16, true), "this is a ...");
assert_eq!(dc_truncate(s, 16), "this is a [...]");
}
#[test]
fn test_dc_truncate_2() {
assert_eq!(dc_truncate("1234", 2, false), "1234");
assert_eq!(dc_truncate("1234", 2, true), "1234");
assert_eq!(dc_truncate("1234", 2), "1234");
}
#[test]
fn test_dc_truncate_3() {
assert_eq!(dc_truncate("1234567", 1, false), "1[...]");
assert_eq!(dc_truncate("1234567", 1, true), "1...");
assert_eq!(dc_truncate("1234567", 1), "1[...]");
}
#[test]
fn test_dc_truncate_4() {
assert_eq!(dc_truncate("123456", 4, false), "123456");
assert_eq!(dc_truncate("123456", 4, true), "123456");
assert_eq!(dc_truncate("123456", 4), "123456");
}
#[test]
fn test_dc_truncate_edge() {
assert_eq!(dc_truncate("", 4, false), "");
assert_eq!(dc_truncate("", 4, true), "");
assert_eq!(dc_truncate("", 4), "");
assert_eq!(dc_truncate("\n hello \n world", 4, false), "\n [...]");
assert_eq!(dc_truncate("\n hello \n world", 4, true), "\n ...");
assert_eq!(dc_truncate("\n hello \n world", 4), "\n [...]");
assert_eq!(dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1), "𐠈[...]");
assert_eq!(
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1, false),
"𐠈[...]"
);
assert_eq!(
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0, false),
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0),
"𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ"
);
// 9 characters, so no truncation
assert_eq!(
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6, false),
"𑒀ὐ¢🜀\u{1e01b}A a🟠",
);
assert_eq!(dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",);
// 12 characters, truncation
assert_eq!(
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6, false),
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6),
"𑒀ὐ¢🜀\u{1e01b}A[...]",
);
}
@@ -701,11 +752,10 @@ mod tests {
#[test]
fn test_dc_truncate(
buf: String,
approx_chars in 0..10000usize,
do_unwrap: bool,
approx_chars in 0..10000usize
) {
let res = dc_truncate(&buf, approx_chars, do_unwrap);
let el_len = if do_unwrap { 3 } else { 5 };
let res = dc_truncate(&buf, approx_chars);
let el_len = 5;
let l = res.chars().count();
if approx_chars > 0 {
assert!(
@@ -719,40 +769,40 @@ mod tests {
if approx_chars > 0 && buf.chars().count() > approx_chars + el_len {
let l = res.len();
if do_unwrap {
assert_eq!(&res[l-3..l], "...", "missing ellipsis in {}", &res);
} else {
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
}
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
}
}
}
#[test]
fn test_file_handling() {
let t = dummy_context();
#[async_std::test]
async fn test_file_handling() {
let t = dummy_context().await;
let context = &t.ctx;
let dc_file_exist = |ctx: &Context, fname: &str| {
ctx.get_blobdir()
.join(Path::new(fname).file_name().unwrap())
.exists()
};
assert!(!dc_delete_file(context, "$BLOBDIR/lkqwjelqkwlje"));
if dc_file_exist(context, "$BLOBDIR/foobar")
|| dc_file_exist(context, "$BLOBDIR/dada")
|| dc_file_exist(context, "$BLOBDIR/foobar.dadada")
|| dc_file_exist(context, "$BLOBDIR/foobar-folder")
{
dc_delete_file(context, "$BLOBDIR/foobar");
dc_delete_file(context, "$BLOBDIR/dada");
dc_delete_file(context, "$BLOBDIR/foobar.dadada");
dc_delete_file(context, "$BLOBDIR/foobar-folder");
macro_rules! dc_file_exist {
($ctx:expr, $fname:expr) => {
$ctx.get_blobdir()
.join(Path::new($fname).file_name().unwrap())
.exists()
};
}
assert!(dc_write_file(context, "$BLOBDIR/foobar", b"content").is_ok());
assert!(dc_file_exist(context, "$BLOBDIR/foobar",));
assert!(!dc_file_exist(context, "$BLOBDIR/foobarx"));
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/foobar"), 7);
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());
assert!(dc_file_exist!(context, "$BLOBDIR/foobar").await);
assert!(!dc_file_exist!(context, "$BLOBDIR/foobarx").await);
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/foobar").await, 7);
let abs_path = context
.get_blobdir()
@@ -760,31 +810,33 @@ mod tests {
.to_string_lossy()
.to_string();
assert!(dc_file_exist(context, &abs_path));
assert!(dc_file_exist!(context, &abs_path).await);
assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada",));
assert!(dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada").await);
// attempting to copy a second time should fail
assert!(!dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada",));
assert!(!dc_copy_file(context, "$BLOBDIR/foobar", "$BLOBDIR/dada").await);
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada",), 7);
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/dada").await, 7);
let buf = dc_read_file(context, "$BLOBDIR/dada").unwrap();
let buf = dc_read_file(context, "$BLOBDIR/dada").await.unwrap();
assert_eq!(buf.len(), 7);
assert_eq!(&buf, b"content");
assert!(dc_delete_file(context, "$BLOBDIR/foobar"));
assert!(dc_delete_file(context, "$BLOBDIR/dada"));
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder").is_ok());
assert!(dc_file_exist(context, "$BLOBDIR/foobar-folder",));
assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder"));
assert!(dc_delete_file(context, "$BLOBDIR/foobar").await);
assert!(dc_delete_file(context, "$BLOBDIR/dada").await);
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder")
.await
.is_ok());
assert!(dc_file_exist!(context, "$BLOBDIR/foobar-folder").await);
assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder").await);
let fn0 = "$BLOBDIR/data.data";
assert!(dc_write_file(context, &fn0, b"content").is_ok());
assert!(dc_write_file(context, &fn0, b"content").await.is_ok());
assert!(dc_delete_file(context, &fn0));
assert!(!dc_file_exist(context, &fn0));
assert!(dc_delete_file(context, &fn0).await);
assert!(!dc_file_exist!(context, &fn0).await);
}
#[test]
@@ -801,15 +853,15 @@ mod tests {
assert!(!listflags_has(listflags, DC_GCL_ADD_SELF));
}
#[test]
fn test_create_smeared_timestamp() {
let t = dummy_context();
#[async_std::test]
async fn test_create_smeared_timestamp() {
let t = dummy_context().await;
assert_ne!(
dc_create_smeared_timestamp(&t.ctx),
dc_create_smeared_timestamp(&t.ctx)
dc_create_smeared_timestamp(&t.ctx).await,
dc_create_smeared_timestamp(&t.ctx).await
);
assert!(
dc_create_smeared_timestamp(&t.ctx)
dc_create_smeared_timestamp(&t.ctx).await
>= SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
@@ -817,17 +869,50 @@ mod tests {
);
}
#[test]
fn test_create_smeared_timestamps() {
let t = dummy_context();
#[async_std::test]
async fn test_create_smeared_timestamps() {
let t = dummy_context().await;
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1;
let start = dc_create_smeared_timestamps(&t.ctx, count as usize);
let next = dc_smeared_time(&t.ctx);
let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
let next = dc_smeared_time(&t.ctx).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);
let next = dc_smeared_time(&t.ctx);
let start = dc_create_smeared_timestamps(&t.ctx, count as usize).await;
let next = dc_smeared_time(&t.ctx).await;
assert!((start + count - 1) < next);
}
#[test]
fn test_duration_to_str() {
assert_eq!(duration_to_str(Duration::from_secs(0)), "0h 0m 0s");
assert_eq!(duration_to_str(Duration::from_secs(59)), "0h 0m 59s");
assert_eq!(duration_to_str(Duration::from_secs(60)), "0h 1m 0s");
assert_eq!(duration_to_str(Duration::from_secs(61)), "0h 1m 1s");
assert_eq!(duration_to_str(Duration::from_secs(59 * 60)), "0h 59m 0s");
assert_eq!(
duration_to_str(Duration::from_secs(59 * 60 + 59)),
"0h 59m 59s"
);
assert_eq!(
duration_to_str(Duration::from_secs(59 * 60 + 60)),
"1h 0m 0s"
);
assert_eq!(
duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 59)),
"2h 59m 59s"
);
assert_eq!(
duration_to_str(Duration::from_secs(2 * 60 * 60 + 59 * 60 + 60)),
"3h 0m 0s"
);
assert_eq!(
duration_to_str(Duration::from_secs(3 * 60 * 60 + 59)),
"3h 0m 59s"
);
assert_eq!(
duration_to_str(Duration::from_secs(3 * 60 * 60 + 60)),
"3h 1m 0s"
);
}
}

View File

@@ -3,7 +3,6 @@
//! A module to remove HTML tags from the email text
use lazy_static::lazy_static;
use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
lazy_static! {
@@ -35,6 +34,7 @@ pub fn dehtml(buf: &str) -> String {
};
let mut reader = quick_xml::Reader::from_str(buf);
reader.check_end_names(false);
let mut buf = Vec::new();
@@ -225,4 +225,23 @@ mod tests {
"<>\"\'& äÄöÖüÜß fooÆçÇ \u{2666}\u{200e}\u{200f}\u{200c}&noent;\u{200d}"
);
}
#[test]
fn test_unclosed_tags() {
let input = r##"
<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'
'http://www.w3.org/TR/html4/loose.dtd'>
<html>
<head>
<title>Hi</title>
<meta http-equiv='Content-Type' content='text/html; charset=iso-8859-1'>
</head>
<body>
lots of text
</body>
</html>
"##;
let txt = dehtml(input);
assert_eq!(txt.trim(), "lots of text");
}
}

View File

@@ -1,19 +1,17 @@
//! End-to-end encryption support.
use std::collections::HashSet;
use std::convert::TryFrom;
use mailparse::ParsedMail;
use num_traits::FromPrimitive;
use crate::aheader::*;
use crate::config::Config;
use crate::constants::KeyGenType;
use crate::context::Context;
use crate::dc_tools::EmailAddress;
use crate::error::*;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{self, Key, KeyPairUse, SignedPublicKey};
use crate::headerdef::HeaderDef;
use crate::headerdef::HeaderDefMap;
use crate::key::{DcKey, Key, SignedPublicKey, SignedSecretKey};
use crate::keyring::*;
use crate::peerstate::*;
use crate::pgp;
@@ -27,18 +25,18 @@ pub struct EncryptHelper {
}
impl EncryptHelper {
pub fn new(context: &Context) -> Result<EncryptHelper> {
pub async fn new(context: &Context) -> Result<EncryptHelper> {
let prefer_encrypt =
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled))
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await)
.unwrap_or_default();
let addr = match context.get_config(Config::ConfiguredAddr) {
let addr = match context.get_config(Config::ConfiguredAddr).await {
None => {
bail!("addr not configured!");
}
Some(addr) => addr,
};
let public_key = load_or_generate_self_public_key(context, &addr)?;
let public_key = SignedPublicKey::load_self(context).await?;
Ok(EncryptHelper {
prefer_encrypt,
@@ -88,12 +86,12 @@ impl EncryptHelper {
}
/// Tries to encrypt the passed in `mail`.
pub fn encrypt(
pub async fn encrypt(
&mut self,
context: &Context,
min_verified: PeerstateVerifiedStatus,
mail_to_encrypt: lettre_email::PartBuilder,
peerstates: &[(Option<Peerstate>, &str)],
peerstates: &[(Option<Peerstate<'_>>, &str)],
) -> Result<String> {
let mut keyring = Keyring::default();
@@ -108,8 +106,7 @@ impl EncryptHelper {
}
let public_key = Key::from(self.public_key.clone());
keyring.add_ref(&public_key);
let sign_key = Key::from_self_private(context, self.addr.clone(), &context.sql)
.ok_or_else(|| format_err!("missing own private key"))?;
let sign_key = Key::from(SignedSecretKey::load_self(context).await?);
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
@@ -119,15 +116,15 @@ impl EncryptHelper {
}
}
pub fn try_decrypt(
pub async fn try_decrypt(
context: &Context,
mail: &ParsedMail<'_>,
message_time: i64,
) -> Result<(Option<Vec<u8>>, HashSet<String>)> {
let from = mail
.headers
.get_header_value(HeaderDef::From_)?
.and_then(|from_addr| mailparse::addrparse(&from_addr).ok())
.get_header(HeaderDef::From_)
.and_then(|from_addr| mailparse::addrparse_header(&from_addr).ok())
.and_then(|from| from.extract_single_info())
.map(|from| from.addr)
.unwrap_or_default();
@@ -136,19 +133,19 @@ pub fn try_decrypt(
let autocryptheader = Aheader::from_headers(context, &from, &mail.headers);
if message_time > 0 {
peerstate = Peerstate::from_addr(context, &context.sql, &from);
peerstate = Peerstate::from_addr(context, &from).await;
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)?;
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)?;
peerstate.save_to_db(&context.sql, false).await?;
}
} else if let Some(ref header) = autocryptheader {
let p = Peerstate::from_header(context, header, message_time);
p.save_to_db(&context.sql, true)?;
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
}
@@ -158,16 +155,19 @@ pub fn try_decrypt(
let mut public_keyring_for_validate = Keyring::default();
let mut out_mail = None;
let mut signatures = HashSet::default();
let self_addr = context.get_config(Config::ConfiguredAddr);
let self_addr = context.get_config(Config::ConfiguredAddr).await;
if let Some(self_addr) = self_addr {
if private_keyring.load_self_private_for_decrypting(context, self_addr, &context.sql) {
if private_keyring
.load_self_private_for_decrypting(context, self_addr, &context.sql)
.await
{
if peerstate.as_ref().map(|p| p.last_seen).unwrap_or_else(|| 0) == 0 {
peerstate = Peerstate::from_addr(&context, &context.sql, &from);
peerstate = Peerstate::from_addr(&context, &from).await;
}
if let Some(ref peerstate) = peerstate {
if peerstate.degrade_event.is_some() {
handle_degrade_event(context, &peerstate)?;
handle_degrade_event(context, &peerstate).await?;
}
if let Some(ref key) = peerstate.gossip_key {
public_keyring_for_validate.add_ref(key);
@@ -189,43 +189,6 @@ pub fn try_decrypt(
Ok((out_mail, signatures))
}
/// Load public key from database or generate a new one.
///
/// This will load a public key from the database, generating and
/// storing a new one when one doesn't exist yet. Care is taken to
/// only generate one key per context even when multiple threads call
/// this function concurrently.
fn load_or_generate_self_public_key(
context: &Context,
self_addr: impl AsRef<str>,
) -> Result<SignedPublicKey> {
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
return SignedPublicKey::try_from(key)
.map_err(|_| Error::Message("Not a public key".into()));
}
let _guard = context.generating_key_mutex.lock().unwrap();
// Check again in case the key was generated while we were waiting for the lock.
if let Some(key) = Key::from_self_public(context, &self_addr, &context.sql) {
return SignedPublicKey::try_from(key)
.map_err(|_| Error::Message("Not a public key".into()));
}
let start = std::time::Instant::now();
let keygen_type =
KeyGenType::from_i32(context.get_config_int(Config::KeyGenType)).unwrap_or_default();
info!(context, "Generating keypair with type {}", keygen_type);
let keypair = pgp::create_keypair(EmailAddress::new(self_addr.as_ref())?, keygen_type)?;
key::store_self_keypair(context, &keypair, KeyPairUse::Default)?;
info!(
context,
"Keypair generated in {:.3}s.",
start.elapsed().as_secs()
);
Ok(keypair.public)
}
/// Returns a reference to the encrypted payload and validates the autocrypt structure.
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Result<&'a ParsedMail<'b>> {
ensure!(
@@ -347,14 +310,18 @@ fn contains_report(mail: &ParsedMail<'_>) -> bool {
///
/// If this succeeds you are also guaranteed that the
/// [Config::ConfiguredAddr] is configured, this address is returned.
pub fn ensure_secret_key_exists(context: &Context) -> Result<String> {
let self_addr = context.get_config(Config::ConfiguredAddr).ok_or_else(|| {
format_err!(concat!(
"Failed to get self address, ",
"cannot ensure secret key if not configured."
))
})?;
load_or_generate_self_public_key(context, &self_addr)?;
// TODO, remove this once deltachat::key::Key no longer exists.
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await
.ok_or_else(|| {
format_err!(concat!(
"Failed to get self address, ",
"cannot ensure secret key if not configured."
))
})?;
SignedPublicKey::load_self(context).await?;
Ok(self_addr)
}
@@ -367,17 +334,17 @@ mod tests {
mod ensure_secret_key_exists {
use super::*;
#[test]
fn test_prexisting() {
let t = dummy_context();
let test_addr = configure_alice_keypair(&t.ctx);
assert_eq!(ensure_secret_key_exists(&t.ctx).unwrap(), test_addr);
#[async_std::test]
async fn test_prexisting() {
let t = dummy_context().await;
let test_addr = configure_alice_keypair(&t.ctx).await;
assert_eq!(ensure_secret_key_exists(&t.ctx).await.unwrap(), test_addr);
}
#[test]
fn test_not_configured() {
let t = dummy_context();
assert!(ensure_secret_key_exists(&t.ctx).is_err());
#[async_std::test]
async fn test_not_configured() {
let t = dummy_context().await;
assert!(ensure_secret_key_exists(&t.ctx).await.is_err());
}
}
@@ -405,49 +372,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
);
}
mod load_or_generate_self_public_key {
use super::*;
#[test]
fn test_existing() {
let t = dummy_context();
let addr = configure_alice_keypair(&t.ctx);
let key = load_or_generate_self_public_key(&t.ctx, addr);
assert!(key.is_ok());
}
#[test]
#[ignore] // generating keys is expensive
fn test_generate() {
let t = dummy_context();
let addr = "alice@example.org";
let key0 = load_or_generate_self_public_key(&t.ctx, addr);
assert!(key0.is_ok());
let key1 = load_or_generate_self_public_key(&t.ctx, addr);
assert!(key1.is_ok());
assert_eq!(key0.unwrap(), key1.unwrap());
}
#[test]
#[ignore]
fn test_generate_concurrent() {
use std::sync::Arc;
use std::thread;
let t = dummy_context();
let ctx = Arc::new(t.ctx);
let ctx0 = Arc::clone(&ctx);
let thr0 =
thread::spawn(move || load_or_generate_self_public_key(&ctx0, "alice@example.org"));
let ctx1 = Arc::clone(&ctx);
let thr1 =
thread::spawn(move || load_or_generate_self_public_key(&ctx1, "alice@example.org"));
let res0 = thr0.join().unwrap();
let res1 = thr1.join().unwrap();
assert_eq!(res0.unwrap(), res1.unwrap());
}
}
#[test]
fn test_has_decrypted_pgp_armor() {
let data = b" -----BEGIN PGP MESSAGE-----";

View File

@@ -1,189 +1,6 @@
//! # Error handling
use lettre_email::mime;
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "{:?}", _0)]
Failure(failure::Error),
#[fail(display = "SQL error: {:?}", _0)]
SqlError(#[cause] crate::sql::Error),
#[fail(display = "{:?}", _0)]
Io(std::io::Error),
#[fail(display = "{:?}", _0)]
Message(String),
#[fail(display = "{:?}", _0)]
MessageWithCause(String, #[cause] failure::Error, failure::Backtrace),
#[fail(display = "{:?}", _0)]
Image(image_meta::ImageError),
#[fail(display = "{:?}", _0)]
Utf8(std::str::Utf8Error),
#[fail(display = "PGP: {:?}", _0)]
Pgp(pgp::errors::Error),
#[fail(display = "Base64Decode: {:?}", _0)]
Base64Decode(base64::DecodeError),
#[fail(display = "{:?}", _0)]
FromUtf8(std::string::FromUtf8Error),
#[fail(display = "{}", _0)]
BlobError(#[cause] crate::blob::BlobError),
#[fail(display = "Invalid Message ID.")]
InvalidMsgId,
#[fail(display = "Watch folder not found {:?}", _0)]
WatchFolderNotFound(String),
#[fail(display = "Invalid Email: {:?}", _0)]
MailParseError(#[cause] mailparse::MailParseError),
#[fail(display = "Building invalid Email: {:?}", _0)]
LettreError(#[cause] lettre_email::error::Error),
#[fail(display = "SMTP error: {:?}", _0)]
SmtpError(#[cause] async_smtp::error::Error),
#[fail(display = "FromStr error: {:?}", _0)]
FromStr(#[cause] mime::FromStrError),
#[fail(display = "Not Configured")]
NotConfigured,
}
pub type Result<T> = std::result::Result<T, Error>;
impl From<crate::sql::Error> for Error {
fn from(err: crate::sql::Error) -> Error {
Error::SqlError(err)
}
}
impl From<base64::DecodeError> for Error {
fn from(err: base64::DecodeError) -> Error {
Error::Base64Decode(err)
}
}
impl From<failure::Error> for Error {
fn from(err: failure::Error) -> Error {
Error::Failure(err)
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Error {
Error::Io(err)
}
}
impl From<std::str::Utf8Error> for Error {
fn from(err: std::str::Utf8Error) -> Error {
Error::Utf8(err)
}
}
impl From<image_meta::ImageError> for Error {
fn from(err: image_meta::ImageError) -> Error {
Error::Image(err)
}
}
impl From<pgp::errors::Error> for Error {
fn from(err: pgp::errors::Error) -> Error {
Error::Pgp(err)
}
}
impl From<std::string::FromUtf8Error> for Error {
fn from(err: std::string::FromUtf8Error) -> Error {
Error::FromUtf8(err)
}
}
impl From<crate::blob::BlobError> for Error {
fn from(err: crate::blob::BlobError) -> Error {
Error::BlobError(err)
}
}
impl From<crate::message::InvalidMsgId> for Error {
fn from(_err: crate::message::InvalidMsgId) -> Error {
Error::InvalidMsgId
}
}
impl From<crate::key::SaveKeyError> for Error {
fn from(err: crate::key::SaveKeyError) -> Error {
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
}
}
impl From<crate::pgp::PgpKeygenError> for Error {
fn from(err: crate::pgp::PgpKeygenError) -> Error {
Error::MessageWithCause(format!("{}", err), err.into(), failure::Backtrace::new())
}
}
impl From<mailparse::MailParseError> for Error {
fn from(err: mailparse::MailParseError) -> Error {
Error::MailParseError(err)
}
}
impl From<lettre_email::error::Error> for Error {
fn from(err: lettre_email::error::Error) -> Error {
Error::LettreError(err)
}
}
impl From<mime::FromStrError> for Error {
fn from(err: mime::FromStrError) -> Error {
Error::FromStr(err)
}
}
#[macro_export]
macro_rules! bail {
($e:expr) => {
return Err($crate::error::Error::Message($e.to_string()));
};
($fmt:expr, $($arg:tt)+) => {
return Err($crate::error::Error::Message(format!($fmt, $($arg)+)));
};
}
#[macro_export]
macro_rules! format_err {
($e:expr) => {
$crate::error::Error::Message($e.to_string());
};
($fmt:expr, $($arg:tt)+) => {
$crate::error::Error::Message(format!($fmt, $($arg)+));
};
}
#[macro_export(local_inner_macros)]
macro_rules! ensure {
($cond:expr, $e:expr) => {
if !($cond) {
bail!($e);
}
};
($cond:expr, $fmt:expr, $($arg:tt)+) => {
if !($cond) {
bail!($fmt, $($arg)+);
}
};
}
pub use anyhow::{bail, ensure, format_err, Error, Result};
#[macro_export]
macro_rules! ensure_eq {

View File

@@ -1,12 +1,65 @@
//! # Events specification
use std::path::PathBuf;
use async_std::path::PathBuf;
use async_std::sync::{channel, Receiver, Sender, TrySendError};
use strum::EnumProperty;
use crate::chat::ChatId;
use crate::message::MsgId;
#[derive(Debug)]
pub struct Events {
receiver: Receiver<Event>,
sender: Sender<Event>,
}
impl Default for Events {
fn default() -> Self {
let (sender, receiver) = channel(1_000);
Self { receiver, sender }
}
}
impl Events {
pub fn emit(&self, event: Event) {
match self.sender.try_send(event) {
Ok(()) => {}
Err(TrySendError::Full(event)) => {
// when we are full, we pop remove the oldest event and push on the new one
let _ = self.receiver.try_recv();
// try again
self.emit(event);
}
Err(TrySendError::Disconnected(_)) => {
unreachable!("unable to emit event, channel disconnected");
}
}
}
/// Retrieve the event emitter.
pub fn get_emitter(&self) -> EventEmitter {
EventEmitter(self.receiver.clone())
}
}
#[derive(Debug, Clone)]
pub struct EventEmitter(Receiver<Event>);
impl EventEmitter {
/// Blocking recv of an event. Return `None` if the `Sender` has been droped.
pub fn recv_sync(&self) -> Option<Event> {
async_std::task::block_on(self.recv())
}
/// Blocking 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()
}
}
impl Event {
/// Returns the corresponding Event id.
pub fn as_id(&self) -> i32 {
@@ -201,10 +254,4 @@ pub enum Event {
/// (Bob has verified alice and waits until Alice does the same for him)
#[strum(props(id = "2061"))]
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
/// This event is sent out to the inviter when a joiner successfully joined a group.
/// @param data1 (int) chat_id
/// @param data2 (int) contact_id
#[strum(props(id = "2062"))]
SecurejoinMemberAdded { chat_id: ChatId, contact_id: u32 },
}

View File

@@ -1,5 +1,5 @@
use crate::strum::AsStaticRef;
use mailparse::{MailHeader, MailHeaderMap, MailParseError};
use mailparse::{MailHeader, MailHeaderMap};
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
#[strum(serialize_all = "kebab_case")]
@@ -52,13 +52,17 @@ impl HeaderDef {
}
pub trait HeaderDefMap {
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError>;
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String>;
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader>;
}
impl HeaderDefMap for [MailHeader<'_>] {
fn get_header_value(&self, headerdef: HeaderDef) -> Result<Option<String>, MailParseError> {
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String> {
self.get_first_value(headerdef.get_headername())
}
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader> {
self.get_first_header(headerdef.get_headername())
}
}
#[cfg(test)]
@@ -79,18 +83,13 @@ mod tests {
let (headers, _) =
mailparse::parse_headers(b"fRoM: Bob\naUtoCryPt-SeTup-MessAge: v99").unwrap();
assert_eq!(
headers
.get_header_value(HeaderDef::AutocryptSetupMessage)
.unwrap(),
headers.get_header_value(HeaderDef::AutocryptSetupMessage),
Some("v99".to_string())
);
assert_eq!(
headers.get_header_value(HeaderDef::From_).unwrap(),
headers.get_header_value(HeaderDef::From_),
Some("Bob".to_string())
);
assert_eq!(
headers.get_header_value(HeaderDef::Autocrypt).unwrap(),
None
);
assert_eq!(headers.get_header_value(HeaderDef::Autocrypt), None);
}
}

View File

@@ -1,20 +1,80 @@
use std::ops::{Deref, DerefMut};
use async_imap::{
error::{Error as ImapError, Result as ImapResult},
Client as ImapClient,
};
use async_native_tls::TlsStream;
use async_std::net::{self, TcpStream};
use super::session::Session;
use crate::login_param::{dc_build_tls, CertificateChecks};
use super::session::SessionStream;
#[derive(Debug)]
pub(crate) enum Client {
Secure(ImapClient<TlsStream<TcpStream>>),
Insecure(ImapClient<TcpStream>),
pub(crate) struct Client {
is_secure: bool,
inner: ImapClient<Box<dyn SessionStream>>,
}
impl Deref for Client {
type Target = ImapClient<Box<dyn SessionStream>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for Client {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl Client {
pub async fn login<U: AsRef<str>, P: AsRef<str>>(
self,
username: U,
password: P,
) -> std::result::Result<Session, (ImapError, Self)> {
let Client { inner, is_secure } = self;
let session = inner
.login(username, password)
.await
.map_err(|(err, client)| {
(
err,
Client {
is_secure,
inner: client,
},
)
})?;
Ok(Session { inner: session })
}
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
self,
auth_type: S,
authenticator: &A,
) -> std::result::Result<Session, (ImapError, Self)> {
let Client { inner, is_secure } = self;
let session =
inner
.authenticate(auth_type, authenticator)
.await
.map_err(|(err, client)| {
(
err,
Client {
is_secure,
inner: client,
},
)
})?;
Ok(Session { inner: session })
}
pub async fn connect_secure<A: net::ToSocketAddrs, S: AsRef<str>>(
addr: A,
domain: S,
@@ -22,7 +82,8 @@ impl Client {
) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let tls = dc_build_tls(certificate_checks);
let tls_stream = tls.connect(domain.as_ref(), stream).await?;
let tls_stream: Box<dyn SessionStream> =
Box::new(tls.connect(domain.as_ref(), stream).await?);
let mut client = ImapClient::new(tls_stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
client.debug = true;
@@ -33,11 +94,14 @@ impl Client {
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client::Secure(client))
Ok(Client {
is_secure: true,
inner: client,
})
}
pub async fn connect_insecure<A: net::ToSocketAddrs>(addr: A) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let stream: Box<dyn SessionStream> = Box::new(TcpStream::connect(addr).await?);
let mut client = ImapClient::new(stream);
if std::env::var(crate::DCC_IMAP_DEBUG).is_ok() {
@@ -48,7 +112,10 @@ impl Client {
.await
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client::Insecure(client))
Ok(Client {
is_secure: false,
inner: client,
})
}
pub async fn secure<S: AsRef<str>>(
@@ -56,49 +123,21 @@ impl Client {
domain: S,
certificate_checks: CertificateChecks,
) -> ImapResult<Client> {
match self {
Client::Insecure(client) => {
let tls = dc_build_tls(certificate_checks);
let client_sec = client.secure(domain, tls).await?;
if self.is_secure {
Ok(self)
} else {
let Client { mut inner, .. } = self;
let tls = dc_build_tls(certificate_checks);
inner.run_command_and_check_ok("STARTTLS", None).await?;
Ok(Client::Secure(client_sec))
}
// Nothing to do
Client::Secure(_) => Ok(self),
}
}
let stream = inner.into_inner();
let ssl_stream = tls.connect(domain.as_ref(), stream).await?;
let boxed: Box<dyn SessionStream> = Box::new(ssl_stream);
pub async fn authenticate<A: async_imap::Authenticator, S: AsRef<str>>(
self,
auth_type: S,
authenticator: &A,
) -> Result<Session, (ImapError, Client)> {
match self {
Client::Secure(i) => match i.authenticate(auth_type, authenticator).await {
Ok(session) => Ok(Session::Secure(session)),
Err((err, c)) => Err((err, Client::Secure(c))),
},
Client::Insecure(i) => match i.authenticate(auth_type, authenticator).await {
Ok(session) => Ok(Session::Insecure(session)),
Err((err, c)) => Err((err, Client::Insecure(c))),
},
}
}
pub async fn login<U: AsRef<str>, P: AsRef<str>>(
self,
username: U,
password: P,
) -> Result<Session, (ImapError, Client)> {
match self {
Client::Secure(i) => match i.login(username, password).await {
Ok(session) => Ok(Session::Secure(session)),
Err((err, c)) => Err((err, Client::Secure(c))),
},
Client::Insecure(i) => match i.login(username, password).await {
Ok(session) => Ok(Session::Insecure(session)),
Err((err, c)) => Err((err, Client::Insecure(c))),
},
Ok(Client {
is_secure: true,
inner: ImapClient::new(boxed),
})
}
}
}

View File

@@ -1,11 +1,7 @@
use super::Imap;
use async_imap::extensions::idle::{Handle as ImapIdleHandle, IdleResponse};
use async_native_tls::TlsStream;
use async_std::net::TcpStream;
use async_imap::extensions::idle::IdleResponse;
use async_std::prelude::*;
use async_std::task;
use std::sync::atomic::Ordering;
use std::time::{Duration, SystemTime};
use crate::context::Context;
@@ -15,285 +11,187 @@ use super::session::Session;
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Fail)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[fail(display = "IMAP IDLE protocol failed to init/complete")]
IdleProtocolFailed(#[cause] async_imap::error::Error),
#[error("IMAP IDLE protocol failed to init/complete")]
IdleProtocolFailed(#[from] async_imap::error::Error),
#[fail(display = "IMAP IDLE protocol timed out")]
IdleTimeout(#[cause] async_std::future::TimeoutError),
#[error("IMAP IDLE protocol timed out")]
IdleTimeout(#[from] async_std::future::TimeoutError),
#[fail(display = "IMAP server does not have IDLE capability")]
#[error("IMAP server does not have IDLE capability")]
IdleAbilityMissing,
#[fail(display = "IMAP select folder error")]
SelectFolderError(#[cause] select_folder::Error),
#[error("IMAP select folder error")]
SelectFolderError(#[from] select_folder::Error),
#[fail(display = "Setup handle error")]
SetupHandleError(#[cause] super::Error),
}
impl From<select_folder::Error> for Error {
fn from(err: select_folder::Error) -> Error {
Error::SelectFolderError(err)
}
}
#[derive(Debug)]
pub(crate) enum IdleHandle {
Secure(ImapIdleHandle<TlsStream<TcpStream>>),
Insecure(ImapIdleHandle<TcpStream>),
}
impl Session {
pub fn idle(self) -> IdleHandle {
match self {
Session::Secure(i) => {
let h = i.idle();
IdleHandle::Secure(h)
}
Session::Insecure(i) => {
let h = i.idle();
IdleHandle::Insecure(h)
}
}
}
#[error("Setup handle error")]
SetupHandleError(#[from] super::Error),
}
impl Imap {
pub fn can_idle(&self) -> bool {
task::block_on(async move { self.config.read().await.can_idle })
self.config.can_idle
}
pub fn idle(&self, context: &Context, watch_folder: Option<String>) -> Result<()> {
task::block_on(async move {
if !self.can_idle() {
return Err(Error::IdleAbilityMissing);
pub async fn idle(&mut self, context: &Context, watch_folder: Option<String>) -> Result<()> {
use futures::future::FutureExt;
if !self.can_idle() {
return Err(Error::IdleAbilityMissing);
}
self.setup_handle_if_needed(context).await?;
self.select_folder(context, watch_folder.clone()).await?;
let session = self.session.take();
let timeout = Duration::from_secs(23 * 60);
if let Some(session) = session {
let mut handle = session.idle();
if let Err(err) = handle.init().await {
return Err(Error::IdleProtocolFailed(err));
}
self.setup_handle_if_needed(context)
.await
.map_err(Error::SetupHandleError)?;
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
self.select_folder(context, watch_folder.clone()).await?;
let session = self.session.lock().await.take();
let timeout = Duration::from_secs(23 * 60);
if let Some(session) = session {
match session.idle() {
// BEWARE: If you change the Secure branch you
// typically also need to change the Insecure branch.
IdleHandle::Secure(mut handle) => {
if let Err(err) = handle.init().await {
return Err(Error::IdleProtocolFailed(err));
}
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
*self.interrupt.lock().await = Some(interrupt);
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
// interrupt_idle has happened before we
// provided self.interrupt
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
std::mem::drop(idle_wait);
info!(context, "Idle wait was skipped");
} else {
info!(context, "Idle entering wait-on-remote state");
match idle_wait.await {
Ok(IdleResponse::NewData(_)) => {
info!(context, "Idle has NewData");
}
// TODO: idle_wait does not distinguish manual interrupts
// from Timeouts if we would know it's a Timeout we could bail
// directly and reconnect .
Ok(IdleResponse::Timeout) => {
info!(context, "Idle-wait timeout or interruption");
}
Ok(IdleResponse::ManualInterrupt) => {
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
}
}
}
// if we can't properly terminate the idle
// protocol let's break the connection.
let res =
async_std::future::timeout(Duration::from_secs(15), handle.done())
.await
.map_err(|err| {
self.trigger_reconnect();
Error::IdleTimeout(err)
})?;
match res {
Ok(session) => {
*self.session.lock().await = Some(Session::Secure(session));
}
Err(err) => {
// if we cannot terminate IDLE it probably
// means that we waited long (with idle_wait)
// but the network went away/changed
self.trigger_reconnect();
return Err(Error::IdleProtocolFailed(err));
}
}
}
IdleHandle::Insecure(mut handle) => {
if let Err(err) = handle.init().await {
return Err(Error::IdleProtocolFailed(err));
}
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
*self.interrupt.lock().await = Some(interrupt);
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
// interrupt_idle has happened before we
// provided self.interrupt
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
std::mem::drop(idle_wait);
info!(context, "Idle wait was skipped");
} else {
info!(context, "Idle entering wait-on-remote state");
match idle_wait.await {
Ok(IdleResponse::NewData(_)) => {
info!(context, "Idle has NewData");
}
// TODO: idle_wait does not distinguish manual interrupts
// from Timeouts if we would know it's a Timeout we could bail
// directly and reconnect .
Ok(IdleResponse::Timeout) => {
info!(context, "Idle-wait timeout or interruption");
}
Ok(IdleResponse::ManualInterrupt) => {
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
}
}
}
// if we can't properly terminate the idle
// protocol let's break the connection.
let res =
async_std::future::timeout(Duration::from_secs(15), handle.done())
.await
.map_err(|err| {
self.trigger_reconnect();
Error::IdleTimeout(err)
})?;
match res {
Ok(session) => {
*self.session.lock().await = Some(Session::Insecure(session));
}
Err(err) => {
// if we cannot terminate IDLE it probably
// means that we waited long (with idle_wait)
// but the network went away/changed
self.trigger_reconnect();
return Err(Error::IdleProtocolFailed(err));
}
}
}
}
}
Ok(())
})
}
pub(crate) fn fake_idle(&self, context: &Context, watch_folder: Option<String>) {
// Idle using polling. This is also needed if we're not yet configured -
// in this case, we're waiting for a configure job (and an interrupt).
task::block_on(async move {
let fake_idle_start_time = SystemTime::now();
info!(context, "IMAP-fake-IDLEing...");
let interrupt = stop_token::StopSource::new();
// check every minute if there are new messages
// TODO: grow sleep durations / make them more flexible
let interval = async_std::stream::interval(Duration::from_secs(60));
let mut interrupt_interval = interrupt.stop_token().stop_stream(interval);
*self.interrupt.lock().await = Some(interrupt);
if self.skip_next_idle_wait.load(Ordering::SeqCst) {
if self.skip_next_idle_wait {
// interrupt_idle has happened before we
// provided self.interrupt
self.skip_next_idle_wait.store(false, Ordering::SeqCst);
info!(context, "fake-idle wait was skipped");
} else {
// loop until we are interrupted or if we fetched something
while let Some(_) = interrupt_interval.next().await {
// try to connect with proper login params
// (setup_handle_if_needed might not know about them if we
// never successfully connected)
if let Err(err) = self.connect_configured(context) {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
if self.config.read().await.can_idle {
// we only fake-idled because network was gone during IDLE, probably
break;
}
info!(context, "fake_idle is connected");
// we are connected, let's see if fetching messages results
// in anything. If so, we behave as if IDLE had data but
// will have already fetched the messages so perform_*_fetch
// will not find any new.
self.skip_next_idle_wait = false;
drop(idle_wait);
drop(interrupt);
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;
}
}
Err(err) => {
error!(context, "could not fetch from folder: {}", err);
self.trigger_reconnect()
}
}
info!(context, "Idle wait was skipped");
} else {
info!(context, "Idle entering wait-on-remote state");
let fut = idle_wait.race(
self.idle_interrupt
.recv()
.map(|_| Ok(IdleResponse::ManualInterrupt)),
);
match fut.await {
Ok(IdleResponse::NewData(_)) => {
info!(context, "Idle has NewData");
}
// TODO: idle_wait does not distinguish manual interrupts
// from Timeouts if we would know it's a Timeout we could bail
// directly and reconnect .
Ok(IdleResponse::Timeout) => {
info!(context, "Idle-wait timeout or interruption");
}
Ok(IdleResponse::ManualInterrupt) => {
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
}
}
}
self.interrupt.lock().await.take();
info!(
context,
"IMAP-fake-IDLE done after {:.4}s",
SystemTime::now()
.duration_since(fake_idle_start_time)
.unwrap_or_default()
.as_millis() as f64
/ 1000.,
);
})
// if we can't properly terminate the idle
// protocol let's break the connection.
let res = handle
.done()
.timeout(Duration::from_secs(15))
.await
.map_err(|err| {
self.trigger_reconnect();
Error::IdleTimeout(err)
})?;
match res {
Ok(session) => {
self.session = Some(Session { inner: session });
}
Err(err) => {
// if we cannot terminate IDLE it probably
// means that we waited long (with idle_wait)
// but the network went away/changed
self.trigger_reconnect();
return Err(Error::IdleProtocolFailed(err));
}
}
}
Ok(())
}
pub fn interrupt_idle(&self, context: &Context) {
task::block_on(async move {
let mut interrupt: Option<stop_token::StopSource> = self.interrupt.lock().await.take();
if interrupt.is_none() {
// idle wait is not running, signal it needs to skip
self.skip_next_idle_wait.store(true, Ordering::SeqCst);
pub(crate) async fn fake_idle(&mut self, context: &Context, watch_folder: Option<String>) {
// Idle using polling. This is also needed if we're not yet configured -
// in this case, we're waiting for a configure job (and an interrupt).
// meanwhile idle-wait may have produced the StopSource
interrupt = self.interrupt.lock().await.take();
let fake_idle_start_time = SystemTime::now();
info!(context, "IMAP-fake-IDLEing...");
if self.skip_next_idle_wait {
// interrupt_idle has happened before we
// provided self.interrupt
self.skip_next_idle_wait = false;
info!(context, "fake-idle wait was skipped");
} else {
// check every minute if there are new messages
// TODO: grow sleep durations / make them more flexible
let mut interval = async_std::stream::interval(Duration::from_secs(60));
// loop until we are interrupted or if we fetched something
loop {
use futures::future::FutureExt;
match interval
.next()
.race(self.idle_interrupt.recv().map(|_| None))
.await
{
Some(_) => {
// try to connect with proper login params
// (setup_handle_if_needed might not know about them if we
// never successfully connected)
if let Err(err) = self.connect_configured(context).await {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
if self.config.can_idle {
// we only fake-idled because network was gone during IDLE, probably
break;
}
info!(context, "fake_idle is connected");
// we are connected, let's see if fetching messages results
// in anything. If so, we behave as if IDLE had data but
// 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;
}
}
Err(err) => {
error!(context, "could not fetch from folder: {}", err);
self.trigger_reconnect()
}
}
}
}
None => {
// Interrupt
break;
}
}
}
// let's manually drop the StopSource
if interrupt.is_some() {
// the imap thread provided us a stop token but might
// not have entered idle_wait yet, give it some time
// for that to happen. XXX handle this without extra wait
// https://github.com/deltachat/deltachat-core-rust/issues/925
std::thread::sleep(Duration::from_millis(200));
info!(context, "low-level: dropping stop-source to interrupt idle");
std::mem::drop(interrupt)
}
});
}
info!(
context,
"IMAP-fake-IDLE done after {:.4}s",
SystemTime::now()
.duration_since(fake_idle_start_time)
.unwrap_or_default()
.as_millis() as f64
/ 1000.,
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,36 +4,63 @@ use crate::context::Context;
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Fail)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[fail(display = "IMAP Could not obtain imap-session object.")]
#[error("IMAP Could not obtain imap-session object.")]
NoSession,
#[fail(display = "IMAP Connection Lost or no connection established")]
#[error("IMAP Connection Lost or no connection established")]
ConnectionLost,
#[fail(display = "IMAP Folder name invalid: {:?}", _0)]
#[error("IMAP Folder name invalid: {0}")]
BadFolderName(String),
#[fail(display = "IMAP close/expunge failed: {}", _0)]
CloseExpungeFailed(#[cause] async_imap::error::Error),
#[error("IMAP close/expunge failed")]
CloseExpungeFailed(#[from] async_imap::error::Error),
#[fail(display = "IMAP other error: {:?}", _0)]
#[error("IMAP other error: {0}")]
Other(String),
}
impl Imap {
/// Issues a CLOSE command to expunge selected folder.
///
/// CLOSE is considerably faster than an EXPUNGE, see
/// https://tools.ietf.org/html/rfc3501#section-6.4.2
async fn close_folder(&mut self, context: &Context) -> Result<()> {
if let Some(ref folder) = self.config.selected_folder {
info!(context, "Expunge messages in \"{}\".", folder);
if let Some(ref mut session) = self.session {
match session.close().await {
Ok(_) => {
info!(context, "close/expunge succeeded");
}
Err(err) => {
self.trigger_reconnect();
return Err(Error::CloseExpungeFailed(err));
}
}
} else {
return Err(Error::NoSession);
}
}
self.config.selected_folder = None;
self.config.selected_folder_needs_expunge = false;
Ok(())
}
/// select a folder, possibly update uid_validity and, if needed,
/// expunge the folder to remove delete-marked messages.
pub(super) async fn select_folder<S: AsRef<str>>(
&self,
&mut self,
context: &Context,
folder: Option<S>,
) -> Result<()> {
if self.session.lock().await.is_none() {
let mut cfg = self.config.write().await;
cfg.selected_folder = None;
cfg.selected_folder_needs_expunge = false;
if self.session.is_none() {
self.config.selected_folder = None;
self.config.selected_folder_needs_expunge = false;
self.trigger_reconnect();
return Err(Error::NoSession);
}
@@ -41,7 +68,7 @@ impl Imap {
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
// if there is _no_ new folder, we continue as we might want to expunge below.
if let Some(ref folder) = folder {
if let Some(ref selected_folder) = self.config.read().await.selected_folder {
if let Some(ref selected_folder) = self.config.selected_folder {
if folder.as_ref() == selected_folder {
return Ok(());
}
@@ -49,33 +76,14 @@ impl Imap {
}
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
let needs_expunge = { self.config.read().await.selected_folder_needs_expunge };
let needs_expunge = { self.config.selected_folder_needs_expunge };
if needs_expunge {
if let Some(ref folder) = self.config.read().await.selected_folder {
info!(context, "Expunge messages in \"{}\".", folder);
// A CLOSE-SELECT is considerably faster than an EXPUNGE-SELECT, see
// https://tools.ietf.org/html/rfc3501#section-6.4.2
if let Some(ref mut session) = &mut *self.session.lock().await {
match session.close().await {
Ok(_) => {
info!(context, "close/expunge succeeded");
}
Err(err) => {
self.trigger_reconnect();
return Err(Error::CloseExpungeFailed(err));
}
}
} else {
return Err(Error::NoSession);
}
}
self.config.write().await.selected_folder_needs_expunge = false;
self.close_folder(context).await?;
}
// select new folder
if let Some(ref folder) = folder {
if let Some(ref mut session) = &mut *self.session.lock().await {
if let Some(ref mut session) = &mut self.session {
let res = session.select(folder).await;
// https://tools.ietf.org/html/rfc3501#section-6.3.1
@@ -84,21 +92,20 @@ impl Imap {
match res {
Ok(mailbox) => {
let mut config = self.config.write().await;
config.selected_folder = Some(folder.as_ref().to_string());
config.selected_mailbox = Some(mailbox);
self.config.selected_folder = Some(folder.as_ref().to_string());
self.config.selected_mailbox = Some(mailbox);
Ok(())
}
Err(async_imap::error::Error::ConnectionLost) => {
self.trigger_reconnect();
self.config.write().await.selected_folder = None;
self.config.selected_folder = None;
Err(Error::ConnectionLost)
}
Err(async_imap::error::Error::Validate(_)) => {
Err(Error::BadFolderName(folder.as_ref().to_string()))
}
Err(err) => {
self.config.write().await.selected_folder = None;
self.config.selected_folder = None;
self.trigger_reconnect();
Err(Error::Other(err.to_string()))
}

View File

@@ -1,172 +1,40 @@
use async_imap::{
error::Result as ImapResult,
types::{Capabilities, Fetch, Mailbox, Name},
Session as ImapSession,
};
use std::ops::{Deref, DerefMut};
use async_imap::Session as ImapSession;
use async_native_tls::TlsStream;
use async_std::net::TcpStream;
use async_std::prelude::*;
#[derive(Debug)]
pub(crate) enum Session {
Secure(ImapSession<TlsStream<TcpStream>>),
Insecure(ImapSession<TcpStream>),
pub(crate) struct Session {
pub(super) inner: ImapSession<Box<dyn SessionStream>>,
}
pub(crate) trait SessionStream:
async_std::io::Read + async_std::io::Write + Unpin + Send + Sync + std::fmt::Debug
{
}
impl SessionStream for TlsStream<Box<dyn SessionStream>> {}
impl SessionStream for TlsStream<TcpStream> {}
impl SessionStream for TcpStream {}
impl Deref for Session {
type Target = ImapSession<Box<dyn SessionStream>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for Session {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl Session {
pub async fn capabilities(&mut self) -> ImapResult<Capabilities> {
let res = match self {
Session::Secure(i) => i.capabilities().await?,
Session::Insecure(i) => i.capabilities().await?,
};
Ok(res)
}
pub async fn list(
&mut self,
reference_name: Option<&str>,
mailbox_pattern: Option<&str>,
) -> ImapResult<Vec<Name>> {
let res = match self {
Session::Secure(i) => {
i.list(reference_name, mailbox_pattern)
.await?
.collect::<ImapResult<_>>()
.await?
}
Session::Insecure(i) => {
i.list(reference_name, mailbox_pattern)
.await?
.collect::<ImapResult<_>>()
.await?
}
};
Ok(res)
}
pub async fn create<S: AsRef<str>>(&mut self, mailbox_name: S) -> ImapResult<()> {
match self {
Session::Secure(i) => i.create(mailbox_name).await?,
Session::Insecure(i) => i.create(mailbox_name).await?,
}
Ok(())
}
pub async fn subscribe<S: AsRef<str>>(&mut self, mailbox: S) -> ImapResult<()> {
match self {
Session::Secure(i) => i.subscribe(mailbox).await?,
Session::Insecure(i) => i.subscribe(mailbox).await?,
}
Ok(())
}
pub async fn close(&mut self) -> ImapResult<()> {
match self {
Session::Secure(i) => i.close().await?,
Session::Insecure(i) => i.close().await?,
}
Ok(())
}
pub async fn select<S: AsRef<str>>(&mut self, mailbox_name: S) -> ImapResult<Mailbox> {
let mbox = match self {
Session::Secure(i) => i.select(mailbox_name).await?,
Session::Insecure(i) => i.select(mailbox_name).await?,
};
Ok(mbox)
}
pub async fn fetch<S1, S2>(&mut self, sequence_set: S1, query: S2) -> ImapResult<Vec<Fetch>>
where
S1: AsRef<str>,
S2: AsRef<str>,
{
let res = match self {
Session::Secure(i) => {
i.fetch(sequence_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
Session::Insecure(i) => {
i.fetch(sequence_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
};
Ok(res)
}
pub async fn uid_fetch<S1, S2>(&mut self, uid_set: S1, query: S2) -> ImapResult<Vec<Fetch>>
where
S1: AsRef<str>,
S2: AsRef<str>,
{
let res = match self {
Session::Secure(i) => {
i.uid_fetch(uid_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
Session::Insecure(i) => {
i.uid_fetch(uid_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
};
Ok(res)
}
pub async fn uid_store<S1, S2>(&mut self, uid_set: S1, query: S2) -> ImapResult<Vec<Fetch>>
where
S1: AsRef<str>,
S2: AsRef<str>,
{
let res = match self {
Session::Secure(i) => {
i.uid_store(uid_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
Session::Insecure(i) => {
i.uid_store(uid_set, query)
.await?
.collect::<ImapResult<_>>()
.await?
}
};
Ok(res)
}
pub async fn uid_mv<S1: AsRef<str>, S2: AsRef<str>>(
&mut self,
uid_set: S1,
mailbox_name: S2,
) -> ImapResult<()> {
match self {
Session::Secure(i) => i.uid_mv(uid_set, mailbox_name).await?,
Session::Insecure(i) => i.uid_mv(uid_set, mailbox_name).await?,
}
Ok(())
}
pub async fn uid_copy<S1: AsRef<str>, S2: AsRef<str>>(
&mut self,
uid_set: S1,
mailbox_name: S2,
) -> ImapResult<()> {
match self {
Session::Secure(i) => i.uid_copy(uid_set, mailbox_name).await?,
Session::Insecure(i) => i.uid_copy(uid_set, mailbox_name).await?,
}
Ok(())
pub fn idle(self) -> async_imap::extensions::idle::Handle<Box<dyn SessionStream>> {
let Session { inner } = self;
inner.idle()
}
}

View File

@@ -1,9 +1,9 @@
//! # Import/export module
use core::cmp::{max, min};
use std::path::Path;
use std::cmp::{max, min};
use num_traits::FromPrimitive;
use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use rand::{thread_rng, Rng};
use crate::blob::BlobObject;
@@ -16,8 +16,7 @@ use crate::dc_tools::*;
use crate::e2ee;
use crate::error::*;
use crate::events::Event;
use crate::job::*;
use crate::key::{self, Key};
use crate::key::{self, DcKey, Key, SignedSecretKey};
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::*;
@@ -53,13 +52,10 @@ pub enum ImexMode {
}
/// Import/export things.
/// For this purpose, the function creates a job that is executed in the IMAP-thread then;
/// this requires to call dc_perform_inbox_jobs() regularly.
///
/// What to do is defined by the *what* parameter.
///
/// While dc_imex() returns immediately, the started job may take a while,
/// you can stop it using dc_stop_ongoing_process(). During execution of the job,
/// During execution of the job,
/// some events are sent out:
///
/// - A number of #DC_EVENT_IMEX_PROGRESS events are sent and may be used to create
@@ -68,41 +64,48 @@ pub enum ImexMode {
/// - For each file written on export, the function sends #DC_EVENT_IMEX_FILE_WRITTEN
///
/// Only one import-/export-progress can run at the same time.
/// To cancel an import-/export-progress, use dc_stop_ongoing_process().
pub fn imex(context: &Context, what: ImexMode, param1: Option<impl AsRef<Path>>) {
let mut param = Params::new();
param.set_int(Param::Cmd, what as i32);
if let Some(param1) = param1 {
param.set(Param::Arg, param1.as_ref().to_string_lossy());
}
/// 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<()> {
use futures::future::FutureExt;
job_kill_action(context, Action::ImexImap);
job_add(context, Action::ImexImap, 0, param, 0);
let cancel = context.alloc_ongoing().await?;
let res = imex_inner(context, what, param1)
.race(cancel.recv().map(|_| Err(format_err!("canceled"))))
.await;
context.free_ongoing().await;
res
}
/// Returns the filename of the backup found (otherwise an error)
pub fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result<String> {
pub async fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result<String> {
let dir_name = dir_name.as_ref();
let dir_iter = std::fs::read_dir(dir_name)?;
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
let mut newest_backup_time = 0;
let mut newest_backup_path: Option<std::path::PathBuf> = None;
for dirent in dir_iter {
let mut newest_backup_path: Option<PathBuf> = None;
while let Some(dirent) = dir_iter.next().await {
if let Ok(dirent) = dirent {
let path = dirent.path();
let name = dirent.file_name();
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) {
if sql.open(context, &path, true).await {
let curr_backup_time = sql
.get_raw_config_int(context, "backup_time")
.await
.unwrap_or_default();
if curr_backup_time > newest_backup_time {
newest_backup_path = Some(path);
newest_backup_time = curr_backup_time;
}
info!(context, "backup_time of {} is {}", name, curr_backup_time);
sql.close(&context);
sql.close().await;
}
}
}
@@ -113,28 +116,32 @@ pub fn has_backup(context: &Context, dir_name: impl AsRef<Path>) -> Result<Strin
}
}
pub fn initiate_key_transfer(context: &Context) -> Result<String> {
ensure!(context.alloc_ongoing(), "could not allocate ongoing");
let res = do_initiate_key_transfer(context);
context.free_ongoing();
pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
use futures::future::FutureExt;
let cancel = context.alloc_ongoing().await?;
let res = do_initiate_key_transfer(context)
.race(cancel.recv().map(|_| Err(format_err!("canceled"))))
.await;
context.free_ongoing().await;
res
}
fn do_initiate_key_transfer(context: &Context) -> Result<String> {
async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
let mut msg: Message;
let setup_code = create_setup_code(context);
/* this may require a keypair to be created. this may take a second ... */
ensure!(!context.shall_stop_ongoing(), "canceled");
let setup_file_content = render_setup_file(context, &setup_code)?;
let setup_file_content = render_setup_file(context, &setup_code).await?;
/* encrypting may also take a while ... */
ensure!(!context.shall_stop_ongoing(), "canceled");
let setup_file_blob = BlobObject::create(
context,
"autocrypt-setup-message.html",
setup_file_content.as_bytes(),
)?;
)
.await?;
let chat_id = chat::create_by_contact_id(context, DC_CONTACT_ID_SELF)?;
let chat_id = chat::create_by_contact_id(context, DC_CONTACT_ID_SELF).await?;
msg = Message::default();
msg.viewtype = Viewtype::File;
msg.param.set(Param::File, setup_file_blob.as_name());
@@ -147,12 +154,11 @@ fn do_initiate_key_transfer(context: &Context) -> Result<String> {
ForcePlaintext::NoAutocryptHeader as i32,
);
ensure!(!context.shall_stop_ongoing(), "canceled");
let msg_id = chat::send_msg(context, chat_id, &mut msg)?;
let msg_id = chat::send_msg(context, chat_id, &mut msg).await?;
info!(context, "Wait for setup message being sent ...",);
while !context.shall_stop_ongoing() {
std::thread::sleep(std::time::Duration::from_secs(1));
if let Ok(msg) = Message::load_from_db(context, msg_id) {
while !context.shall_stop_ongoing().await {
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
if let Ok(msg) = Message::load_from_db(context, msg_id).await {
if msg.is_sent() {
info!(context, "... setup message sent.",);
break;
@@ -170,15 +176,13 @@ fn do_initiate_key_transfer(context: &Context) -> Result<String> {
/// Renders HTML body of a setup file message.
///
/// The `passphrase` must be at least 2 characters long.
pub fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> {
pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> {
ensure!(
passphrase.len() >= 2,
"Passphrase must be at least 2 chars long."
);
let self_addr = e2ee::ensure_secret_key_exists(context)?;
let private_key = Key::from_self_private(context, self_addr, &context.sql)
.ok_or_else(|| format_err!("Failed to get private key."))?;
let ac_headers = match context.get_config_bool(Config::E2eeEnabled) {
let private_key = Key::from(SignedSecretKey::load_self(context).await?);
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await {
false => None,
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
};
@@ -195,8 +199,8 @@ pub fn render_setup_file(context: &Context, passphrase: &str) -> Result<String>
);
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
let msg_subj = context.stock_str(StockMessage::AcSetupMsgSubject);
let msg_body = context.stock_str(StockMessage::AcSetupMsgBody);
let msg_subj = context.stock_str(StockMessage::AcSetupMsgSubject).await;
let msg_body = context.stock_str(StockMessage::AcSetupMsgBody).await;
let msg_body_html = msg_body.replace("\r", "").replace("\n", "<br>");
Ok(format!(
concat!(
@@ -239,8 +243,8 @@ pub fn create_setup_code(_context: &Context) -> String {
ret
}
fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
if !context.sql.get_raw_config_bool(context, "bcc_self") {
async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
if !context.sql.get_raw_config_bool(context, "bcc_self").await {
let mut msg = Message::new(Viewtype::Text);
// TODO: define this as a stockstring once the wording is settled.
msg.text = Some(
@@ -249,26 +253,30 @@ fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
go to the settings and enable \"Send copy to self\"."
.to_string(),
);
chat::add_device_msg(context, Some("bcc-self-hint"), Some(&mut msg))?;
chat::add_device_msg(context, Some("bcc-self-hint"), Some(&mut msg)).await?;
}
Ok(())
}
pub fn continue_key_transfer(context: &Context, msg_id: MsgId, setup_code: &str) -> Result<()> {
pub async fn continue_key_transfer(
context: &Context,
msg_id: MsgId,
setup_code: &str,
) -> Result<()> {
ensure!(!msg_id.is_special(), "wrong id");
let msg = Message::load_from_db(context, msg_id)?;
let msg = Message::load_from_db(context, msg_id).await?;
ensure!(
msg.is_setupmessage(),
"Message is no Autocrypt Setup Message."
);
if let Some(filename) = msg.get_file(context) {
let file = dc_open_file(context, filename)?;
let file = dc_open_file_std(context, filename)?;
let sc = normalize_setup_code(setup_code);
let armored_key = decrypt_setup_file(context, &sc, file)?;
set_self_key(context, &armored_key, true, true)?;
maybe_add_bcc_self_device_msg(context)?;
set_self_key(context, &armored_key, true, true).await?;
maybe_add_bcc_self_device_msg(context).await?;
Ok(())
} else {
@@ -276,7 +284,7 @@ pub fn continue_key_transfer(context: &Context, msg_id: MsgId, setup_code: &str)
}
}
fn set_self_key(
async fn set_self_key(
context: &Context,
armored: &str,
set_default: bool,
@@ -301,7 +309,8 @@ fn set_self_key(
};
context
.sql
.set_raw_config_int(context, "e2ee_enabled", e2ee_enabled)?;
.set_raw_config_int(context, "e2ee_enabled", e2ee_enabled)
.await?;
}
None => {
if prefer_encrypt_required {
@@ -310,7 +319,7 @@ fn set_self_key(
}
};
let self_addr = context.get_config(Config::ConfiguredAddr);
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())?;
@@ -331,7 +340,8 @@ fn set_self_key(
} else {
key::KeyPairUse::ReadOnly
},
)?;
)
.await?;
Ok(())
}
@@ -359,52 +369,50 @@ pub fn normalize_setup_code(s: &str) -> String {
out
}
#[allow(non_snake_case)]
pub fn JobImexImap(context: &Context, job: &Job) -> Result<()> {
ensure!(context.alloc_ongoing(), "could not allocate ongoing");
let what: Option<ImexMode> = job.param.get_int(Param::Cmd).and_then(ImexMode::from_i32);
let param = job.param.get(Param::Arg).unwrap_or_default();
async fn imex_inner(
context: &Context,
what: ImexMode,
param: Option<impl AsRef<Path>>,
) -> Result<()> {
ensure!(param.is_some(), "No Import/export dir/file given.");
ensure!(!param.is_empty(), "No Import/export dir/file given.");
info!(context, "Import/export process started.");
context.call_cb(Event::ImexProgress(10));
context.emit_event(Event::ImexProgress(10));
ensure!(context.sql.is_open(), "Database not opened.");
if what == Some(ImexMode::ExportBackup) || what == Some(ImexMode::ExportSelfKeys) {
ensure!(context.sql.is_open().await, "Database not opened.");
let path = param.unwrap();
if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
// before we export anything, make sure the private key exists
if e2ee::ensure_secret_key_exists(context).is_err() {
context.free_ongoing();
if e2ee::ensure_secret_key_exists(context).await.is_err() {
bail!("Cannot create private key or private key not available.");
} else {
dc_create_folder(context, &param)?;
dc_create_folder(context, &path).await?;
}
}
let path = Path::new(param);
let success = match what {
Some(ImexMode::ExportSelfKeys) => export_self_keys(context, path),
Some(ImexMode::ImportSelfKeys) => import_self_keys(context, path),
Some(ImexMode::ExportBackup) => export_backup(context, path),
Some(ImexMode::ImportBackup) => import_backup(context, path),
None => {
bail!("unknown IMEX type");
}
ImexMode::ExportSelfKeys => export_self_keys(context, path).await,
ImexMode::ImportSelfKeys => import_self_keys(context, path).await,
ImexMode::ExportBackup => export_backup(context, path).await,
ImexMode::ImportBackup => import_backup(context, path).await,
};
context.free_ongoing();
match success {
Ok(()) => {
info!(context, "IMEX successfully completed");
context.call_cb(Event::ImexProgress(1000));
context.emit_event(Event::ImexProgress(1000));
Ok(())
}
Err(err) => {
context.call_cb(Event::ImexProgress(0));
context.emit_event(Event::ImexProgress(0));
bail!("IMEX FAILED to complete: {}", err);
}
}
}
/// Import Backup
fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Result<()> {
async fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Result<()> {
info!(
context,
"Import \"{}\" to \"{}\".",
@@ -413,84 +421,93 @@ fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Resul
);
ensure!(
!context.is_configured(),
!context.is_configured().await,
"Cannot import backups to accounts in use."
);
context.sql.close(&context);
dc_delete_file(context, context.get_dbfile());
context.sql.close().await;
dc_delete_file(context, context.get_dbfile()).await;
ensure!(
!context.get_dbfile().exists(),
!context.get_dbfile().exists().await,
"Cannot delete old database."
);
ensure!(
dc_copy_file(context, backup_to_import.as_ref(), context.get_dbfile()),
dc_copy_file(context, backup_to_import.as_ref(), context.get_dbfile()).await,
"could not copy file"
);
/* error already logged */
/* re-open copied database file */
ensure!(
context.sql.open(&context, &context.get_dbfile(), false),
context
.sql
.open(&context, &context.get_dbfile(), false)
.await,
"could not re-open db"
);
delete_and_reset_all_device_msgs(&context)?;
delete_and_reset_all_device_msgs(&context).await?;
let total_files_cnt = context
.sql
.query_get_value::<_, isize>(context, "SELECT COUNT(*) FROM backup_blobs;", params![])
.query_get_value::<isize>(context, "SELECT COUNT(*) FROM backup_blobs;", paramsv![])
.await
.unwrap_or_default() as usize;
info!(
context,
"***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt,
);
let res = context.sql.query_map(
"SELECT file_name, file_content FROM backup_blobs ORDER BY id;",
params![],
|row| {
let name: String = row.get(0)?;
let blob: Vec<u8> = row.get(1)?;
let files = context
.sql
.query_map(
"SELECT file_name, file_content FROM backup_blobs ORDER BY id;",
paramsv![],
|row| {
let name: String = row.get(0)?;
let blob: Vec<u8> = row.get(1)?;
Ok((name, blob))
},
|files| {
for (processed_files_cnt, file) in files.enumerate() {
let (file_name, file_blob) = file?;
if context.shall_stop_ongoing() {
return Ok(false);
}
let mut permille = processed_files_cnt * 1000 / total_files_cnt;
if permille < 10 {
permille = 10
}
if permille > 990 {
permille = 990
}
context.call_cb(Event::ImexProgress(permille));
if file_blob.is_empty() {
continue;
}
Ok((name, blob))
},
|files| {
files
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
let path_filename = context.get_blobdir().join(file_name);
dc_write_file(context, &path_filename, &file_blob)?;
}
Ok(true)
},
);
match res {
Ok(all_files_extracted) => {
if all_files_extracted {
// only delete backup_blobs if all files were successfully extracted
sql::execute(context, &context.sql, "DROP TABLE backup_blobs;", params![])?;
sql::try_execute(context, &context.sql, "VACUUM;").ok();
Ok(())
} else {
bail!("received stop signal");
}
let mut all_files_extracted = true;
for (processed_files_cnt, (file_name, file_blob)) in files.into_iter().enumerate() {
if context.shall_stop_ongoing().await {
all_files_extracted = false;
break;
}
Err(err) => Err(err.into()),
let mut permille = processed_files_cnt * 1000 / total_files_cnt;
if permille < 10 {
permille = 10
}
if permille > 990 {
permille = 990
}
context.emit_event(Event::ImexProgress(permille));
if file_blob.is_empty() {
continue;
}
let path_filename = context.get_blobdir().join(file_name);
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![])
.await?;
context.sql.execute("VACUUM;", paramsv![]).await.ok();
Ok(())
} else {
bail!("received stop signal");
}
}
@@ -499,28 +516,31 @@ fn import_backup(context: &Context, backup_to_import: impl AsRef<Path>) -> Resul
******************************************************************************/
/* the FILE_PROGRESS macro calls the callback with the permille of files processed.
The macro avoids weird values of 0% or 100% while still working. */
fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
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)
// 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 = dc_get_next_backup_path(dir, now)?;
let dest_path_filename = dc_get_next_backup_path(dir, now).await?;
let dest_path_string = dest_path_filename.to_string_lossy().to_string();
sql::housekeeping(context);
sql::housekeeping(context).await;
sql::try_execute(context, &context.sql, "VACUUM;").ok();
context.sql.execute("VACUUM;", paramsv![]).await.ok();
// we close the database during the copy of the dbfile
context.sql.close(context);
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);
context.sql.open(&context, &context.get_dbfile(), false);
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!(
@@ -531,86 +551,90 @@ fn export_backup(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
}
let dest_sql = Sql::new();
ensure!(
dest_sql.open(context, &dest_path_filename, false),
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) {
let res = match add_files_to_export(context, &dest_sql).await {
Err(err) => {
dc_delete_file(context, &dest_path_filename);
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)?;
context.call_cb(Event::ImexFileWritten(dest_path_filename));
dest_sql
.set_raw_config_int(context, "backup_time", now as i32)
.await?;
context.emit_event(Event::ImexFileWritten(dest_path_filename));
Ok(())
}
};
dest_sql.close(context);
dest_sql.close().await;
Ok(res?)
}
fn add_files_to_export(context: &Context, sql: &Sql) -> Result<()> {
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") {
sql::execute(
context,
&sql,
if !sql.table_exists("backup_blobs").await? {
sql.execute(
"CREATE TABLE backup_blobs (id INTEGER PRIMARY KEY, file_name, file_content);",
params![],
)?
paramsv![],
)
.await?;
}
// copy all files from BLOBDIR into backup-db
let mut total_files_cnt = 0;
let dir = context.get_blobdir();
let dir_handle = std::fs::read_dir(&dir)?;
total_files_cnt += dir_handle.filter(|r| r.is_ok()).count();
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);
// scan directory, pass 2: copy files
let dir_handle = std::fs::read_dir(&dir)?;
let exported_all_files = sql.prepare(
"INSERT INTO backup_blobs (file_name, file_content) VALUES (?, ?);",
|mut stmt, _| {
let mut processed_files_cnt = 0;
for entry in dir_handle {
let entry = entry?;
if context.shall_stop_ongoing() {
return Ok(false);
}
processed_files_cnt += 1;
let permille = max(min(processed_files_cnt * 1000 / total_files_cnt, 990), 10);
context.call_cb(Event::ImexProgress(permille));
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
if name.starts_with("delta-chat") && name.ends_with(".bak") {
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(Event::ImexProgress(permille));
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
if name.starts_with("delta-chat") && name.ends_with(".bak") {
continue;
}
info!(context, "EXPORT: copying filename={}", name);
let curr_path_filename = context.get_blobdir().join(entry.file_name());
if let Ok(buf) = dc_read_file(context, &curr_path_filename).await {
if buf.is_empty() {
continue;
}
info!(context, "EXPORT: copying filename={}", name);
let curr_path_filename = context.get_blobdir().join(entry.file_name());
if let Ok(buf) = dc_read_file(context, &curr_path_filename) {
if buf.is_empty() {
continue;
}
// bail out if we can't insert
stmt.execute(params![name, buf])?;
}
// 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(true)
},
)?;
ensure!(exported_all_files, "canceled during export-files");
}
Ok(())
})
.await?;
Ok(())
}
/*******************************************************************************
* Classic key import
******************************************************************************/
fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
async fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
/* hint: even if we switch to import Autocrypt Setup Files, we should leave the possibility to import
plain ASC keys, at least keys without a password, if we do not want to implement a password entry function.
Importing ASC keys is useful to use keys in Delta Chat used by any other non-Autocrypt-PGP implementation.
@@ -621,8 +645,8 @@ fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
let mut imported_cnt = 0;
let dir_name = dir.as_ref().to_string_lossy();
let dir_handle = std::fs::read_dir(&dir)?;
for entry in dir_handle {
let mut dir_handle = async_std::fs::read_dir(&dir).await?;
while let Some(entry) = dir_handle.next().await {
let entry_fn = entry?.file_name();
let name_f = entry_fn.to_string_lossy();
let path_plus_name = dir.as_ref().join(&entry_fn);
@@ -642,10 +666,10 @@ fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
continue;
}
}
match dc_read_file(context, &path_plus_name) {
match dc_read_file(context, &path_plus_name).await {
Ok(buf) => {
let armored = std::string::String::from_utf8_lossy(&buf);
if let Err(err) = set_self_key(context, &armored, set_default, false) {
if let Err(err) = set_self_key(context, &armored, set_default, false).await {
error!(context, "set_self_key: {}", err);
continue;
}
@@ -662,45 +686,54 @@ fn import_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
Ok(())
}
fn export_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;
context.sql.query_map(
"SELECT id, public_key, private_key, is_default FROM keypairs;",
params![],
|row| {
let id = row.get(0)?;
let public_key_blob: Vec<u8> = row.get(1)?;
let public_key = Key::from_slice(&public_key_blob, KeyType::Public);
let private_key_blob: Vec<u8> = row.get(2)?;
let private_key = Key::from_slice(&private_key_blob, KeyType::Private);
let is_default: i32 = row.get(3)?;
let keys = context
.sql
.query_map(
"SELECT id, public_key, private_key, is_default FROM keypairs;",
paramsv![],
|row| {
let id = row.get(0)?;
let public_key_blob: Vec<u8> = row.get(1)?;
let public_key = Key::from_slice(&public_key_blob, KeyType::Public);
let private_key_blob: Vec<u8> = row.get(2)?;
let private_key = Key::from_slice(&private_key_blob, KeyType::Private);
let is_default: i32 = row.get(3)?;
Ok((id, public_key, private_key, is_default))
},
|keys| {
for key_pair in keys {
let (id, public_key, private_key, is_default) = key_pair?;
let id = Some(id).filter(|_| is_default != 0);
if let Some(key) = public_key {
if export_key_to_asc_file(context, &dir, id, &key).is_err() {
export_errors += 1;
}
} else {
export_errors += 1;
}
if let Some(key) = private_key {
if export_key_to_asc_file(context, &dir, id, &key).is_err() {
export_errors += 1;
}
} else {
export_errors += 1;
}
Ok((id, public_key, private_key, is_default))
},
|keys| {
keys.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
for (id, public_key, private_key, is_default) in keys {
let id = Some(id).filter(|_| is_default != 0);
if let Some(key) = public_key {
if export_key_to_asc_file(context, &dir, id, &key)
.await
.is_err()
{
export_errors += 1;
}
Ok(())
},
)?;
} else {
export_errors += 1;
}
if let Some(key) = private_key {
if export_key_to_asc_file(context, &dir, id, &key)
.await
.is_err()
{
export_errors += 1;
}
} else {
export_errors += 1;
}
}
ensure!(export_errors == 0, "errors while exporting keys");
Ok(())
@@ -709,7 +742,7 @@ fn export_self_keys(context: &Context, dir: impl AsRef<Path>) -> Result<()> {
/*******************************************************************************
* Classic key export
******************************************************************************/
fn export_key_to_asc_file(
async fn export_key_to_asc_file(
context: &Context,
dir: impl AsRef<Path>,
id: Option<i64>,
@@ -722,13 +755,13 @@ fn export_key_to_asc_file(
dir.as_ref().join(format!("{}-key-{}.asc", kind, &id))
};
info!(context, "Exporting key {}", file_name.display());
dc_delete_file(context, &file_name);
dc_delete_file(context, &file_name).await;
let res = key.write_asc_to_file(&file_name, context);
let res = key.write_asc_to_file(&file_name, context).await;
if res.is_err() {
error!(context, "Cannot write key to {}", file_name.display());
} else {
context.call_cb(Event::ImexFileWritten(file_name));
context.emit_event(Event::ImexFileWritten(file_name));
}
res
}
@@ -740,12 +773,12 @@ mod tests {
use crate::test_utils::*;
use ::pgp::armor::BlockType;
#[test]
fn test_render_setup_file() {
let t = test_context(Some(Box::new(logging_cb)));
#[async_std::test]
async fn test_render_setup_file() {
let t = test_context().await;
configure_alice_keypair(&t.ctx);
let msg = render_setup_file(&t.ctx, "hello").unwrap();
configure_alice_keypair(&t.ctx).await;
let msg = render_setup_file(&t.ctx, "hello").await.unwrap();
println!("{}", &msg);
// Check some substrings, indicating things got substituted.
// In particular note the mixing of `\r\n` and `\n` depending
@@ -756,25 +789,25 @@ mod tests {
assert!(msg.contains("-----BEGIN PGP MESSAGE-----\r\n"));
assert!(msg.contains("Passphrase-Format: numeric9x4\r\n"));
assert!(msg.contains("Passphrase-Begin: he\n"));
assert!(msg.contains("==\n"));
assert!(msg.contains("-----END PGP MESSAGE-----\n"));
}
#[test]
fn test_render_setup_file_newline_replace() {
let t = dummy_context();
#[async_std::test]
async fn test_render_setup_file_newline_replace() {
let t = dummy_context().await;
t.ctx
.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
.await
.unwrap();
configure_alice_keypair(&t.ctx);
let msg = render_setup_file(&t.ctx, "pw").unwrap();
configure_alice_keypair(&t.ctx).await;
let msg = render_setup_file(&t.ctx, "pw").await.unwrap();
println!("{}", &msg);
assert!(msg.contains("<p>hello<br>there</p>"));
}
#[test]
fn test_create_setup_code() {
let t = dummy_context();
#[async_std::test]
async fn test_create_setup_code() {
let t = dummy_context().await;
let setupcode = create_setup_code(&t.ctx);
assert_eq!(setupcode.len(), 44);
assert_eq!(setupcode.chars().nth(4).unwrap(), '-');
@@ -787,15 +820,17 @@ mod tests {
assert_eq!(setupcode.chars().nth(39).unwrap(), '-');
}
#[test]
fn test_export_key_to_asc_file() {
let context = dummy_context();
#[async_std::test]
async fn test_export_key_to_asc_file() {
let context = dummy_context().await;
let key = Key::from(alice_keypair().public);
let blobdir = "$BLOBDIR";
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key).is_ok());
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!("{}/public-key-default.asc", blobdir);
let bytes = std::fs::read(&filename).unwrap();
let bytes = async_std::fs::read(&filename).await.unwrap();
assert_eq!(bytes, key.to_asc(None).into_bytes());
}
@@ -815,9 +850,9 @@ mod tests {
const S_EM_SETUPCODE: &str = "1742-0185-6197-1303-7016-8412-3581-4441-0597";
const S_EM_SETUPFILE: &str = include_str!("../test-data/message/stress.txt");
#[test]
fn test_split_and_decrypt() {
let ctx = dummy_context();
#[async_std::test]
async fn test_split_and_decrypt() {
let ctx = dummy_context().await;
let context = &ctx.ctx;
let buf_1 = S_EM_SETUPFILE.as_bytes().to_vec();

1293
src/job.rs

File diff suppressed because it is too large Load Diff

View File

@@ -1,207 +0,0 @@
use std::sync::{Arc, Condvar, Mutex};
use crate::context::Context;
use crate::error::{Error, Result};
use crate::imap::Imap;
#[derive(Debug)]
pub struct JobThread {
pub name: &'static str,
pub folder_config_name: &'static str,
pub imap: Imap,
pub state: Arc<(Mutex<JobState>, Condvar)>,
}
#[derive(Clone, Debug, Default)]
pub struct JobState {
idle: bool,
jobs_needed: bool,
suspended: bool,
using_handle: bool,
}
impl JobThread {
pub fn new(name: &'static str, folder_config_name: &'static str, imap: Imap) -> Self {
JobThread {
name,
folder_config_name,
imap,
state: Arc::new((Mutex::new(Default::default()), Condvar::new())),
}
}
pub fn suspend(&self, context: &Context) {
info!(context, "Suspending {}-thread.", self.name,);
{
self.state.0.lock().unwrap().suspended = true;
}
self.interrupt_idle(context);
loop {
let using_handle = self.state.0.lock().unwrap().using_handle;
if !using_handle {
return;
}
std::thread::sleep(std::time::Duration::from_micros(300 * 1000));
}
}
pub fn unsuspend(&self, context: &Context) {
info!(context, "Unsuspending {}-thread.", self.name);
let &(ref lock, ref cvar) = &*self.state.clone();
let mut state = lock.lock().unwrap();
state.suspended = false;
state.idle = true;
cvar.notify_one();
}
pub fn interrupt_idle(&self, context: &Context) {
{
self.state.0.lock().unwrap().jobs_needed = true;
}
info!(context, "Interrupting {}-IDLE...", self.name);
self.imap.interrupt_idle(context);
let &(ref lock, ref cvar) = &*self.state.clone();
let mut state = lock.lock().unwrap();
state.idle = true;
cvar.notify_one();
info!(context, "Interrupting {}-IDLE... finished", self.name);
}
pub async fn fetch(&mut self, context: &Context, use_network: bool) {
{
let &(ref lock, _) = &*self.state.clone();
let mut state = lock.lock().unwrap();
if state.suspended {
return;
}
state.using_handle = true;
}
if use_network {
if let Err(err) = self.connect_and_fetch(context).await {
warn!(context, "connect+fetch failed: {}, reconnect & retry", err);
self.imap.trigger_reconnect();
if let Err(err) = self.connect_and_fetch(context).await {
warn!(context, "connect+fetch failed: {}", err);
}
}
}
self.state.0.lock().unwrap().using_handle = false;
}
async fn connect_and_fetch(&mut self, context: &Context) -> Result<()> {
let prefix = format!("{}-fetch", self.name);
match self.imap.connect_configured(context) {
Ok(()) => {
if let Some(watch_folder) = self.get_watch_folder(context) {
let start = std::time::Instant::now();
info!(context, "{} started...", prefix);
let res = self
.imap
.fetch(context, &watch_folder)
.await
.map_err(Into::into);
let elapsed = start.elapsed().as_millis();
info!(context, "{} done in {:.3} ms.", prefix, elapsed);
res
} else {
Err(Error::WatchFolderNotFound("not-set".to_string()))
}
}
Err(err) => Err(crate::error::Error::Message(err.to_string())),
}
}
fn get_watch_folder(&self, context: &Context) -> Option<String> {
match context.sql.get_raw_config(context, self.folder_config_name) {
Some(name) => Some(name),
None => {
if self.folder_config_name == "configured_inbox_folder" {
// initialized with old version, so has not set configured_inbox_folder
Some("INBOX".to_string())
} else {
None
}
}
}
}
pub fn idle(&self, context: &Context, use_network: bool) {
{
let &(ref lock, ref cvar) = &*self.state.clone();
let mut state = lock.lock().unwrap();
if state.jobs_needed {
info!(
context,
"{}-IDLE will not be started as it was interrupted while not idling.",
self.name,
);
state.jobs_needed = false;
return;
}
if state.suspended {
while !state.idle {
state = cvar.wait(state).unwrap();
}
state.idle = false;
return;
}
state.using_handle = true;
if !use_network {
state.using_handle = false;
while !state.idle {
state = cvar.wait(state).unwrap();
}
state.idle = false;
return;
}
}
let prefix = format!("{}-IDLE", self.name);
let do_fake_idle = match self.imap.connect_configured(context) {
Ok(()) => {
if !self.imap.can_idle() {
true // we have to do fake_idle
} else {
let watch_folder = self.get_watch_folder(context);
info!(context, "{} started...", prefix);
let res = self.imap.idle(context, watch_folder);
info!(context, "{} ended...", prefix);
if let Err(err) = res {
warn!(context, "{} failed: {} -> reconnecting", prefix, err);
// something is borked, let's start afresh on the next occassion
self.imap.disconnect(context);
}
false
}
}
Err(err) => {
info!(context, "{}-IDLE connection fail: {:?}", self.name, err);
// if the connection fails, use fake_idle to retry periodically
// fake_idle() will be woken up by interrupt_idle() as
// well so will act on maybe_network events
true
}
};
if do_fake_idle {
let watch_folder = self.get_watch_folder(context);
self.imap.fake_idle(context, watch_folder);
}
self.state.0.lock().unwrap().using_handle = false;
}
}

View File

@@ -2,40 +2,42 @@
use std::collections::BTreeMap;
use std::io::Cursor;
use std::path::Path;
use async_std::path::Path;
use async_trait::async_trait;
use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
use pgp::ser::Serialize;
use pgp::types::{KeyTrait, SecretKeyTrait};
use crate::config::Config;
use crate::constants::*;
use crate::context::Context;
use crate::dc_tools::*;
use crate::sql::Sql;
use crate::dc_tools::{dc_write_file, time, EmailAddress, InvalidEmailError};
use crate::sql;
// Re-export key types
pub use crate::pgp::KeyPair;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
/// Error type for deltachat key handling.
#[derive(Fail, Debug)]
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[fail(display = "Could not decode base64")]
Base64Decode(#[cause] base64::DecodeError, failure::Backtrace),
#[fail(display = "rPGP error: {}", _0)]
PgpError(#[cause] pgp::errors::Error, failure::Backtrace),
}
impl From<base64::DecodeError> for Error {
fn from(err: base64::DecodeError) -> Error {
Error::Base64Decode(err, failure::Backtrace::new())
}
}
impl From<pgp::errors::Error> for Error {
fn from(err: pgp::errors::Error) -> Error {
Error::PgpError(err, failure::Backtrace::new())
}
#[error("Could not decode base64")]
Base64Decode(#[from] base64::DecodeError),
#[error("rPGP error: {}", _0)]
Pgp(#[from] pgp::errors::Error),
#[error("Failed to generate PGP key: {}", _0)]
Keygen(#[from] crate::pgp::PgpKeygenError),
#[error("Failed to load key: {}", _0)]
LoadKey(#[from] sql::Error),
#[error("Failed to save generated key: {}", _0)]
StoreKey(#[from] SaveKeyError),
#[error("No address configured")]
NoConfiguredAddr,
#[error("Configured address is invalid: {}", _0)]
InvalidConfiguredAddr(#[from] InvalidEmailError),
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -45,6 +47,7 @@ pub type Result<T> = std::result::Result<T, Error>;
/// This trait is implemented for rPGP's [SignedPublicKey] and
/// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world.
#[async_trait]
pub trait DcKey: Serialize + Deserializable {
type KeyType: Serialize + Deserializable;
@@ -63,6 +66,9 @@ pub trait DcKey: Serialize + Deserializable {
Self::from_slice(&bytes)
}
/// Load the users' default key from the database.
async fn load_self(context: &Context) -> Result<Self::KeyType>;
/// Serialise the key to a base64 string.
fn to_base64(&self) -> String {
// Not using Serialize::to_bytes() to make clear *why* it is
@@ -75,12 +81,108 @@ pub trait DcKey: Serialize + Deserializable {
}
}
#[async_trait]
impl DcKey for SignedPublicKey {
type KeyType = SignedPublicKey;
async fn load_self(context: &Context) -> Result<Self::KeyType> {
match context
.sql
.query_row(
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
{
Ok(bytes) => Self::from_slice(&bytes),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
let keypair = generate_keypair(context).await?;
Ok(keypair.public)
}
Err(err) => Err(err.into()),
}
}
}
#[async_trait]
impl DcKey for SignedSecretKey {
type KeyType = SignedSecretKey;
async fn load_self(context: &Context) -> Result<Self::KeyType> {
match context
.sql
.query_row(
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
{
Ok(bytes) => Self::from_slice(&bytes),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
let keypair = generate_keypair(context).await?;
Ok(keypair.secret)
}
Err(err) => Err(err.into()),
}
}
}
async fn generate_keypair(context: &Context) -> Result<KeyPair> {
let addr = context
.get_config(Config::ConfiguredAddr)
.await
.ok_or_else(|| 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#"
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)?)),
)
.await
{
Ok((pub_bytes, sec_bytes)) => Ok(KeyPair {
addr,
public: SignedPublicKey::from_slice(&pub_bytes)?,
secret: SignedSecretKey::from_slice(&sec_bytes)?,
}),
Err(sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => {
let start = std::time::Instant::now();
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await)
.unwrap_or_default();
info!(context, "Generating keypair with type {}", keytype);
let keypair = crate::pgp::create_keypair(addr, keytype)?;
store_self_keypair(context, &keypair, KeyPairUse::Default).await?;
info!(
context,
"Keypair generated in {:.3}s.",
start.elapsed().as_secs()
);
Ok(keypair)
}
Err(err) => Err(err.into()),
}
}
/// Cryptographic key
@@ -197,34 +299,6 @@ impl Key {
}
}
pub fn from_self_public(
context: &Context,
self_addr: impl AsRef<str>,
sql: &Sql,
) -> Option<Self> {
let addr = self_addr.as_ref();
sql.query_get_value(
context,
"SELECT public_key FROM keypairs WHERE addr=? AND is_default=1;",
&[addr],
)
.and_then(|blob: Vec<u8>| Self::from_slice(&blob, KeyType::Public))
}
pub fn from_self_private(
context: &Context,
self_addr: impl AsRef<str>,
sql: &Sql,
) -> Option<Self> {
sql.query_get_value(
context,
"SELECT private_key FROM keypairs WHERE addr=? AND is_default=1;",
&[self_addr.as_ref()],
)
.and_then(|blob: Vec<u8>| Self::from_slice(&blob, KeyType::Private))
}
pub fn to_bytes(&self) -> Vec<u8> {
match self {
Key::Public(k) => k.to_bytes().unwrap_or_default(),
@@ -266,14 +340,14 @@ impl Key {
.expect("failed to serialize key")
}
pub fn write_asc_to_file(
pub async fn write_asc_to_file(
&self,
file: impl AsRef<Path>,
context: &Context,
) -> std::io::Result<()> {
let file_content = self.to_asc(None).into_bytes();
let res = dc_write_file(context, &file, &file_content);
let res = dc_write_file(context, &file, &file_content).await;
if res.is_err() {
error!(context, "Cannot write key to {}", file.as_ref().display());
}
@@ -316,21 +390,19 @@ pub enum KeyPairUse {
}
/// Error saving a keypair to the database.
#[derive(Fail, Debug)]
#[fail(display = "SaveKeyError: {}", message)]
#[derive(Debug, thiserror::Error)]
#[error("SaveKeyError: {message}")]
pub struct SaveKeyError {
message: String,
#[cause]
cause: failure::Error,
backtrace: failure::Backtrace,
#[source]
cause: anyhow::Error,
}
impl SaveKeyError {
fn new(message: impl Into<String>, cause: impl Into<failure::Error>) -> Self {
fn new(message: impl Into<String>, cause: impl Into<anyhow::Error>) -> Self {
Self {
message: message.into(),
cause: cause.into(),
backtrace: failure::Backtrace::new(),
}
}
}
@@ -347,7 +419,7 @@ impl SaveKeyError {
/// same key again overwrites it.
///
/// [Config::ConfiguredAddr]: crate::config::Config::ConfiguredAddr
pub fn store_self_keypair(
pub async fn store_self_keypair(
context: &Context,
keypair: &KeyPair,
default: KeyPairUse,
@@ -366,34 +438,37 @@ pub fn store_self_keypair(
.sql
.execute(
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
params![public_key, secret_key],
paramsv![public_key, secret_key],
)
.await
.map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?;
if default == KeyPairUse::Default {
context
.sql
.execute("UPDATE keypairs SET is_default=0;", params![])
.execute("UPDATE keypairs SET is_default=0;", paramsv![])
.await
.map_err(|err| SaveKeyError::new("failed to clear default", err))?;
}
let is_default = match default {
KeyPairUse::Default => true,
KeyPairUse::ReadOnly => false,
KeyPairUse::Default => true as i32,
KeyPairUse::ReadOnly => false as i32,
};
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)
VALUES (?,?,?,?,?);",
params![
keypair.addr.to_string(),
is_default as i32,
public_key,
secret_key,
time()
],
params,
)
.map(|_| ())
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))
.await
.map_err(|err| SaveKeyError::new("failed to insert keypair", err))?;
Ok(())
}
/// Make a fingerprint human-readable, in hex format.
@@ -428,6 +503,7 @@ mod tests {
use crate::test_utils::*;
use std::convert::TryFrom;
use async_std::sync::Arc;
use lazy_static::lazy_static;
lazy_static! {
@@ -553,6 +629,63 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
}
}
#[async_std::test]
async fn test_load_self_existing() {
let alice = alice_keypair();
let t = dummy_context().await;
configure_alice_keypair(&t.ctx).await;
let pubkey = SignedPublicKey::load_self(&t.ctx).await.unwrap();
assert_eq!(alice.public, pubkey);
let seckey = SignedSecretKey::load_self(&t.ctx).await.unwrap();
assert_eq!(alice.secret, seckey);
}
#[async_std::test]
#[ignore] // generating keys is expensive
async fn test_load_self_generate_public() {
let t = dummy_context().await;
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
.unwrap();
let key = SignedPublicKey::load_self(&t.ctx).await;
assert!(key.is_ok());
}
#[async_std::test]
#[ignore] // generating keys is expensive
async fn test_load_self_generate_secret() {
let t = dummy_context().await;
t.ctx
.set_config(Config::ConfiguredAddr, Some("alice@example.com"))
.await
.unwrap();
let key = SignedSecretKey::load_self(&t.ctx).await;
assert!(key.is_ok());
}
#[async_std::test]
#[ignore] // generating keys is expensive
async fn test_load_self_generate_concurrent() {
use std::thread;
let t = dummy_context().await;
t.ctx
.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 res0 = thr0.join().unwrap();
let res1 = thr1.join().unwrap();
assert_eq!(res0.unwrap(), res1.unwrap());
}
#[test]
fn test_ascii_roundtrip() {
let public_key = Key::from(KEYPAIR.public.clone());
@@ -578,22 +711,29 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
assert_eq!(public.primary_key, KEYPAIR.public.primary_key);
}
#[test]
fn test_save_self_key_twice() {
#[async_std::test]
async fn test_save_self_key_twice() {
// Saving the same key twice should result in only one row in
// the keypairs table.
let t = dummy_context();
let nrows = || {
t.ctx
.sql
.query_get_value::<_, u32>(&t.ctx, "SELECT COUNT(*) FROM keypairs;", params![])
let t = dummy_context().await;
let ctx = Arc::new(t.ctx);
let ctx1 = ctx.clone();
let nrows = || async {
ctx1.sql
.query_get_value::<u32>(&ctx1, "SELECT COUNT(*) FROM keypairs;", paramsv![])
.await
.unwrap()
};
assert_eq!(nrows(), 0);
store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap();
assert_eq!(nrows(), 1);
store_self_keypair(&t.ctx, &KEYPAIR, KeyPairUse::Default).unwrap();
assert_eq!(nrows(), 1);
assert_eq!(nrows().await, 0);
store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default)
.await
.unwrap();
assert_eq!(nrows().await, 1);
store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default)
.await
.unwrap();
assert_eq!(nrows().await, 1);
}
// Convenient way to create a new key if you need one, run with

View File

@@ -27,7 +27,7 @@ impl<'a> Keyring<'a> {
&self.keys
}
pub fn load_self_private_for_decrypting(
pub async fn load_self_private_for_decrypting(
&mut self,
context: &Context,
self_addr: impl AsRef<str>,
@@ -36,8 +36,9 @@ impl<'a> Keyring<'a> {
sql.query_get_value(
context,
"SELECT private_key FROM keypairs ORDER BY addr=? DESC, is_default DESC;",
&[self_addr.as_ref()],
paramsv![self_addr.as_ref().to_string()],
)
.await
.and_then(|blob: Vec<u8>| Key::from_slice(&blob, KeyType::Private))
.map(|key| self.add_owned(key))
.is_some()

View File

@@ -2,8 +2,6 @@
#![deny(clippy::correctness, missing_debug_implementations, clippy::all)]
#![allow(clippy::match_bool)]
#[macro_use]
extern crate failure_derive;
#[macro_use]
extern crate num_derive;
#[macro_use]
@@ -16,11 +14,22 @@ extern crate strum_macros;
#[macro_use]
extern crate debug_stub_derive;
pub trait ToSql: rusqlite::ToSql + Send + Sync {}
impl<T: rusqlite::ToSql + Send + Sync> ToSql for T {}
#[macro_use]
pub mod log;
#[macro_use]
pub mod error;
#[cfg(feature = "internals")]
#[macro_use]
pub mod sql;
#[cfg(not(feature = "internals"))]
#[macro_use]
mod sql;
pub mod headerdef;
pub(crate) mod events;
@@ -38,9 +47,9 @@ pub mod context;
mod e2ee;
mod imap;
pub mod imex;
mod scheduler;
#[macro_use]
pub mod job;
mod job_thread;
pub mod key;
mod keyring;
pub mod location;
@@ -58,7 +67,6 @@ pub mod qr;
pub mod securejoin;
mod simplify;
mod smtp;
pub mod sql;
pub mod stock;
mod token;
#[macro_use]

View File

@@ -1,7 +1,6 @@
//! Location handling
use bitflags::bitflags;
use quick_xml;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use crate::chat::{self, ChatId};
@@ -9,13 +8,12 @@ use crate::config::Config;
use crate::constants::*;
use crate::context::*;
use crate::dc_tools::*;
use crate::error::Error;
use crate::error::{ensure, Error};
use crate::events::Event;
use crate::job::{self, job_action_exists, job_add, Job};
use crate::job::{self, Job};
use crate::message::{Message, MsgId};
use crate::mimeparser::SystemMessage;
use crate::param::*;
use crate::sql;
use crate::stock::StockMessage;
/// Location record
@@ -123,9 +121,9 @@ impl Kml {
}
} else if self.tag.contains(KmlTag::COORDINATES) {
let parts = val.splitn(2, ',').collect::<Vec<_>>();
if parts.len() == 2 {
self.curr.longitude = parts[0].parse().unwrap_or_default();
self.curr.latitude = parts[1].parse().unwrap_or_default();
if let [longitude, latitude] = &parts[..] {
self.curr.longitude = longitude.parse().unwrap_or_default();
self.curr.latitude = latitude.parse().unwrap_or_default();
}
}
}
@@ -192,91 +190,103 @@ impl Kml {
}
// location streaming
pub fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: i64) {
pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: i64) {
let now = time();
if !(seconds < 0 || chat_id.is_special()) {
let is_sending_locations_before = is_sending_locations_to_chat(context, chat_id);
if sql::execute(
context,
&context.sql,
"UPDATE chats \
let is_sending_locations_before = is_sending_locations_to_chat(context, chat_id).await;
if context
.sql
.execute(
"UPDATE chats \
SET locations_send_begin=?, \
locations_send_until=? \
WHERE id=?",
params![
if 0 != seconds { now } else { 0 },
if 0 != seconds { now + seconds } else { 0 },
chat_id,
],
)
.is_ok()
paramsv![
if 0 != seconds { now } else { 0 },
if 0 != seconds { now + seconds } else { 0 },
chat_id,
],
)
.await
.is_ok()
{
if 0 != seconds && !is_sending_locations_before {
let mut msg = Message::new(Viewtype::Text);
msg.text =
Some(context.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0));
msg.param.set_cmd(SystemMessage::LocationStreamingEnabled);
chat::send_msg(context, chat_id, &mut msg).unwrap_or_default();
} else if 0 == seconds && is_sending_locations_before {
let stock_str =
context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
chat::add_info_msg(context, chat_id, stock_str);
}
context.call_cb(Event::ChatModified(chat_id));
if 0 != seconds {
schedule_MAYBE_SEND_LOCATIONS(context, false);
job_add(
context,
job::Action::MaybeSendLocationsEnded,
chat_id.to_u32() as i32,
Params::new(),
seconds + 1,
msg.text = Some(
context
.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0)
.await,
);
msg.param.set_cmd(SystemMessage::LocationStreamingEnabled);
chat::send_msg(context, chat_id, &mut msg)
.await
.unwrap_or_default();
} else if 0 == seconds && is_sending_locations_before {
let stock_str = context
.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0)
.await;
chat::add_info_msg(context, chat_id, stock_str).await;
}
context.emit_event(Event::ChatModified(chat_id));
if 0 != seconds {
schedule_maybe_send_locations(context, false).await;
job::add(
context,
job::Job::new(
job::Action::MaybeSendLocationsEnded,
chat_id.to_u32(),
Params::new(),
seconds + 1,
),
)
.await;
}
}
}
}
#[allow(non_snake_case)]
fn schedule_MAYBE_SEND_LOCATIONS(context: &Context, force_schedule: bool) {
if force_schedule || !job_action_exists(context, job::Action::MaybeSendLocations) {
job_add(
async fn schedule_maybe_send_locations(context: &Context, force_schedule: bool) {
if force_schedule || !job::action_exists(context, job::Action::MaybeSendLocations).await {
job::add(
context,
job::Action::MaybeSendLocations,
0,
Params::new(),
60,
);
job::Job::new(job::Action::MaybeSendLocations, 0, Params::new(), 60),
)
.await;
};
}
pub fn is_sending_locations_to_chat(context: &Context, chat_id: ChatId) -> bool {
pub async fn is_sending_locations_to_chat(context: &Context, chat_id: ChatId) -> bool {
context
.sql
.exists(
"SELECT id FROM chats WHERE (? OR id=?) AND locations_send_until>?;",
params![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()],
paramsv![if chat_id.is_unset() { 1 } else { 0 }, chat_id, time()],
)
.await
.unwrap_or_default()
}
pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
if latitude == 0.0 && longitude == 0.0 {
return true;
}
let mut continue_streaming = false;
if let Ok(chats) = context.sql.query_map(
"SELECT id FROM chats WHERE locations_send_until>?;",
params![time()],
|row| row.get::<_, i32>(0),
|chats| chats.collect::<Result<Vec<_>, _>>().map_err(Into::into),
) {
if let Ok(chats) = context
.sql
.query_map(
"SELECT id FROM chats WHERE locations_send_until>?;",
paramsv![time()],
|row| row.get::<_, i32>(0),
|chats| chats.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await
{
for chat_id in chats {
if let Err(err) = context.sql.execute(
"INSERT INTO locations \
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
params![
paramsv![
latitude,
longitude,
accuracy,
@@ -284,22 +294,22 @@ pub fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> b
chat_id,
DC_CONTACT_ID_SELF,
]
) {
).await {
warn!(context, "failed to store location {:?}", err);
} else {
continue_streaming = true;
}
}
if continue_streaming {
context.call_cb(Event::LocationChanged(Some(DC_CONTACT_ID_SELF)));
context.emit_event(Event::LocationChanged(Some(DC_CONTACT_ID_SELF)));
};
schedule_MAYBE_SEND_LOCATIONS(context, false);
schedule_maybe_send_locations(context, false).await;
}
continue_streaming
}
pub fn get_range(
pub async fn get_range(
context: &Context,
chat_id: ChatId,
contact_id: u32,
@@ -318,7 +328,7 @@ pub fn get_range(
AND (? OR l.from_id=?) \
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
params![
paramsv![
if chat_id.is_unset() { 1 } else { 0 },
chat_id,
if contact_id == 0 { 1 } else { 0 },
@@ -357,6 +367,7 @@ pub fn get_range(
Ok(ret)
},
)
.await
.unwrap_or_default()
}
@@ -365,28 +376,33 @@ fn is_marker(txt: &str) -> bool {
}
/// Deletes all locations from the database.
pub fn delete_all(context: &Context) -> Result<(), Error> {
sql::execute(context, &context.sql, "DELETE FROM locations;", params![])?;
context.call_cb(Event::LocationChanged(None));
pub async fn delete_all(context: &Context) -> Result<(), Error> {
context
.sql
.execute("DELETE FROM locations;", paramsv![])
.await?;
context.emit_event(Event::LocationChanged(None));
Ok(())
}
pub fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Error> {
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Error> {
let mut last_added_location_id = 0;
let self_addr = context
.get_config(Config::ConfiguredAddr)
.await
.unwrap_or_default();
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
params![chat_id], |row| {
paramsv![chat_id], |row| {
let send_begin: i64 = row.get(0)?;
let send_until: i64 = row.get(1)?;
let last_sent: i64 = row.get(2)?;
Ok((send_begin, send_until, last_sent))
})?;
})
.await?;
let now = time();
let mut location_count = 0;
@@ -405,7 +421,7 @@ pub fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Erro
AND independent=0 \
GROUP BY timestamp \
ORDER BY timestamp;",
params![DC_CONTACT_ID_SELF, locations_send_begin, locations_last_sent, DC_CONTACT_ID_SELF],
paramsv![DC_CONTACT_ID_SELF, locations_send_begin, locations_last_sent, DC_CONTACT_ID_SELF],
|row| {
let location_id: i32 = row.get(0)?;
let latitude: f64 = row.get(1)?;
@@ -430,7 +446,7 @@ pub fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32), Erro
}
Ok(())
}
)?;
).await?;
ret += "</Document>\n</kml>";
}
@@ -463,37 +479,38 @@ pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String
)
}
pub fn set_kml_sent_timestamp(
pub async fn set_kml_sent_timestamp(
context: &Context,
chat_id: ChatId,
timestamp: i64,
) -> Result<(), Error> {
sql::execute(
context,
&context.sql,
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
params![timestamp, chat_id],
)?;
context
.sql
.execute(
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
paramsv![timestamp, chat_id],
)
.await?;
Ok(())
}
pub fn set_msg_location_id(
pub async fn set_msg_location_id(
context: &Context,
msg_id: MsgId,
location_id: u32,
) -> Result<(), Error> {
sql::execute(
context,
&context.sql,
"UPDATE msgs SET location_id=? WHERE id=?;",
params![location_id, msg_id],
)?;
context
.sql
.execute(
"UPDATE msgs SET location_id=? WHERE id=?;",
paramsv![location_id, msg_id],
)
.await?;
Ok(())
}
pub fn save(
pub async fn save(
context: &Context,
chat_id: ChatId,
contact_id: u32,
@@ -501,54 +518,66 @@ pub fn save(
independent: bool,
) -> Result<u32, Error> {
ensure!(!chat_id.is_special(), "Invalid chat id");
context
.sql
.prepare2(
"SELECT id FROM locations WHERE timestamp=? AND from_id=?",
"INSERT INTO locations\
let mut newest_timestamp = 0;
let mut newest_location_id = 0;
for location in locations {
let &Location {
timestamp,
latitude,
longitude,
accuracy,
..
} = location;
context
.sql
.with_conn(move |mut conn| {
let mut stmt_test = conn
.prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
let mut stmt_insert = conn.prepare_cached(
"INSERT INTO locations\
(timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \
VALUES (?,?,?,?,?,?,?);",
|mut stmt_test, mut stmt_insert, conn| {
let mut newest_timestamp = 0;
let mut newest_location_id = 0;
)?;
for location in locations {
let exists =
stmt_test.exists(params![location.timestamp, contact_id as i32])?;
let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?;
if independent || !exists {
stmt_insert.execute(params![
location.timestamp,
if independent || !exists {
stmt_insert.execute(paramsv![
timestamp,
contact_id as i32,
chat_id,
latitude,
longitude,
accuracy,
independent,
])?;
if timestamp > newest_timestamp {
// okay to drop, as we use cached prepared statements
drop(stmt_test);
drop(stmt_insert);
newest_timestamp = timestamp;
newest_location_id = crate::sql::get_rowid2(
&mut conn,
"locations",
"timestamp",
timestamp,
"from_id",
contact_id as i32,
chat_id,
location.latitude,
location.longitude,
location.accuracy,
independent,
])?;
if location.timestamp > newest_timestamp {
newest_timestamp = location.timestamp;
newest_location_id = sql::get_rowid2_with_conn(
context,
conn,
"locations",
"timestamp",
location.timestamp,
"from_id",
contact_id as i32,
);
}
)?;
}
}
Ok(newest_location_id)
},
)
.map_err(Into::into)
Ok(())
})
.await?;
}
Ok(newest_location_id)
}
#[allow(non_snake_case)]
pub(crate) fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Status {
pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> job::Status {
let now = time();
let mut continue_streaming = false;
info!(
@@ -556,101 +585,118 @@ pub(crate) fn JobMaybeSendLocations(context: &Context, _job: &Job) -> job::Statu
" ----------------- MAYBE_SEND_LOCATIONS -------------- ",
);
if let Ok(rows) = context.sql.query_map(
"SELECT id, locations_send_begin, locations_last_sent \
let rows = context
.sql
.query_map(
"SELECT id, locations_send_begin, locations_last_sent \
FROM chats \
WHERE locations_send_until>?;",
params![now],
|row| {
let chat_id: ChatId = row.get(0)?;
let locations_send_begin: i64 = row.get(1)?;
let locations_last_sent: i64 = row.get(2)?;
continue_streaming = true;
paramsv![now],
|row| {
let chat_id: ChatId = row.get(0)?;
let locations_send_begin: i64 = row.get(1)?;
let locations_last_sent: i64 = row.get(2)?;
continue_streaming = true;
// be a bit tolerant as the timer may not align exactly with time(NULL)
if now - locations_last_sent < (60 - 3) {
Ok(None)
} else {
Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
}
},
|rows| {
rows.filter_map(|v| v.transpose())
.collect::<Result<Vec<_>, _>>()
.map_err(Into::into)
},
) {
// be a bit tolerant as the timer may not align exactly with time(NULL)
if now - locations_last_sent < (60 - 3) {
Ok(None)
} else {
Ok(Some((chat_id, locations_send_begin, locations_last_sent)))
}
},
|rows| {
rows.filter_map(|v| v.transpose())
.collect::<Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await;
if rows.is_ok() {
let msgs = context
.sql
.prepare(
"SELECT id \
.with_conn(move |conn| {
let rows = rows.unwrap();
let mut stmt_locations = conn.prepare_cached(
"SELECT id \
FROM locations \
WHERE from_id=? \
AND timestamp>=? \
AND timestamp>? \
AND independent=0 \
ORDER BY timestamp;",
|mut stmt_locations, _| {
let msgs = rows
.into_iter()
.filter_map(|(chat_id, locations_send_begin, locations_last_sent)| {
if !stmt_locations
.exists(params![
DC_CONTACT_ID_SELF,
locations_send_begin,
locations_last_sent,
])
.unwrap_or_default()
{
// if there is no new location, there's nothing to send.
// however, maybe we want to bypass this test eg. 15 minutes
None
} else {
// pending locations are attached automatically to every message,
// so also to this empty text message.
// DC_CMD_LOCATION is only needed to create a nicer subject.
//
// for optimisation and to avoid flooding the sending queue,
// we could sending these messages only if we're really online.
// the easiest way to determine this, is to check for an empty message queue.
// (might not be 100%, however, as positions are sent combined later
// and dc_set_location() is typically called periodically, this is ok)
let mut msg = Message::new(Viewtype::Text);
msg.hidden = true;
msg.param.set_cmd(SystemMessage::LocationOnly);
Some((chat_id, msg))
}
})
.collect::<Vec<_>>();
Ok(msgs)
},
)
.unwrap_or_default(); // TODO: Better error handling
)?;
let mut msgs = Vec::new();
for (chat_id, locations_send_begin, locations_last_sent) in &rows {
if !stmt_locations
.exists(paramsv![
DC_CONTACT_ID_SELF,
*locations_send_begin,
*locations_last_sent,
])
.unwrap_or_default()
{
// if there is no new location, there's nothing to send.
// however, maybe we want to bypass this test eg. 15 minutes
} else {
// pending locations are attached automatically to every message,
// so also to this empty text message.
// DC_CMD_LOCATION is only needed to create a nicer subject.
//
// for optimisation and to avoid flooding the sending queue,
// we could sending these messages only if we're really online.
// the easiest way to determine this, is to check for an empty message queue.
// (might not be 100%, however, as positions are sent combined later
// and dc_set_location() is typically called periodically, this is ok)
let mut msg = Message::new(Viewtype::Text);
msg.hidden = true;
msg.param.set_cmd(SystemMessage::LocationOnly);
msgs.push((*chat_id, msg));
}
}
Ok(msgs)
})
.await
.unwrap_or_default();
for (chat_id, mut msg) in msgs.into_iter() {
// TODO: better error handling
chat::send_msg(context, chat_id, &mut msg).unwrap_or_default();
chat::send_msg(context, chat_id, &mut msg)
.await
.unwrap_or_default();
}
}
if continue_streaming {
schedule_MAYBE_SEND_LOCATIONS(context, true);
schedule_maybe_send_locations(context, true).await;
}
job::Status::Finished(Ok(()))
}
#[allow(non_snake_case)]
pub(crate) fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) -> job::Status {
pub(crate) async fn job_maybe_send_locations_ended(
context: &Context,
job: &mut Job,
) -> job::Status {
// this function is called when location-streaming _might_ have ended for a chat.
// the function checks, if location-streaming is really ended;
// if so, a device-message is added if not yet done.
let chat_id = ChatId::new(job.foreign_id);
let (send_begin, send_until) = job_try!(context.sql.query_row(
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
params![chat_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
));
let (send_begin, send_until) = job_try!(
context
.sql
.query_row(
"SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?",
paramsv![chat_id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)),
)
.await
);
if !(send_begin != 0 && time() <= send_until) {
// still streaming -
@@ -660,12 +706,14 @@ pub(crate) fn JobMaybeSendLocationsEnded(context: &Context, job: &mut Job) -> jo
// not streaming, device-message already sent
job_try!(context.sql.execute(
"UPDATE chats SET locations_send_begin=0, locations_send_until=0 WHERE id=?",
params![chat_id],
));
paramsv![chat_id],
).await);
let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
chat::add_info_msg(context, chat_id, stock_str);
context.call_cb(Event::ChatModified(chat_id));
let stock_str = context
.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0)
.await;
chat::add_info_msg(context, chat_id, stock_str).await;
context.emit_event(Event::ChatModified(chat_id));
}
}
job::Status::Finished(Ok(()))
@@ -676,9 +724,9 @@ mod tests {
use super::*;
use crate::test_utils::dummy_context;
#[test]
fn test_kml_parse() {
let context = dummy_context();
#[async_std::test]
async fn test_kml_parse() {
let context = dummy_context().await;
let xml =
b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n<Document addr=\"user@example.org\">\n<Placemark><Timestamp><when>2019-03-06T21:09:57Z</when></Timestamp><Point><coordinates accuracy=\"32.000000\">9.423110,53.790302</coordinates></Point></Placemark>\n<PlaceMARK>\n<Timestamp><WHEN > \n\t2018-12-13T22:11:12Z\t</WHEN></Timestamp><Point><coordinates aCCuracy=\"2.500000\"> 19.423110 \t , \n 63.790302\n </coordinates></Point></PlaceMARK>\n</Document>\n</kml>";

View File

@@ -7,9 +7,7 @@ macro_rules! info {
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
let thread = ::std::thread::current();
let full = format!("{thid:?} {file}:{line}: {msg}",
thid = thread.id(),
let full = format!("{file}:{line}: {msg}",
file = file!(),
line = line!(),
msg = &formatted);
@@ -24,9 +22,7 @@ macro_rules! warn {
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
let thread = ::std::thread::current();
let full = format!("{thid:?} {file}:{line}: {msg}",
thid = thread.id(),
let full = format!("{file}:{line}: {msg}",
file = file!(),
line = line!(),
msg = &formatted);
@@ -48,6 +44,6 @@ macro_rules! error {
#[macro_export]
macro_rules! emit_event {
($ctx:expr, $event:expr) => {
$ctx.call_cb($event);
$ctx.emit_event($event);
};
}

View File

@@ -50,59 +50,69 @@ impl LoginParam {
}
/// Read the login parameters from the database.
pub fn from_database(context: &Context, prefix: impl AsRef<str>) -> Self {
pub async fn from_database(context: &Context, prefix: impl AsRef<str>) -> Self {
let prefix = prefix.as_ref();
let sql = &context.sql;
let key = format!("{}addr", prefix);
let addr = sql
.get_raw_config(context, key)
.await
.unwrap_or_default()
.trim()
.to_string();
let key = format!("{}mail_server", prefix);
let mail_server = sql.get_raw_config(context, key).unwrap_or_default();
let mail_server = sql.get_raw_config(context, key).await.unwrap_or_default();
let key = format!("{}mail_port", prefix);
let mail_port = sql.get_raw_config_int(context, key).unwrap_or_default();
let mail_port = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let key = format!("{}mail_user", prefix);
let mail_user = sql.get_raw_config(context, key).unwrap_or_default();
let mail_user = sql.get_raw_config(context, key).await.unwrap_or_default();
let key = format!("{}mail_pw", prefix);
let mail_pw = sql.get_raw_config(context, key).unwrap_or_default();
let mail_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
let key = format!("{}imap_certificate_checks", prefix);
let imap_certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(context, key) {
if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
} else {
Default::default()
};
let key = format!("{}send_server", prefix);
let send_server = sql.get_raw_config(context, key).unwrap_or_default();
let send_server = sql.get_raw_config(context, key).await.unwrap_or_default();
let key = format!("{}send_port", prefix);
let send_port = sql.get_raw_config_int(context, key).unwrap_or_default();
let send_port = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
let key = format!("{}send_user", prefix);
let send_user = sql.get_raw_config(context, key).unwrap_or_default();
let send_user = sql.get_raw_config(context, key).await.unwrap_or_default();
let key = format!("{}send_pw", prefix);
let send_pw = sql.get_raw_config(context, key).unwrap_or_default();
let send_pw = sql.get_raw_config(context, key).await.unwrap_or_default();
let key = format!("{}smtp_certificate_checks", prefix);
let smtp_certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(context, key) {
if let Some(certificate_checks) = sql.get_raw_config_int(context, key).await {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
} else {
Default::default()
};
let key = format!("{}server_flags", prefix);
let server_flags = sql.get_raw_config_int(context, key).unwrap_or_default();
let server_flags = sql
.get_raw_config_int(context, key)
.await
.unwrap_or_default();
LoginParam {
addr,
@@ -125,7 +135,7 @@ impl LoginParam {
}
/// Save this loginparam to the database.
pub fn save_to_database(
pub async fn save_to_database(
&self,
context: &Context,
prefix: impl AsRef<str>,
@@ -134,40 +144,49 @@ impl LoginParam {
let sql = &context.sql;
let key = format!("{}addr", prefix);
sql.set_raw_config(context, key, Some(&self.addr))?;
sql.set_raw_config(context, key, Some(&self.addr)).await?;
let key = format!("{}mail_server", prefix);
sql.set_raw_config(context, key, Some(&self.mail_server))?;
sql.set_raw_config(context, key, Some(&self.mail_server))
.await?;
let key = format!("{}mail_port", prefix);
sql.set_raw_config_int(context, key, self.mail_port)?;
sql.set_raw_config_int(context, key, self.mail_port).await?;
let key = format!("{}mail_user", prefix);
sql.set_raw_config(context, key, Some(&self.mail_user))?;
sql.set_raw_config(context, key, Some(&self.mail_user))
.await?;
let key = format!("{}mail_pw", prefix);
sql.set_raw_config(context, key, Some(&self.mail_pw))?;
sql.set_raw_config(context, key, Some(&self.mail_pw))
.await?;
let key = format!("{}imap_certificate_checks", prefix);
sql.set_raw_config_int(context, key, self.imap_certificate_checks as i32)?;
sql.set_raw_config_int(context, key, self.imap_certificate_checks as i32)
.await?;
let key = format!("{}send_server", prefix);
sql.set_raw_config(context, key, Some(&self.send_server))?;
sql.set_raw_config(context, key, Some(&self.send_server))
.await?;
let key = format!("{}send_port", prefix);
sql.set_raw_config_int(context, key, self.send_port)?;
sql.set_raw_config_int(context, key, self.send_port).await?;
let key = format!("{}send_user", prefix);
sql.set_raw_config(context, key, Some(&self.send_user))?;
sql.set_raw_config(context, key, Some(&self.send_user))
.await?;
let key = format!("{}send_pw", prefix);
sql.set_raw_config(context, key, Some(&self.send_pw))?;
sql.set_raw_config(context, key, Some(&self.send_pw))
.await?;
let key = format!("{}smtp_certificate_checks", prefix);
sql.set_raw_config_int(context, key, self.smtp_certificate_checks as i32)?;
sql.set_raw_config_int(context, key, self.smtp_certificate_checks as i32)
.await?;
let key = format!("{}server_flags", prefix);
sql.set_raw_config_int(context, key, self.server_flags)?;
sql.set_raw_config_int(context, key, self.server_flags)
.await?;
Ok(())
}

View File

@@ -40,11 +40,11 @@ impl Lot {
}
pub fn get_text1(&self) -> Option<&str> {
self.text1.as_ref().map(|s| s.as_str())
self.text1.as_deref()
}
pub fn get_text2(&self) -> Option<&str> {
self.text2.as_ref().map(|s| s.as_str())
self.text2.as_deref()
}
pub fn get_text1_meaning(&self) -> Meaning {

File diff suppressed because it is too large Load Diff

View File

@@ -9,12 +9,13 @@ use crate::contact::*;
use crate::context::{get_version_str, Context};
use crate::dc_tools::*;
use crate::e2ee::*;
use crate::error::Error;
use crate::error::{bail, ensure, format_err, Error};
use crate::location;
use crate::message::{self, Message};
use crate::mimeparser::SystemMessage;
use crate::param::*;
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::simplify::escape_message_footer_marks;
use crate::stock::StockMessage;
// attachments of 25 mb brutto should work on the majority of providers
@@ -65,89 +66,89 @@ pub struct RenderedEmail {
}
impl<'a, 'b> MimeFactory<'a, 'b> {
pub fn from_msg(
pub async fn from_msg(
context: &'a Context,
msg: &'b Message,
attach_selfavatar: bool,
) -> Result<MimeFactory<'a, 'b>, Error> {
let chat = Chat::load_from_db(context, msg.chat_id)?;
let chat = Chat::load_from_db(context, msg.chat_id).await?;
let from_addr = context
.get_config(Config::ConfiguredAddr)
.await
.unwrap_or_default();
let from_displayname = context
.get_config(Config::Displayname)
.await
.unwrap_or_default();
let from_displayname = context.get_config(Config::Displayname).unwrap_or_default();
let mut recipients = Vec::with_capacity(5);
let mut req_mdn = false;
if chat.is_self_talk() {
recipients.push((from_displayname.to_string(), from_addr.to_string()));
} else {
context.sql.query_map(
"SELECT c.authname, c.addr \
context
.sql
.query_map(
"SELECT c.authname, c.addr \
FROM chats_contacts cc \
LEFT JOIN contacts c ON cc.contact_id=c.id \
WHERE cc.chat_id=? AND cc.contact_id>9;",
params![msg.chat_id],
|row| {
let authname: String = row.get(0)?;
let addr: String = row.get(1)?;
Ok((authname, addr))
},
|rows| {
for row in rows {
let (authname, addr) = row?;
if !recipients_contain_addr(&recipients, &addr) {
recipients.push((authname, addr));
paramsv![msg.chat_id],
|row| {
let authname: String = row.get(0)?;
let addr: String = row.get(1)?;
Ok((authname, addr))
},
|rows| {
for row in rows {
let (authname, addr) = row?;
if !recipients_contain_addr(&recipients, &addr) {
recipients.push((authname, addr));
}
}
}
Ok(())
},
)?;
Ok(())
},
)
.await?;
let command = msg.param.get_cmd();
/* for added members, the list is just fine */
if command == SystemMessage::MemberRemovedFromGroup {
let email_to_remove = msg.param.get(Param::Arg).unwrap_or_default();
let self_addr = context
.get_config(Config::ConfiguredAddr)
.unwrap_or_default();
if !email_to_remove.is_empty()
&& !addr_cmp(email_to_remove, self_addr)
&& !recipients_contain_addr(&recipients, &email_to_remove)
{
recipients.push(("".to_string(), email_to_remove.to_string()));
}
}
if command != SystemMessage::AutocryptSetupMessage
&& command != SystemMessage::SecurejoinMessage
&& context.get_config_bool(Config::MdnsEnabled)
&& context.get_config_bool(Config::MdnsEnabled).await
{
req_mdn = true;
}
}
let (in_reply_to, references) = context.sql.query_row(
"SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?",
params![msg.id],
|row| {
let in_reply_to: String = row.get(0)?;
let references: String = row.get(1)?;
let (in_reply_to, references) = context
.sql
.query_row(
"SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?",
paramsv![msg.id],
|row| {
let in_reply_to: String = row.get(0)?;
let references: String = row.get(1)?;
Ok((
render_rfc724_mid_list(&in_reply_to),
render_rfc724_mid_list(&references),
))
},
)?;
Ok((
render_rfc724_mid_list(&in_reply_to),
render_rfc724_mid_list(&references),
))
},
)
.await?;
let default_str = context
.stock_str(StockMessage::StatusLine)
.await
.to_string();
let factory = MimeFactory {
from_addr,
from_displayname,
selfstatus: context
.get_config(Config::Selfstatus)
.unwrap_or_else(|| context.stock_str(StockMessage::StatusLine).to_string()),
.await
.unwrap_or_else(|| default_str),
recipients,
timestamp: msg.timestamp_sort,
loaded: Loaded::Message { chat },
@@ -162,29 +163,42 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
Ok(factory)
}
pub fn from_mdn(
pub async fn from_mdn(
context: &'a Context,
msg: &'b Message,
additional_msg_ids: Vec<String>,
) -> Result<Self, Error> {
) -> Result<MimeFactory<'a, 'b>, Error> {
ensure!(!msg.chat_id.is_special(), "Invalid chat id");
let contact = Contact::load_from_db(context, msg.from_id)?;
let contact = Contact::load_from_db(context, msg.from_id).await?;
let from_addr = context
.get_config(Config::ConfiguredAddr)
.await
.unwrap_or_default();
let from_displayname = context
.get_config(Config::Displayname)
.await
.unwrap_or_default();
let default_str = context
.stock_str(StockMessage::StatusLine)
.await
.to_string();
let selfstatus = context
.get_config(Config::Selfstatus)
.await
.unwrap_or_else(|| default_str);
let timestamp = dc_create_smeared_timestamp(context).await;
Ok(MimeFactory {
let res = MimeFactory::<'a, 'b> {
context,
from_addr: context
.get_config(Config::ConfiguredAddr)
.unwrap_or_default(),
from_displayname: context.get_config(Config::Displayname).unwrap_or_default(),
selfstatus: context
.get_config(Config::Selfstatus)
.unwrap_or_else(|| context.stock_str(StockMessage::StatusLine).to_string()),
from_addr,
from_displayname,
selfstatus,
recipients: vec![(
contact.get_authname().to_string(),
contact.get_addr().to_string(),
)],
timestamp: dc_create_smeared_timestamp(context),
timestamp,
loaded: Loaded::MDN { additional_msg_ids },
msg,
in_reply_to: String::default(),
@@ -192,26 +206,31 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
req_mdn: false,
last_added_location_id: 0,
attach_selfavatar: false,
})
};
Ok(res)
}
fn peerstates_for_recipients(&self) -> Result<Vec<(Option<Peerstate>, &str)>, Error> {
async fn peerstates_for_recipients(&self) -> Result<Vec<(Option<Peerstate<'_>>, &str)>, Error> {
let self_addr = self
.context
.get_config(Config::ConfiguredAddr)
.await
.ok_or_else(|| format_err!("Not configured"))?;
Ok(self
let mut res = Vec::new();
for (_, addr) in self
.recipients
.iter()
.filter(|(_, addr)| addr != &self_addr)
.map(|(_, addr)| {
(
Peerstate::from_addr(self.context, &self.context.sql, addr),
addr.as_str(),
)
})
.collect())
{
res.push((
Peerstate::from_addr(self.context, addr).await,
addr.as_str(),
));
}
Ok(res)
}
fn is_e2ee_guaranteed(&self) -> bool {
@@ -271,11 +290,11 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
}
fn should_do_gossip(&self) -> bool {
async fn should_do_gossip(&self) -> bool {
match &self.loaded {
Loaded::Message { chat } => {
// beside key- and member-changes, force re-gossip every 48 hours
let gossiped_timestamp = chat.get_gossiped_timestamp(self.context);
let gossiped_timestamp = chat.get_gossiped_timestamp(self.context).await;
if time() > gossiped_timestamp + (2 * 24 * 60 * 60) {
return true;
}
@@ -316,12 +335,13 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
}
fn subject_str(&self) -> String {
async fn subject_str(&self) -> String {
match self.loaded {
Loaded::Message { ref chat } => {
if self.msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
self.context
.stock_str(StockMessage::AcSetupMsgSubject)
.await
.into_owned()
} else if chat.typ == Chattype::Group || chat.typ == Chattype::VerifiedGroup {
let re = if self.in_reply_to.is_empty() {
@@ -337,12 +357,17 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
&self.msg.param,
32,
self.context,
);
)
.await;
let raw_subject = raw.lines().next().unwrap_or_default();
format!("Chat: {}", raw_subject)
}
}
Loaded::MDN { .. } => self.context.stock_str(StockMessage::ReadRcpt).into_owned(),
Loaded::MDN { .. } => self
.context
.stock_str(StockMessage::ReadRcpt)
.await
.into_owned(),
}
}
@@ -353,7 +378,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
.collect()
}
pub fn render(mut self) -> Result<RenderedEmail, Error> {
pub async fn render(mut self) -> Result<RenderedEmail, Error> {
// Headers that are encrypted
// - Chat-*, except Chat-Version
// - Secure-Join*
@@ -368,7 +393,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
self.from_addr.clone(),
);
let mut to = Vec::with_capacity(self.recipients.len());
let mut to = Vec::new();
for (name, addr) in self.recipients.iter() {
if name.is_empty() {
to.push(Address::new_mailbox(addr.clone()));
@@ -380,6 +405,10 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
}
if to.is_empty() {
to.push(from.clone());
}
if !self.references.is_empty() {
unprotected_headers.push(Header::new("References".into(), self.references.clone()));
}
@@ -432,17 +461,18 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let min_verified = self.min_verified();
let grpimage = self.grpimage();
let force_plaintext = self.should_force_plaintext();
let subject_str = self.subject_str();
let subject_str = self.subject_str().await;
let e2ee_guaranteed = self.is_e2ee_guaranteed();
let mut encrypt_helper = EncryptHelper::new(self.context)?;
let mut encrypt_helper = EncryptHelper::new(self.context).await?;
let subject = encode_words(&subject_str);
let mut message = match self.loaded {
Loaded::Message { .. } => {
self.render_message(&mut protected_headers, &mut unprotected_headers, &grpimage)?
self.render_message(&mut protected_headers, &mut unprotected_headers, &grpimage)
.await?
}
Loaded::MDN { .. } => self.render_mdn()?,
Loaded::MDN { .. } => self.render_mdn().await?,
};
if force_plaintext != ForcePlaintext::NoAutocryptHeader as i32 {
@@ -453,7 +483,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
protected_headers.push(Header::new("Subject".into(), subject));
let peerstates = self.peerstates_for_recipients()?;
let peerstates = self.peerstates_for_recipients().await?;
let should_encrypt =
encrypt_helper.should_encrypt(self.context, e2ee_guaranteed, &peerstates)?;
let is_encrypted = should_encrypt && force_plaintext == 0;
@@ -481,7 +511,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let outer_message = if is_encrypted {
// Add gossip headers in chats with multiple recipients
if peerstates.len() > 1 && self.should_do_gossip() {
if peerstates.len() > 1 && self.should_do_gossip().await {
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
if peerstate.peek_key(min_verified).is_some() {
if let Some(header) = peerstate.render_gossip_header(min_verified) {
@@ -529,8 +559,9 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
println!("{}", raw_message);
}
let encrypted =
encrypt_helper.encrypt(self.context, min_verified, message, &peerstates)?;
let encrypted = encrypt_helper
.encrypt(self.context, min_verified, message, &peerstates)
.await?;
outer_message = outer_message
.child(
@@ -602,9 +633,9 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
Some(part)
}
fn get_location_kml_part(&mut self) -> Result<PartBuilder, Error> {
async fn get_location_kml_part(&mut self) -> Result<PartBuilder, Error> {
let (kml_content, last_added_location_id) =
location::get_kml(self.context, self.msg.chat_id)?;
location::get_kml(self.context, self.msg.chat_id).await?;
let part = PartBuilder::new()
.content_type(
&"application/vnd.google-earth.kml+xml"
@@ -624,7 +655,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
#[allow(clippy::cognitive_complexity)]
fn render_message(
async fn render_message(
&mut self,
protected_headers: &mut Vec<Header>,
unprotected_headers: &mut Vec<Header>,
@@ -719,6 +750,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
placeholdertext = Some(
self.context
.stock_str(StockMessage::AcSetupMsgBody)
.await
.to_string(),
);
}
@@ -765,7 +797,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
meta.viewtype = Viewtype::Image;
meta.param.set(Param::File, grpimage);
let (mail, filename_as_sent) = build_body_file(context, &meta, "group-image")?;
let (mail, filename_as_sent) = build_body_file(context, &meta, "group-image").await?;
meta_part = Some(mail);
protected_headers.push(Header::new("Chat-Group-Avatar".into(), filename_as_sent));
}
@@ -818,7 +850,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
let message_text = format!(
"{}{}{}{}{}",
fwdhint.unwrap_or_default(),
&final_text,
escape_message_footer_marks(final_text),
if !final_text.is_empty() && !footer.is_empty() {
"\r\n\r\n"
} else {
@@ -836,13 +868,13 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
// add attachment part
if chat::msgtype_has_file(self.msg.viewtype) {
if !is_file_size_okay(context, &self.msg) {
if !is_file_size_okay(context, &self.msg).await {
bail!(
"Message exceeds the recommended {} MB.",
RECOMMENDED_FILE_SIZE / 1_000_000,
);
} else {
let (file_part, _) = build_body_file(context, &self.msg, "")?;
let (file_part, _) = build_body_file(context, &self.msg, "").await?;
parts.push(file_part);
}
}
@@ -855,8 +887,8 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
parts.push(msg_kml_part);
}
if location::is_sending_locations_to_chat(context, self.msg.chat_id) {
match self.get_location_kml_part() {
if location::is_sending_locations_to_chat(context, self.msg.chat_id).await {
match self.get_location_kml_part().await {
Ok(part) => parts.push(part),
Err(err) => {
warn!(context, "mimefactory: could not send location: {}", err);
@@ -865,7 +897,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
if self.attach_selfavatar {
match context.get_config(Config::Selfavatar) {
match context.get_config(Config::Selfavatar).await {
Some(path) => match build_selfavatar_file(context, &path) {
Ok((part, filename)) => {
parts.push(part);
@@ -892,7 +924,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
}
/// Render an MDN
fn render_mdn(&mut self) -> Result<PartBuilder, Error> {
async fn render_mdn(&mut self) -> Result<PartBuilder, Error> {
// RFC 6522, this also requires the `report-type` parameter which is equal
// to the MIME subtype of the second body part of the multipart/report
//
@@ -927,13 +959,15 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
{
self.context
.stock_str(StockMessage::EncryptedMsg)
.await
.into_owned()
} else {
self.msg.get_summarytext(self.context, 32)
self.msg.get_summarytext(self.context, 32).await
};
let p2 = self
.context
.stock_string_repl_str(StockMessage::ReadRcptMailBody, p1);
.stock_string_repl_str(StockMessage::ReadRcptMailBody, p1)
.await;
let message_text = format!("{}\r\n", p2);
message = message.child(
PartBuilder::new()
@@ -990,14 +1024,15 @@ fn wrapped_base64_encode(buf: &[u8]) -> String {
.join("\r\n")
}
fn build_body_file(
async fn build_body_file(
context: &Context,
msg: &Message,
base_name: &str,
) -> Result<(PartBuilder, String), Error> {
let blob = msg
.param
.get_blob(Param::File, context, true)?
.get_blob(Param::File, context, true)
.await?
.ok_or_else(|| format_err!("msg has no filename"))?;
let suffix = blob.suffix().unwrap_or("dat");
@@ -1093,10 +1128,10 @@ fn recipients_contain_addr(recipients: &[(String, String)], addr: &str) -> bool
.any(|(_, cur)| cur.to_lowercase() == addr_lc)
}
fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
async fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
match msg.param.get_path(Param::File, context).unwrap_or(None) {
Some(path) => {
let bytes = dc_get_filebytes(context, &path);
let bytes = dc_get_filebytes(context, &path).await;
bytes <= UPPER_LIMIT_FILE_SIZE
}
None => false,
@@ -1106,7 +1141,7 @@ fn is_file_size_okay(context: &Context, msg: &Message) -> bool {
fn render_rfc724_mid(rfc724_mid: &str) -> String {
let rfc724_mid = rfc724_mid.trim().to_string();
if rfc724_mid.chars().nth(0).unwrap_or_default() == '<' {
if rfc724_mid.chars().next().unwrap_or_default() == '<' {
rfc724_mid
} else {
format!("<{}>", rfc724_mid)

File diff suppressed because it is too large Load Diff

View File

@@ -48,7 +48,7 @@ struct Response {
scope: Option<String>,
}
pub fn dc_get_oauth2_url(
pub async fn dc_get_oauth2_url(
context: &Context,
addr: impl AsRef<str>,
redirect_uri: impl AsRef<str>,
@@ -61,6 +61,7 @@ pub fn dc_get_oauth2_url(
"oauth2_pending_redirect_uri",
Some(redirect_uri.as_ref()),
)
.await
.is_err()
{
return None;
@@ -74,21 +75,21 @@ pub fn dc_get_oauth2_url(
}
}
// The following function may block due http-requests;
// must not be called from the main thread or by the ui!
pub fn dc_get_oauth2_access_token(
pub async fn dc_get_oauth2_access_token(
context: &Context,
addr: impl AsRef<str>,
code: impl AsRef<str>,
regenerate: bool,
) -> Option<String> {
if let Some(oauth2) = Oauth2::from_address(addr) {
let lock = context.oauth2_critical.clone();
let _l = lock.lock().unwrap();
let lock = context.oauth2_mutex.lock().await;
// read generated token
if !regenerate && !is_expired(context) {
let access_token = context.sql.get_raw_config(context, "oauth2_access_token");
if !regenerate && !is_expired(context).await {
let access_token = context
.sql
.get_raw_config(context, "oauth2_access_token")
.await;
if access_token.is_some() {
// success
return access_token;
@@ -96,10 +97,14 @@ pub fn dc_get_oauth2_access_token(
}
// generate new token: build & call auth url
let refresh_token = context.sql.get_raw_config(context, "oauth2_refresh_token");
let refresh_token = context
.sql
.get_raw_config(context, "oauth2_refresh_token")
.await;
let refresh_token_for = context
.sql
.get_raw_config(context, "oauth2_refresh_token_for")
.await
.unwrap_or_else(|| "unset".into());
let (redirect_uri, token_url, update_redirect_uri_on_success) =
@@ -109,6 +114,7 @@ pub fn dc_get_oauth2_access_token(
context
.sql
.get_raw_config(context, "oauth2_pending_redirect_uri")
.await
.unwrap_or_else(|| "unset".into()),
oauth2.init_token,
true,
@@ -122,6 +128,7 @@ pub fn dc_get_oauth2_access_token(
context
.sql
.get_raw_config(context, "oauth2_redirect_uri")
.await
.unwrap_or_else(|| "unset".into()),
oauth2.refresh_token,
false,
@@ -154,10 +161,7 @@ pub fn dc_get_oauth2_access_token(
}
// ... and POST
let response = reqwest::blocking::Client::new()
.post(post_url)
.form(&post_param)
.send();
let response = surf::post(post_url).body_form(&post_param);
if response.is_err() {
warn!(
context,
@@ -165,19 +169,8 @@ pub fn dc_get_oauth2_access_token(
);
return None;
}
let response = response.unwrap();
if !response.status().is_success() {
warn!(
context,
"Unsuccessful response when calling OAuth2 at {}: {:?}",
token_url,
response.status()
);
return None;
}
// generate new token: parse returned json
let parsed: reqwest::Result<Response> = response.json();
let parsed: Result<Response, _> = response.unwrap().recv_json().await;
if parsed.is_err() {
warn!(
context,
@@ -185,7 +178,6 @@ pub fn dc_get_oauth2_access_token(
);
return None;
}
println!("response: {:?}", &parsed);
// update refresh_token if given, typically on the first round, but we update it later as well.
let response = parsed.unwrap();
@@ -193,10 +185,12 @@ pub fn dc_get_oauth2_access_token(
context
.sql
.set_raw_config(context, "oauth2_refresh_token", Some(token))
.await
.ok();
context
.sql
.set_raw_config(context, "oauth2_refresh_token_for", Some(code.as_ref()))
.await
.ok();
}
@@ -206,6 +200,7 @@ pub fn dc_get_oauth2_access_token(
context
.sql
.set_raw_config(context, "oauth2_access_token", Some(token))
.await
.ok();
let expires_in = response
.expires_in
@@ -215,18 +210,22 @@ pub fn dc_get_oauth2_access_token(
context
.sql
.set_raw_config_int64(context, "oauth2_timestamp_expires", expires_in)
.await
.ok();
if update_redirect_uri_on_success {
context
.sql
.set_raw_config(context, "oauth2_redirect_uri", Some(redirect_uri.as_ref()))
.await
.ok();
}
} else {
warn!(context, "Failed to find OAuth2 access token");
}
drop(lock);
response.access_token
} else {
warn!(context, "Internal OAuth2 error: 2");
@@ -235,7 +234,7 @@ pub fn dc_get_oauth2_access_token(
}
}
pub fn dc_get_oauth2_addr(
pub async fn dc_get_oauth2_addr(
context: &Context,
addr: impl AsRef<str>,
code: impl AsRef<str>,
@@ -244,13 +243,14 @@ pub fn dc_get_oauth2_addr(
oauth2.get_userinfo?;
if let Some(access_token) =
dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false)
dc_get_oauth2_access_token(context, addr.as_ref(), code.as_ref(), false).await
{
let addr_out = oauth2.get_addr(context, access_token);
let addr_out = oauth2.get_addr(context, access_token).await;
if addr_out.is_none() {
// regenerate
if let Some(access_token) = dc_get_oauth2_access_token(context, addr, code, true) {
oauth2.get_addr(context, access_token)
if let Some(access_token) = dc_get_oauth2_access_token(context, addr, code, true).await
{
oauth2.get_addr(context, access_token).await
} else {
None
}
@@ -280,7 +280,7 @@ impl Oauth2 {
}
}
fn get_addr(&self, context: &Context, access_token: impl AsRef<str>) -> Option<String> {
async fn get_addr(&self, context: &Context, access_token: impl AsRef<str>) -> Option<String> {
let userinfo_url = self.get_userinfo.unwrap_or_else(|| "");
let userinfo_url = replace_in_uri(&userinfo_url, "$ACCESS_TOKEN", access_token);
@@ -291,50 +291,35 @@ impl Oauth2 {
// "verified_email": true,
// "picture": "https://lh4.googleusercontent.com/-Gj5jh_9R0BY/AAAAAAAAAAI/AAAAAAAAAAA/IAjtjfjtjNA/photo.jpg"
// }
let response = reqwest::blocking::Client::new().get(&userinfo_url).send();
let response: Result<HashMap<String, serde_json::Value>, surf::Error> =
surf::get(userinfo_url).recv_json().await;
if response.is_err() {
warn!(context, "Error getting userinfo: {:?}", response);
return None;
}
let response = response.unwrap();
if !response.status().is_success() {
warn!(context, "Error getting userinfo: {:?}", response.status());
return None;
}
let parsed: reqwest::Result<HashMap<String, serde_json::Value>> = response.json();
if parsed.is_err() {
warn!(
context,
"Failed to parse userinfo JSON response: {:?}", parsed
);
return None;
}
if let Ok(response) = parsed {
// CAVE: serde_json::Value.as_str() removes the quotes of json-strings
// but serde_json::Value.to_string() does not!
if let Some(addr) = response.get("email") {
if let Some(s) = addr.as_str() {
Some(s.to_string())
} else {
warn!(context, "E-mail in userinfo is not a string: {}", addr);
None
}
let parsed = response.unwrap();
// CAVE: serde_json::Value.as_str() removes the quotes of json-strings
// but serde_json::Value.to_string() does not!
if let Some(addr) = parsed.get("email") {
if let Some(s) = addr.as_str() {
Some(s.to_string())
} else {
warn!(context, "E-mail missing in userinfo.");
warn!(context, "E-mail in userinfo is not a string: {}", addr);
None
}
} else {
warn!(context, "Failed to parse userinfo.");
warn!(context, "E-mail missing in userinfo.");
None
}
}
}
fn is_expired(context: &Context) -> bool {
async fn is_expired(context: &Context) -> bool {
let expire_timestamp = context
.sql
.get_raw_config_int64(context, "oauth2_timestamp_expires")
.await
.unwrap_or_default();
if expire_timestamp <= 0 {
@@ -393,32 +378,32 @@ mod tests {
assert_eq!(Oauth2::from_address("hello@web.de"), None);
}
#[test]
fn test_dc_get_oauth2_addr() {
let ctx = dummy_context();
#[async_std::test]
async fn test_dc_get_oauth2_addr() {
let ctx = dummy_context().await;
let addr = "dignifiedquire@gmail.com";
let code = "fail";
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code);
let res = dc_get_oauth2_addr(&ctx.ctx, addr, code).await;
// this should fail as it is an invalid password
assert_eq!(res, None);
}
#[test]
fn test_dc_get_oauth2_url() {
let ctx = dummy_context();
#[async_std::test]
async fn test_dc_get_oauth2_url() {
let ctx = dummy_context().await;
let addr = "dignifiedquire@gmail.com";
let redirect_uri = "chat.delta:/com.b44t.messenger";
let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri);
let res = dc_get_oauth2_url(&ctx.ctx, addr, redirect_uri).await;
assert_eq!(res, Some("https://accounts.google.com/o/oauth2/auth?client_id=959970109878%2D4mvtgf6feshskf7695nfln6002mom908%2Eapps%2Egoogleusercontent%2Ecom&redirect_uri=chat%2Edelta%3A%2Fcom%2Eb44t%2Emessenger&response_type=code&scope=https%3A%2F%2Fmail.google.com%2F%20email&access_type=offline".into()));
}
#[test]
fn test_dc_get_oauth2_token() {
let ctx = dummy_context();
#[async_std::test]
async fn test_dc_get_oauth2_token() {
let ctx = dummy_context().await;
let addr = "dignifiedquire@gmail.com";
let code = "fail";
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false);
let res = dc_get_oauth2_access_token(&ctx.ctx, addr, code, false).await;
// this should fail as it is an invalid password
assert_eq!(res, None);
}

View File

@@ -1,14 +1,14 @@
use std::collections::BTreeMap;
use std::fmt;
use std::path::PathBuf;
use std::str;
use async_std::path::PathBuf;
use num_traits::FromPrimitive;
use serde::{Deserialize, Serialize};
use crate::blob::{BlobError, BlobObject};
use crate::context::Context;
use crate::error;
use crate::error::{self, bail, ensure};
use crate::message::MsgId;
use crate::mimeparser::SystemMessage;
@@ -88,12 +88,6 @@ pub enum Param {
/// For Jobs
SetLongitude = b'n',
/// For Jobs
ServerFolder = b'Z',
/// For Jobs
ServerUid = b'z',
/// For Jobs
AlsoMove = b'M',
@@ -281,7 +275,8 @@ impl Params {
/// created without copying if the path already referes to a valid
/// blob. If so a [BlobObject] will be returned regardless of the
/// `create` argument.
pub fn get_blob<'a>(
#[allow(clippy::needless_lifetimes)]
pub async fn get_blob<'a>(
&self,
key: Param,
context: &'a Context,
@@ -294,7 +289,7 @@ impl Params {
let file = ParamsFile::from_param(context, val)?;
let blob = match file {
ParamsFile::FsPath(path) => match create {
true => BlobObject::new_from_path(context, path)?,
true => BlobObject::new_from_path(context, path).await?,
false => BlobObject::from_path(context, path)?,
},
ParamsFile::Blob(blob) => blob,
@@ -368,8 +363,8 @@ impl<'a> ParamsFile<'a> {
mod tests {
use super::*;
use std::fs;
use std::path::Path;
use async_std::fs;
use async_std::path::Path;
use crate::test_utils::*;
@@ -417,9 +412,9 @@ mod tests {
assert_eq!(p1.get(Param::Forwarded).unwrap(), "cli%40deltachat.de");
}
#[test]
fn test_params_file_fs_path() {
let t = dummy_context();
#[async_std::test]
async fn test_params_file_fs_path() {
let t = dummy_context().await;
if let ParamsFile::FsPath(p) = ParamsFile::from_param(&t.ctx, "/foo/bar/baz").unwrap() {
assert_eq!(p, Path::new("/foo/bar/baz"));
} else {
@@ -427,9 +422,9 @@ mod tests {
}
}
#[test]
fn test_params_file_blob() {
let t = dummy_context();
#[async_std::test]
async fn test_params_file_blob() {
let t = dummy_context().await;
if let ParamsFile::Blob(b) = ParamsFile::from_param(&t.ctx, "$BLOBDIR/foo").unwrap() {
assert_eq!(b.as_name(), "$BLOBDIR/foo");
} else {
@@ -438,28 +433,33 @@ mod tests {
}
// Tests for Params::get_file(), Params::get_path() and Params::get_blob().
#[test]
fn test_params_get_fileparam() {
let t = dummy_context();
#[async_std::test]
async fn test_params_get_fileparam() {
let t = dummy_context().await;
let fname = t.dir.path().join("foo");
let mut p = Params::new();
p.set(Param::File, fname.to_str().unwrap());
let file = p.get_file(Param::File, &t.ctx).unwrap().unwrap();
assert_eq!(file, ParamsFile::FsPath(fname.clone()));
assert_eq!(file, ParamsFile::FsPath(fname.clone().into()));
let path = p.get_path(Param::File, &t.ctx).unwrap().unwrap();
let path: PathBuf = p.get_path(Param::File, &t.ctx).unwrap().unwrap();
let fname: PathBuf = fname.into();
assert_eq!(path, fname);
// Blob does not exist yet, expect BlobError.
let err = p.get_blob(Param::File, &t.ctx, false).unwrap_err();
let err = p.get_blob(Param::File, &t.ctx, false).await.unwrap_err();
match err {
BlobError::WrongBlobdir { .. } => (),
_ => panic!("wrong error type/variant: {:?}", err),
}
fs::write(fname, b"boo").unwrap();
let blob = p.get_blob(Param::File, &t.ctx, true).unwrap().unwrap();
fs::write(fname, b"boo").await.unwrap();
let blob = p
.get_blob(Param::File, &t.ctx, true)
.await
.unwrap()
.unwrap();
assert_eq!(
blob,
BlobObject::from_name(&t.ctx, "foo".to_string()).unwrap()
@@ -468,7 +468,11 @@ mod tests {
// Blob in blobdir, expect blob.
let bar = t.ctx.get_blobdir().join("bar");
p.set(Param::File, bar.to_str().unwrap());
let blob = p.get_blob(Param::File, &t.ctx, false).unwrap().unwrap();
let blob = p
.get_blob(Param::File, &t.ctx, false)
.await
.unwrap()
.unwrap();
assert_eq!(
blob,
BlobObject::from_name(&t.ctx, "bar".to_string()).unwrap()
@@ -477,6 +481,10 @@ mod tests {
p.remove(Param::File);
assert!(p.get_file(Param::File, &t.ctx).unwrap().is_none());
assert!(p.get_path(Param::File, &t.ctx).unwrap().is_none());
assert!(p.get_blob(Param::File, &t.ctx, false).unwrap().is_none());
assert!(p
.get_blob(Param::File, &t.ctx, false)
.await
.unwrap()
.is_none());
}
}

View File

@@ -9,7 +9,7 @@ use crate::aheader::*;
use crate::constants::*;
use crate::context::Context;
use crate::key::{Key, SignedPublicKey};
use crate::sql::{self, Sql};
use crate::sql::Sql;
#[derive(Debug)]
pub enum PeerstateKeyType {
@@ -144,12 +144,16 @@ impl<'a> Peerstate<'a> {
res
}
pub fn from_addr(context: &'a Context, _sql: &Sql, addr: &str) -> Option<Self> {
pub async fn from_addr(context: &'a Context, addr: &str) -> Option<Peerstate<'a>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, verified_key, verified_key_fingerprint FROM acpeerstates WHERE addr=? COLLATE NOCASE;";
Self::from_stmt(context, query, &[addr])
Self::from_stmt(context, query, paramsv![addr]).await
}
pub fn from_fingerprint(context: &'a Context, _sql: &Sql, fingerprint: &str) -> Option<Self> {
pub async fn from_fingerprint(
context: &'a Context,
_sql: &Sql,
fingerprint: &str,
) -> Option<Peerstate<'a>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint \
@@ -161,15 +165,16 @@ impl<'a> Peerstate<'a> {
Self::from_stmt(
context,
query,
params![fingerprint, fingerprint, fingerprint],
paramsv![fingerprint, fingerprint, fingerprint],
)
.await
}
fn from_stmt<P>(context: &'a Context, query: &str, params: P) -> Option<Self>
where
P: IntoIterator,
P::Item: rusqlite::ToSql,
{
async fn from_stmt(
context: &'a Context,
query: &str,
params: Vec<&dyn crate::ToSql>,
) -> Option<Peerstate<'a>> {
context
.sql
.query_row(query, params, |row| {
@@ -227,6 +232,7 @@ impl<'a> Peerstate<'a> {
Ok(res)
})
.await
.ok()
}
@@ -409,52 +415,48 @@ impl<'a> Peerstate<'a> {
}
}
pub fn save_to_db(&self, sql: &Sql, create: bool) -> crate::sql::Result<()> {
pub async fn save_to_db(&self, sql: &Sql, create: bool) -> crate::sql::Result<()> {
if create {
sql::execute(
self.context,
sql,
sql.execute(
"INSERT INTO acpeerstates (addr) VALUES(?);",
params![self.addr],
)?;
paramsv![self.addr],
)
.await?;
}
if self.to_save == Some(ToSave::All) || create {
sql::execute(
self.context,
sql,
sql.execute(
"UPDATE acpeerstates \
SET last_seen=?, last_seen_autocrypt=?, prefer_encrypted=?, \
public_key=?, gossip_timestamp=?, gossip_key=?, public_key_fingerprint=?, gossip_key_fingerprint=?, \
verified_key=?, verified_key_fingerprint=? \
WHERE addr=?;",
params![
paramsv![
self.last_seen,
self.last_seen_autocrypt,
self.prefer_encrypt as i64,
self.public_key.as_ref().map(|k| k.to_bytes()),
self.gossip_timestamp,
self.gossip_key.as_ref().map(|k| k.to_bytes()),
&self.public_key_fingerprint,
&self.gossip_key_fingerprint,
self.public_key_fingerprint,
self.gossip_key_fingerprint,
self.verified_key.as_ref().map(|k| k.to_bytes()),
&self.verified_key_fingerprint,
&self.addr,
self.verified_key_fingerprint,
self.addr,
],
)?;
).await?;
} else if self.to_save == Some(ToSave::Timestamps) {
sql::execute(
self.context,
sql,
sql.execute(
"UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \
WHERE addr=?;",
params![
paramsv![
self.last_seen,
self.last_seen_autocrypt,
self.gossip_timestamp,
&self.addr
self.addr
],
)?;
)
.await?;
}
Ok(())
@@ -479,9 +481,9 @@ mod tests {
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[test]
fn test_peerstate_save_to_db() {
let ctx = crate::test_utils::dummy_context();
#[async_std::test]
async fn test_peerstate_save_to_db() {
let ctx = crate::test_utils::dummy_context().await;
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from(alice_keypair().public);
@@ -504,11 +506,12 @@ mod tests {
};
assert!(
peerstate.save_to_db(&ctx.ctx.sql, true).is_ok(),
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
"failed to save to db"
);
let peerstate_new = Peerstate::from_addr(&ctx.ctx, &ctx.ctx.sql, addr)
let peerstate_new = Peerstate::from_addr(&ctx.ctx, addr)
.await
.expect("failed to load peerstate from db");
// clear to_save, as that is not persissted
@@ -516,13 +519,14 @@ mod tests {
assert_eq!(peerstate, peerstate_new);
let peerstate_new2 =
Peerstate::from_fingerprint(&ctx.ctx, &ctx.ctx.sql, &pub_key.fingerprint())
.await
.expect("failed to load peerstate from db");
assert_eq!(peerstate, peerstate_new2);
}
#[test]
fn test_peerstate_double_create() {
let ctx = crate::test_utils::dummy_context();
#[async_std::test]
async fn test_peerstate_double_create() {
let ctx = crate::test_utils::dummy_context().await;
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from(alice_keypair().public);
@@ -544,18 +548,18 @@ mod tests {
};
assert!(
peerstate.save_to_db(&ctx.ctx.sql, true).is_ok(),
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
"failed to save"
);
assert!(
peerstate.save_to_db(&ctx.ctx.sql, true).is_ok(),
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
"double-call with create failed"
);
}
#[test]
fn test_peerstate_with_empty_gossip_key_save_to_db() {
let ctx = crate::test_utils::dummy_context();
#[async_std::test]
async fn test_peerstate_with_empty_gossip_key_save_to_db() {
let ctx = crate::test_utils::dummy_context().await;
let addr = "hello@mail.com";
let pub_key = crate::key::Key::from(alice_keypair().public);
@@ -578,11 +582,12 @@ mod tests {
};
assert!(
peerstate.save_to_db(&ctx.ctx.sql, true).is_ok(),
peerstate.save_to_db(&ctx.ctx.sql, true).await.is_ok(),
"failed to save"
);
let peerstate_new = Peerstate::from_addr(&ctx.ctx, &ctx.ctx.sql, addr)
let peerstate_new = Peerstate::from_addr(&ctx.ctx, addr)
.await
.expect("failed to load peerstate from db");
// clear to_save, as that is not persissted

View File

@@ -18,7 +18,7 @@ use rand::{thread_rng, CryptoRng, Rng};
use crate::constants::KeyGenType;
use crate::dc_tools::EmailAddress;
use crate::error::Result;
use crate::error::{bail, ensure, format_err, Result};
use crate::key::*;
use crate::keyring::*;
@@ -117,21 +117,19 @@ pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, Str
///
/// Most of these are likely coding errors rather than user errors
/// since all variability is hardcoded.
#[derive(Fail, Debug)]
#[fail(display = "PgpKeygenError: {}", message)]
pub(crate) struct PgpKeygenError {
#[derive(Debug, thiserror::Error)]
#[error("PgpKeygenError: {message}")]
pub struct PgpKeygenError {
message: String,
#[cause]
cause: failure::Error,
backtrace: failure::Backtrace,
#[source]
cause: anyhow::Error,
}
impl PgpKeygenError {
fn new(message: impl Into<String>, cause: impl Into<failure::Error>) -> Self {
fn new(message: impl Into<String>, cause: impl Into<anyhow::Error>) -> Self {
Self {
message: message.into(),
cause: cause.into(),
backtrace: failure::Backtrace::new(),
}
}
}
@@ -189,7 +187,7 @@ pub(crate) fn create_keypair(
.unwrap(),
)
.build()
.map_err(|err| PgpKeygenError::new("invalid key params", failure::err_msg(err)))?;
.map_err(|err| PgpKeygenError::new("invalid key params", format_err!(err)))?;
let key = key_params
.generate()
.map_err(|err| PgpKeygenError::new("invalid params", err))?;
@@ -394,7 +392,6 @@ mod tests {
}
#[test]
#[ignore] // is too expensive
fn test_create_keypair() {
let keypair0 = create_keypair(
EmailAddress::new("foo@bar.de").unwrap(),
@@ -573,88 +570,4 @@ mod tests {
.unwrap();
assert_eq!(plain, CLEARTEXT);
}
fn test_encrypt_decrypt_fuzz(key_type: KeyGenType) {
// sending messages from Alice to Bob
let alice = create_keypair(EmailAddress::new("a@e.org").unwrap(), key_type).unwrap();
let bob = create_keypair(EmailAddress::new("b@e.org").unwrap(), key_type).unwrap();
let alice_secret = Key::from(alice.secret.clone());
let alice_public = Key::from(alice.public.clone());
let bob_secret = Key::from(bob.secret.clone());
let bob_public = Key::from(bob.public.clone());
let plain: &[u8] = b"just a test";
let mut encr_keyring = Keyring::default();
encr_keyring.add_ref(&bob_public);
let mut decr_keyring = Keyring::default();
decr_keyring.add_ref(&bob_secret);
let mut validate_keyring = Keyring::default();
validate_keyring.add_ref(&alice_public);
for _ in 0..1000 {
let ctext = pk_encrypt(plain.as_ref(), &encr_keyring, Some(&alice_secret)).unwrap();
let plain2 =
pk_decrypt(ctext.as_ref(), &decr_keyring, &validate_keyring, None).unwrap();
assert_eq!(plain2, plain);
}
}
#[test]
#[ignore]
fn test_encrypt_decrypt_fuzz_rsa() {
test_encrypt_decrypt_fuzz(KeyGenType::Rsa2048)
}
#[test]
fn test_encrypt_decrypt_fuzz_ecc() {
test_encrypt_decrypt_fuzz(KeyGenType::Ed25519)
}
#[test]
fn test_encrypt_decrypt_fuzz_alice() -> Result<()> {
let plain: &[u8] = b"just a test";
let lit_msg = Message::new_literal_bytes("", plain);
let mut rng = thread_rng();
let enc_key =
SignedPublicKey::from_base64(include_str!("../test-data/key/alice-public.asc"))
.unwrap();
let enc_subkey = &enc_key.public_subkeys[0];
assert!(enc_subkey.is_encryption_key());
let pkeys: Vec<&SignedPublicSubKey> = vec![&enc_subkey];
let skey = SignedSecretKey::from_base64(include_str!("../test-data/key/alice-secret.asc"))
.unwrap();
let skeys: Vec<&SignedSecretKey> = vec![&skey];
for _ in 0..1000 {
let msg = lit_msg.encrypt_to_keys(
&mut rng,
pgp::crypto::sym::SymmetricKeyAlgorithm::AES128,
&pkeys[..],
)?;
let ctext = msg.to_armored_string(None)?;
println!("{}", ctext);
let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
ensure!(!msgs.is_empty(), "No valid messages found");
let dec_msg = &msgs[0];
let plain2: Vec<u8> = match dec_msg.get_content()? {
Some(content) => content,
None => bail!("Decrypted message is empty"),
};
assert_eq!(plain2, plain);
}
Ok(())
}
}

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