Compare commits

..

121 Commits

Author SHA1 Message Date
B. Petersen
3253d427ec do not downgrade protected chats over the wire
downgrading is possible only for oneself,
not for the whole group.

enabling is still done for all as a "best effort".

this gives the user who enabled protection
strong guarantess about ones state.

downside may be different views on a chat by different users,
however, that could also happen before.
2021-10-17 14:08:11 +02:00
dependabot[bot]
7fb305e898 Merge pull request #2746 from deltachat/dependabot/cargo/thiserror-1.0.30 2021-10-15 23:00:26 +00:00
dependabot[bot]
d4255a4979 Merge pull request #2753 from deltachat/dependabot/cargo/stop-token-0.5.1 2021-10-15 22:58:35 +00:00
dependabot[bot]
b21dcd17b7 cargo: bump stop-token from 0.4.0 to 0.5.1
Bumps [stop-token](https://github.com/async-rs/stop-token) from 0.4.0 to 0.5.1.
- [Release notes](https://github.com/async-rs/stop-token/releases)
- [Commits](https://github.com/async-rs/stop-token/commits)

---
updated-dependencies:
- dependency-name: stop-token
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-15 21:12:24 +00:00
bjoern
0caea85d16 priorize CertificateChecks setting from user over the one from provider-db (#2749)
* priorize CertificateChecks setting from user over the one from provider-db

* avoid some duplicate code

* remove questionable comment
2021-10-13 21:45:51 +02:00
dependabot[bot]
42e0fb5eb9 cargo: bump thiserror from 1.0.29 to 1.0.30
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.29 to 1.0.30.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.29...1.0.30)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-12 21:14:43 +00:00
link2xt
6061d71492 cargo: update strum to 0.22 2021-10-12 21:12:46 +00:00
link2xt
dbd8814d2c Refactor qr module 2021-10-10 15:11:01 +03:00
link2xt
a3562c5940 Remove get_summarytext_by_raw
Use `Summary.truncated_text()` instead.

Co-Authored-By: Floris Bruynooghe <flub@devork.be>
2021-10-09 22:13:42 +03:00
dependabot[bot]
49b07c1c6a Merge pull request #2697 from deltachat/dependabot/cargo/async-tar-0.4.2 2021-10-09 11:50:19 +00:00
dependabot[bot]
51d220f1e0 Merge pull request #2732 from deltachat/dependabot/cargo/stop-token-0.4.0 2021-10-09 11:14:10 +00:00
dependabot[bot]
9f81a94d86 cargo: bump stop-token from 0.2.0 to 0.4.0
Bumps [stop-token](https://github.com/async-rs/stop-token) from 0.2.0 to 0.4.0.
- [Release notes](https://github.com/async-rs/stop-token/releases)
- [Commits](https://github.com/async-rs/stop-token/commits)

---
updated-dependencies:
- dependency-name: stop-token
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-09 10:58:29 +00:00
dependabot[bot]
f6098fc931 cargo: bump async-tar from 0.3.0 to 0.4.2
Bumps [async-tar](https://github.com/dignifiedquire/async-tar) from 0.3.0 to 0.4.2.
- [Release notes](https://github.com/dignifiedquire/async-tar/releases)
- [Commits](https://github.com/dignifiedquire/async-tar/compare/v0.3.0...v0.4.2)

---
updated-dependencies:
- dependency-name: async-tar
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-09 10:58:22 +00:00
link2xt
6e3c2fc839 dc_receive_imf: simplify timestamp calculation
Reduce the number of mutable variables and out parameters in
`add_parts`.

Also don't call `dc_create_smeared_timestamp` if there is no `Date`
header. Timestamps are only supposed to be "created" when the message
is sent, not received, to make sure sent messages are sorted properly
in MUAs that only use the date for sorting. Delta Chat uses database
IDs for sorting in addition to timestamps, so it can sort messages
with equal timestamps properly.

Update `dc_smeared_time` documentation.

Turn `dc_smeared_time` and `dc_create_smeared_timestamp` comments into
documentation comments.
2021-10-09 13:44:59 +03:00
link2xt
30e616f74f Increase MSRV to 1.51.0 and cargo update 2021-10-09 12:08:07 +03:00
Hocuri
5e29cae81a Fix: Don't update quota in an endless loop (#2726)
The problem was:
When opening the connectivity view while there is no network,
get_connectivity_html() calls schedule_quota_update(), which schedules a
UpdateRecentQuota job. But in update_recent_quota(), connecting fails
and a ConnectivityChanged event is emitted (connectivity changes from
Error to Connecting and back). Therefore the UI calls
get_connectivity_html() again, and the loop is complete.

This made the UI completely unresponsible. To reproduce, just turn wi-fi
off and open the connectivity view.

The fix is:
schedule_quota_update() now only schedules a new job if there is no old
job. To prevent the possible (though probably unlikely) problem that an
old quota update job has a backoff of, like, a day and therefore quota
is not updated, I reduced the backoff time for quota jobs to 10s.

Fixes possibly https://github.com/deltachat/deltachat-android/issues/2043, but we should "re-try" this
2021-10-05 10:57:34 +02:00
bjoern
1ee19bf3ca prepare 1.61 (#2715)
* update changelog for 1.61

* adapt hints in version-helper

* bump version to 1.61.0
2021-10-03 17:53:15 +02:00
bjoern
b18bdd1b00 fix "QR process failed" error and add a test (#2725)
* better readable enum

* add a failing test

* let new secure-joins abort existing ones

before, a stale secure-join or setup-contact
made the whole qr-scanning unusable until
the app is restarted,
resulting in "QR process failed" errors.

this commit fixes the issue by
aborting existing scans -
in cases, a user really wants two concurrect joins running,
this is not perfect, but that did not worked before as well.

* remove unused AlreadyRunning variant

* make clippy happy
2021-10-03 13:17:09 +02:00
link2xt
6c59b0de85 Remove unused Imap.interrupt
It is always set to `None`.
2021-10-03 01:14:50 +00:00
dependabot[bot]
c1d82ad417 Merge pull request #2717 from deltachat/dependabot/cargo/smallvec-1.7.0 2021-10-02 17:33:10 +00:00
dependabot[bot]
ba931773d1 Merge pull request #2716 from deltachat/dependabot/cargo/pretty_assertions-1.0.0 2021-10-02 16:19:33 +00:00
bjoern
b6f88a9fca make string 123 MiB of 456 MiB used translatable (#2723) 2021-10-01 21:07:38 +02:00
bjoern
b0902102a2 add some more hints and missing crosslinks to dc_get_chat_type() (#2724) 2021-10-01 21:07:23 +02:00
bjoern
4f19036408 extend forward-test by broadcasts (#2722) 2021-09-30 18:18:37 +02:00
dependabot[bot]
fe1f9c0ed9 Merge pull request #2720 from deltachat/dependabot/cargo/async-smtp-3e7a8f3 2021-09-30 12:23:47 +00:00
bjoern
bcadd0cd5c broadcasts (#2707)
* add Chattype::Broadcast and create_broadcast_list()

* do not disclose recipients for broadcasts

* allow sending/add-/remove-member for broadcast

* set broadcast subject same as for one-to-one chats

* broadcast-recipient-list does not include SELF

* use special icon for broadcast groups

* generate initial broadcast names

* make clippy happy

* send BCC message unencrypted to avoid unexpected disclosing; encryption is opportunistic anyway. if we have 'protected chats' at some point, we can think that over.

* reword 'To:'-group

* simplify can-send-check

* add broadcast tests

* tweak comments

* Update deltachat-ffi/deltachat.h

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

* change name of can_edit() to is_self_in_chat()

Co-authored-by: Hocuri <hocuri@gmx.de>
2021-09-30 13:56:05 +02:00
bjoern
30a3da97da do not leak group names on forwarding, add tests for that (#2719)
* add a test to check no possibly sensible data are forwarded

* do not leak group names on forwarding

* adapt existing test
2021-09-30 13:19:37 +02:00
dependabot[bot]
a8b2a20146 cargo: bump async-smtp from 2c21f5f to 3e7a8f3
Bumps [async-smtp](https://github.com/async-email/async-smtp) from `2c21f5f` to `3e7a8f3`.
- [Release notes](https://github.com/async-email/async-smtp/releases)
- [Commits](2c21f5fb64...3e7a8f3de1)

---
updated-dependencies:
- dependency-name: async-smtp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-29 21:10:13 +00:00
bjoern
82819a642f update provider database (#2718) 2021-09-29 12:59:38 +02:00
dependabot[bot]
3960d4129e cargo: bump smallvec from 1.6.1 to 1.7.0
Bumps [smallvec](https://github.com/servo/rust-smallvec) from 1.6.1 to 1.7.0.
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.6.1...v1.7.0)

---
updated-dependencies:
- dependency-name: smallvec
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-28 21:12:49 +00:00
dependabot[bot]
e405ddf080 cargo: bump pretty_assertions from 0.7.2 to 1.0.0
Bumps [pretty_assertions](https://github.com/colin-kiegel/rust-pretty-assertions) from 0.7.2 to 1.0.0.
- [Release notes](https://github.com/colin-kiegel/rust-pretty-assertions/releases)
- [Changelog](https://github.com/colin-kiegel/rust-pretty-assertions/blob/main/CHANGELOG.md)
- [Commits](https://github.com/colin-kiegel/rust-pretty-assertions/compare/v0.7.2...v1.0.0)

---
updated-dependencies:
- dependency-name: pretty_assertions
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-28 21:12:37 +00:00
dependabot[bot]
1eadbbb7cd Merge pull request #2698 from deltachat/dependabot/cargo/rustyline-9.0.0 2021-09-28 17:54:21 +00:00
bjoern
941b8caa8b move create_multiuser_record() to ChatId (#2706)
this is needed for targeting "non-blocking group QR joins"
as create_multiuser_record() would also be needed from other places.

this will make rebasing/rewriting and finally reviewing #2508 easier.
2021-09-27 20:24:25 +02:00
bjoern
95bce993ad set message-state OutMdnRcvd on first read-receipt (#2699)
in the past, group-messages were marked as "read by recipient"
only when at least 50% of the group members have send a read receipt -
in practise, this does happen never or much too late esp. in larger groups.

setting the state OutMdnRcvd already on the first read-receipt
seems to be much more intuitive and useful as you at least know
one person has read the message.

this is also what other messengers as telegram are doing here.

moreover, this fixes a bug that did not add all read-receipts
to the "Info" screen - once "enough" read-receipts were received,
and the state was already set to OutMdnRcvd, subsequent read-receipts
were ignored.
probably because the "Info" screen did not show the read-receipts since forever -
and for the second tick, the addutional read-receipts are not needed.
2021-09-27 13:39:27 +02:00
link2xt
acbf363fc8 Try to lock strict TLS if certificate checks are automatic 2021-09-26 19:03:04 +03:00
link2xt
2309c7ca13 Emit events from account manager
Errors and warnings are emitted with a special 0 account ID.
2021-09-25 18:25:52 +03:00
link2xt
89d8b26192 Downgrade zeroize_derive to 1.1.0
Version 1.2.0 is not supported by Rust 1.48.0
2021-09-25 12:21:27 +00:00
link2xt
ee32a7b00a Remove minor versions from deltachat_derive and deltachat-ffi
Also run `cargo update`
2021-09-25 12:15:59 +00:00
link2xt
1dbbf6b3be Create configured folders if they are deleted 2021-09-25 02:51:32 +03:00
link2xt
f8a4a88fb2 Accept &str instead of AsRef<str> in fetch_new_mesages() 2021-09-25 02:51:32 +03:00
link2xt
3096193d58 ephemeral: always apply timers from system messages
Also extend the tests to catch regressions.
2021-09-25 02:51:23 +03:00
cyBerta
d8b47dc4aa add parameter description for DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED (#2705)
* add parameter description for DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED

* Update deltachat-ffi/deltachat.h

add description for data2 parameter

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

Co-authored-by: bjoern <r10s@b44t.com>
2021-09-23 11:18:01 +02:00
bjoern
a5826d6a06 let quota-warning reappear after import, rewarn at 95% (#2702)
* let quota-warning reappear after import

an import removes all device-messages,
including the quota warning.

resetting `Config::QuotaExceeding` makes sure,
the warning reappears soon after import -
otherwise the warning would reappear only after
storage is cleaned up and exceeds again.

* a second quota warning when 95% storage exceeded

* factor out warning-check and add a test for that
2021-09-22 12:41:35 +02:00
dependabot[bot]
5df0be8311 cargo: bump rustyline from 8.2.0 to 9.0.0
Bumps [rustyline](https://github.com/kkawakam/rustyline) from 8.2.0 to 9.0.0.
- [Release notes](https://github.com/kkawakam/rustyline/releases)
- [Changelog](https://github.com/kkawakam/rustyline/blob/master/History.md)
- [Commits](https://github.com/kkawakam/rustyline/compare/v8.2.0...v9.0.0)

---
updated-dependencies:
- dependency-name: rustyline
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-20 21:12:49 +00:00
bjoern
66a5e0743d make connectivity and quota views translatable (#2694)
* add missing stock strings for connectivity and quota

* reword quota error

* use 'Incoming/Outgoing Messages' instead of 'Inbox/Outbox'

* just say 'Not supported by your provider.' if quota cannot be read.

- the context is already given by the headline
  'Storage on domain.org'
- the string is short and does not disturb much
  (it is a very common error)
- the string does not mention 'quota' as such,
  which will be hard to translate ('Kontingentsinformationen') -
  even if ppl as we know about the Quota extensions an such things.

there was also the idea to hide the whole section,
however, that is confusing in a multi-device-usage
when things are sometimes shown and sometimes not.
2021-09-20 21:43:53 +02:00
bjoern
43d1d9b1b3 emit MsgsChanged(chat_id, 0) on full downloads (#2696)
before, MsgsChanged(chat_id, new_msg_id) was emitted,
but that does not cover the deleted message.

in theory, we could emit both,
however, that would just be a waste of refresh in uis.

also before, events were used this way,
however, also the documentations are updated to
reflect reality better.
2021-09-20 20:50:22 +02:00
bjoern
3e0f601212 implement set/get_ui_config() resurrection (#2672)
* Implement set/get_ui_config and use those methods if config string starts with 'ui.'

* use ensure! macro
2021-09-20 20:50:07 +02:00
link2xt
085a899de2 Fix ephemeral timer rollback protection
Implement get_previous_message() that only looks for the last
Message-ID in the References: header instead of using
get_parent_message() which falls back to using other Message-IDs from
References and In-Reply-To field.
2021-09-20 01:54:51 +03:00
link2xt
b07e20b955 ephemeral: add failing rollback protection test 2021-09-20 01:54:51 +03:00
link2xt
4e8724694a Notify about incoming contact requests 2021-09-19 04:06:01 +03:00
link2xt
47bf67e658 Resultification 2021-09-18 21:56:02 +03:00
link2xt
7bb7748b6b Deduplicate peerstates during housekeeping 2021-09-18 21:31:57 +03:00
Simon Laux
b33ad05c3b chore add .DS_Store to gitignore 2021-09-18 19:01:55 +02:00
bjoern
398cea6466 document DC_STATE and DC_CHAT_TYPE explicitly (#2688)
* document DC_STATE explicitly and add real hyperlinks

* document DC_CHAT_TYPE explicitly

* sort constants alphabetically
2021-09-18 13:51:07 +02:00
link2xt
1afd2f2d66 Fix clippy warnings in repl 2021-09-18 10:29:32 +00:00
link2xt
48f1ef3641 Remove minor versions from Cargo.toml 2021-09-17 22:06:41 +00:00
link2xt
e95911a484 Update OpenSSL 2021-09-17 21:50:44 +00:00
link2xt
b1af486e10 Log all decisions when applying ephemeral timer to chats
This should make it easier to debug problems related to timer
rollbacks and reported failure to disable the timer in some chats.
2021-09-18 00:48:33 +03:00
bjoern
bffb41326c better names for more mailinglist-types (#2685)
* add a test for xing mailinglists

* strip long hash-prefixes from mailinglist name if we got the name from the List-Id as a last resort

* add a test for newsletter@ mailinglists

these mailinglists have the list-name in `From:`
and can be detected by addresses starting with `newsletter@`.

* if not list-name is set, use the `From:` name for addresses starting with `newsletter@`

this is similar to what we do with `notifications@`

* Update src/dc_receive_imf.rs

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

* add an example to the regex

Co-authored-by: Hocuri <hocuri@gmx.de>
2021-09-16 18:43:37 +02:00
dependabot[bot]
c532055153 Merge pull request #2683 from deltachat/dependabot/cargo/anyhow-1.0.44 2021-09-16 12:31:07 +00:00
dependabot[bot]
be595f8601 Merge pull request #2686 from deltachat/dependabot/cargo/serde_json-1.0.68 2021-09-16 12:20:26 +00:00
bjoern
1d1d98e02b do not use term Message-ID for msg_id, clarify that on downloading also complete replacements may happen. (#2684) 2021-09-16 14:17:08 +02:00
dependabot[bot]
771e84af6e cargo: bump serde_json from 1.0.67 to 1.0.68
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.67 to 1.0.68.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.67...v1.0.68)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-14 21:12:30 +00:00
dependabot[bot]
bbfed20d34 cargo: bump anyhow from 1.0.43 to 1.0.44
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.43 to 1.0.44.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.43...1.0.44)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-13 21:12:34 +00:00
bjoern
0f2095947c download on demand (#2631)
* draft a download-api

* basic implementation

* allow partial downloads for protected chats

* use a separate column for download_state

* force a minimal timeout for delete_server_after in combination with partial messages

* add a warning if a possible download may expire by delete_server_after

* test load_imap_deletion_msgid()

* add a test for a partial download

* improve documentation and visibility

* let get_download_limit() return Result<Option>

* rusty getters

* apply MIN_DELETE_SERVER_AFTER to shown availability time

* move stub-creation to download.rs, use stock-strings, nicer logging

* make clippy happy (cargo clippy --tests)

* refine tests and comments

* fix typo

* remove superfluous closure in ffi

* respect partial_download for immediately scheduled DeleteMsgOnImap jobs
2021-09-13 21:12:00 +02:00
Hocuri
46956caf75 Fix: Recognize ndns that put the headers into "message/global-headers" part (Improve ndn detection) (#2598)
I sent a message, and the ndn (Non Delivery Notification) was not parsed correctly, so here comes
the fix and the test.
2021-09-12 21:02:51 +02:00
link2xt
6f3dd7f0c2 Use saturating addition for ephemeral timers
Integer overflows crash the application by default.

On a first sight this is only a potential crash that can't be
triggered, because timestamps are stored as i64 and ephemeral timer
duration is u32.
2021-09-12 19:23:54 +03:00
link2xt
15dcd62652 imap: remove unnecessary Imap.connected variable
It is always the same as Imap.session.is_some().
2021-09-12 19:23:44 +03:00
bjoern
da2f30786b get correct names of .xt.local mailinglists (#2665)
* add a test for .xt.local mailinglists

* get correct names of .xt.local mailinglists

these mailinglist probably come from the xt:Commerce system
and are pretty widely used.

i have not seen an .xt.local mailinglist with name set in List-Id,
however, if that happens, it will still be taken.

only if the name is unset,
we use the name from the From-header.
2021-09-12 15:34:29 +02:00
bjoern
50a5e715d2 simpler subject of Autocrypt-Setup-Message (#2673)
since some time, core handles per-message-subjects
and Mimefactory also picks that up.

therefore, we can remove the old special handling.
2021-09-12 14:34:55 +02:00
link2xt
1bef623c89 Update changelog 2021-09-12 12:31:27 +00:00
link2xt
7745db8310 Ignore MDNs sent to self
These sometimes arrive happen due to a bug in previous versions of
Delta Chat.
2021-09-12 02:18:50 +03:00
link2xt
1d1491c95d Introduce summary module
summary::Summary replaces Lot in the Rust API for methods returning
chatlist summaries.  Lot is a legacy type for C API compatibility, so
Summary can be converted into Lot.
2021-09-12 01:32:33 +03:00
dependabot[bot]
2a0f6f5cf7 Merge pull request #2671 from deltachat/dependabot/cargo/sha2-0.9.8 2021-09-11 10:18:56 +00:00
dependabot[bot]
b27793e852 cargo: bump sha2 from 0.9.6 to 0.9.8
Bumps [sha2](https://github.com/RustCrypto/hashes) from 0.9.6 to 0.9.8.
- [Release notes](https://github.com/RustCrypto/hashes/releases)
- [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.9.6...sha2-v0.9.8)

---
updated-dependencies:
- dependency-name: sha2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-09 21:13:26 +00:00
Robert Schütz
8fb5e038a9 fix pkg-config file
The CMAKE_INSTALL_FULL_<dir> variables are only defined if
GNUInstallDirs is included.
2021-09-08 17:48:33 -07:00
bjoern
e518dc3331 clarify dc_is_configured() (#2668) 2021-09-08 16:05:38 +02:00
dependabot[bot]
ea1368a36b Merge pull request #2661 from deltachat/dependabot/cargo/syn-1.0.76 2021-09-08 09:36:41 +00:00
dependabot[bot]
0aeb2bd6fb cargo: bump syn from 1.0.75 to 1.0.76
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.75 to 1.0.76.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.75...1.0.76)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-08 08:34:30 +00:00
dependabot[bot]
0263d0816a Merge pull request #2662 from deltachat/dependabot/cargo/thiserror-1.0.29 2021-09-08 08:33:09 +00:00
dependabot[bot]
bb71f6ec98 cargo: bump thiserror from 1.0.28 to 1.0.29
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.28 to 1.0.29.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.28...1.0.29)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-06 21:12:39 +00:00
link2xt
02a1abc0d5 Remove emit_event! macro 2021-09-05 22:45:30 +03:00
link2xt
40fe65716f ffi: add RwLock to dc_accounts_t for thread safety 2021-09-05 18:53:17 +03:00
link2xt
d05b399eac accounts: remove unnecessary Arc<RwLock<_>> from Config.inner 2021-09-05 18:53:17 +03:00
dependabot[bot]
c31216f043 Merge pull request #2645 from deltachat/dependabot/cargo/sha2-0.9.6 2021-09-05 03:37:33 +00:00
dependabot[bot]
f66bde7275 cargo: bump sha2 from 0.9.5 to 0.9.6
Bumps [sha2](https://github.com/RustCrypto/hashes) from 0.9.5 to 0.9.6.
- [Release notes](https://github.com/RustCrypto/hashes/releases)
- [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.9.5...sha2-v0.9.6)

---
updated-dependencies:
- dependency-name: sha2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-05 03:20:48 +00:00
link2xt
7f819de49f Always check certificate when connecting over SOCKS5 in Automatic mode
There is a real risk of an active attack when connecting to non-.onion
servers over Tor, as bad Tor exit nodes are cheap to set up.

It's probably not needed for .onion domains, but we don't make an
exception for now.
2021-09-05 06:18:54 +03:00
link2xt
5f065b245f Resultification 2021-09-05 06:18:38 +03:00
bjoern
3c43d790a3 update chat/contact data only when there was no newer update (#2642)
* check update timestamps for signatures, user-avatars, ephemeral-settings, last-subject

* check update timestamp for group-avatars

* check update timestamp for group-names

* check update timestamp for memberlist

* check update timestamp for protection-settings

* add a more advanced test

* add another more advanced test

* set last-subject-timestamp more carefully

* bubble up errros from set_*timestamp() and check for from_id==0 before

* simplify Params::set_i64()

* remove comment that is more confusing than helpful

* use update_timestamp() wording consistently
2021-09-04 22:16:39 +02:00
link2xt
d33177a721 accounts: remove Arc and RwLock from Accounts.accounts
Also make Accounts uncloneable. It is still possible to derive Clone,
but does not make sense to do so, as .clone() creates two separate
account managers which use the same files but different unsynchronized
in-memory data structures.
2021-09-04 20:33:11 +03:00
dependabot[bot]
aa2e03382b Merge pull request #2650 from deltachat/dependabot/cargo/thiserror-1.0.28 2021-09-04 15:42:24 +00:00
dependabot[bot]
2a59e6121b Merge pull request #2652 from deltachat/dependabot/cargo/serde_json-1.0.67 2021-09-04 15:40:59 +00:00
dependabot[bot]
1a438d61df Merge pull request #2649 from deltachat/dependabot/cargo/futures-0.3.17 2021-09-04 15:26:52 +00:00
bjoern
444486f5df better readable code fragments on c.delta.chat (#2647)
* better readable code fragments on c.delta.chat

* make it even a bit nicer
2021-09-04 17:10:05 +02:00
dependabot[bot]
1eae2477c3 cargo: bump serde_json from 1.0.66 to 1.0.67
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.66 to 1.0.67.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.66...v1.0.67)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-04 15:02:45 +00:00
dependabot[bot]
7b3eefc6c6 cargo: bump thiserror from 1.0.26 to 1.0.28
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.26 to 1.0.28.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.26...1.0.28)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-04 15:01:34 +00:00
dependabot[bot]
4dd0830baf Merge pull request #2643 from deltachat/dependabot/cargo/sha-1-0.9.8 2021-09-04 15:00:52 +00:00
dependabot[bot]
8e3f062881 cargo: bump futures from 0.3.16 to 0.3.17
Bumps [futures](https://github.com/rust-lang/futures-rs) from 0.3.16 to 0.3.17.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.16...0.3.17)

---
updated-dependencies:
- dependency-name: futures
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-04 15:00:24 +00:00
dependabot[bot]
cf445f265a Merge pull request #2627 from deltachat/dependabot/cargo/surf-2.3.1 2021-09-04 14:58:18 +00:00
dependabot[bot]
963c66b76c Merge pull request #2644 from deltachat/dependabot/cargo/async-std-1.10.0 2021-09-04 14:57:43 +00:00
dependabot[bot]
79df667e1e Merge pull request #2648 from deltachat/dependabot/cargo/mailparse-0.13.6 2021-09-04 14:54:28 +00:00
dependabot[bot]
785c796bd6 Merge pull request #2646 from deltachat/dependabot/cargo/pgp-0.7.2 2021-09-04 14:53:20 +00:00
dependabot[bot]
6a2112ba66 Merge pull request #2622 from deltachat/dependabot/cargo/syn-1.0.75 2021-09-03 09:45:06 +00:00
dependabot[bot]
3f170279da cargo: bump syn from 1.0.74 to 1.0.75
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.74 to 1.0.75.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.74...1.0.75)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-03 09:21:11 +00:00
dependabot[bot]
3408501a75 cargo: bump async-std from 1.9.0 to 1.10.0
Bumps [async-std](https://github.com/async-rs/async-std) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/async-rs/async-std/releases)
- [Changelog](https://github.com/async-rs/async-std/blob/master/CHANGELOG.md)
- [Commits](https://github.com/async-rs/async-std/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: async-std
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-03 09:20:31 +00:00
dependabot[bot]
3b765cb3c9 Merge pull request #2625 from deltachat/dependabot/cargo/fast-socks5-0.4.3 2021-09-03 09:18:55 +00:00
dependabot[bot]
8a9ea388ed Merge pull request #2609 from deltachat/dependabot/cargo/bitflags-1.3.2 2021-09-03 09:15:37 +00:00
dependabot[bot]
77acf910bf cargo: bump bitflags from 1.3.1 to 1.3.2
Bumps [bitflags](https://github.com/bitflags/bitflags) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/1.3.1...1.3.2)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-02 18:41:20 +00:00
dependabot[bot]
c04c87658c cargo: bump mailparse from 0.13.5 to 0.13.6
Bumps [mailparse](https://github.com/staktrace/mailparse) from 0.13.5 to 0.13.6.
- [Release notes](https://github.com/staktrace/mailparse/releases)
- [Commits](https://github.com/staktrace/mailparse/compare/v0.13.5...v0.13.6)

---
updated-dependencies:
- dependency-name: mailparse
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-02 18:39:51 +00:00
dependabot[bot]
fd784ec223 cargo: bump async-trait from 0.1.50 to 0.1.51 (#2572)
Bumps [async-trait](https://github.com/dtolnay/async-trait) from 0.1.50 to 0.1.51.
- [Release notes](https://github.com/dtolnay/async-trait/releases)
- [Commits](https://github.com/dtolnay/async-trait/compare/0.1.50...0.1.51)

---
updated-dependencies:
- dependency-name: async-trait
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-02 20:39:47 +02:00
dependabot[bot]
25f1b0c4af cargo: bump criterion from 0.3.4 to 0.3.5 (#2564)
Bumps [criterion](https://github.com/bheisler/criterion.rs) from 0.3.4 to 0.3.5.
- [Release notes](https://github.com/bheisler/criterion.rs/releases)
- [Changelog](https://github.com/bheisler/criterion.rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bheisler/criterion.rs/compare/0.3.4...0.3.5)

---
updated-dependencies:
- dependency-name: criterion
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-02 20:38:28 +02:00
dependabot[bot]
580ec6e6ce cargo: bump pgp from 0.7.1 to 0.7.2
Bumps [pgp](https://github.com/rpgp/rpgp) from 0.7.1 to 0.7.2.
- [Release notes](https://github.com/rpgp/rpgp/releases)
- [Changelog](https://github.com/rpgp/rpgp/blob/master/release.toml)
- [Commits](https://github.com/rpgp/rpgp/compare/v0.7.1...v0.7.2)

---
updated-dependencies:
- dependency-name: pgp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-01 22:19:33 +00:00
dependabot[bot]
8e5195c4f6 cargo: bump native-tls from 0.2.7 to 0.2.8 (#2597)
Bumps [native-tls](https://github.com/sfackler/rust-native-tls) from 0.2.7 to 0.2.8.
- [Release notes](https://github.com/sfackler/rust-native-tls/releases)
- [Changelog](https://github.com/sfackler/rust-native-tls/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sfackler/rust-native-tls/compare/v0.2.7...v0.2.8)

---
updated-dependencies:
- dependency-name: native-tls
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-02 00:18:14 +02:00
dependabot[bot]
729a1e1cd2 Merge pull request #2641 from deltachat/dependabot/cargo/serde-1.0.130 2021-09-01 20:55:33 +00:00
dependabot[bot]
78b93f3621 cargo: bump serde from 1.0.127 to 1.0.130
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.127 to 1.0.130.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.127...v1.0.130)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-01 20:11:06 +00:00
dependabot[bot]
4111489daf cargo: bump anyhow from 1.0.42 to 1.0.43 (#2610)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.42 to 1.0.43.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.42...1.0.43)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-01 22:09:03 +02:00
dependabot[bot]
b7bd4c6ba7 cargo: bump sha-1 from 0.9.7 to 0.9.8
Bumps [sha-1](https://github.com/RustCrypto/hashes) from 0.9.7 to 0.9.8.
- [Release notes](https://github.com/RustCrypto/hashes/releases)
- [Commits](https://github.com/RustCrypto/hashes/compare/sha-1-v0.9.7...sha-1-v0.9.8)

---
updated-dependencies:
- dependency-name: sha-1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-01 20:08:40 +00:00
dependabot[bot]
83dc0bc2b0 Merge pull request #2632 from deltachat/dependabot/cargo/libc-0.2.101 2021-09-01 20:07:10 +00:00
dependabot[bot]
1679ddddf0 cargo: bump libc from 0.2.98 to 0.2.101
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.98 to 0.2.101.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.98...0.2.101)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-25 21:08:14 +00:00
dependabot[bot]
de258645f4 cargo: bump surf from 2.2.0 to 2.3.1
Bumps [surf](https://github.com/http-rs/surf) from 2.2.0 to 2.3.1.
- [Release notes](https://github.com/http-rs/surf/releases)
- [Changelog](https://github.com/http-rs/surf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/http-rs/surf/compare/v2.2.0...v2.3.1)

---
updated-dependencies:
- dependency-name: surf
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-24 21:12:32 +00:00
dependabot[bot]
b463b602a9 cargo: bump fast-socks5 from 0.4.2 to 0.4.3
Bumps [fast-socks5](https://github.com/dizda/fast-socks5) from 0.4.2 to 0.4.3.
- [Release notes](https://github.com/dizda/fast-socks5/releases)
- [Commits](https://github.com/dizda/fast-socks5/compare/v0.4.2...v0.4.3)

---
updated-dependencies:
- dependency-name: fast-socks5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-23 21:10:21 +00:00
71 changed files with 6289 additions and 3356 deletions

View File

@@ -76,14 +76,13 @@ jobs:
rust: 1.54.0
python: false # Python bindings compilation on Windows is not supported.
# Minimum Supported Rust Version = 1.48.0
# This is the Debian "bullseye" release version of Rust.
# Minimum Supported Rust Version = 1.51.0
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
- os: ubuntu-latest
rust: 1.48.0
rust: 1.51.0
python: 3.7
runs-on: ${{ matrix.os }}
steps:

1
.gitignore vendored
View File

@@ -28,3 +28,4 @@ deltachat-ffi/xml
.rsynclist
coverage/
.DS_Store

View File

@@ -1,4 +1,42 @@
# Changelog
# Changelog
## 1.61.0
### API Changes
- download-on-demand added: `dc_msg_get_download_status()`, `dc_download_full_msg()`
and `download_limit` config option #2631 #2696
- `dc_create_broadcast_list()` and chat type `DC_CHAT_TYPE_BROADCAST` added #2707 #2722
- allow ui-specific configs: `dc_set_ui_config()` and `dc_get_ui_config()` #2672
- new strings from `DC_STR_PARTIAL_DOWNLOAD_MSG_BODY`
to `DC_STR_PART_OF_TOTAL_USED` #2631 #2694 #2707 #2723
- emit warnings and errors from account manager with account-id 0 #2712
### Changes
- notify about incoming contact requests #2690
- messages are marked as read on first read receipt #2699
- quota warning reappears after import, rewarning at 95% #2702
- lock strict TLS if certificate checks are automatic #2711
- always check certificates strictly when connecting over SOCKS5 in Automatic mode #2657
- `Accounts` is not cloneable anymore #2654 #2658
- update chat/contact data only when there was no newer update #2642
- better detection of mailing list names #2665 #2685
- log all decisions when applying ephemeral timer to chats #2679
- connectivity view now translatable #2694 #2723
- improve Doxygen documentation #2647 #2668 #2684 #2688 #2705
- refactorings #2656 #2659 #2677 #2673 #2678 #2675 #2663 #2692 #2706
- update provider database #2618
### Fixes
- ephemeral timer rollback protection #2693 #2709
- recreate configured folders if they are deleted #2691
- ignore MDNs sent to self #2674
- recognize NDNs that put headers into "message/global-headers" part #2598
- avoid `dc_get_contacts()` returning duplicate contact ids #2591
- do not leak group names on forwarding messages #2719
- in case of smtp-errors, iterate over all addresses to fix ipv6/v4 problems #2720
- fix pkg-config file #2660
- fix "QR process failed" error #2725
## 1.60.0
@@ -13,6 +51,7 @@
### Fixes
- keep event emitter from closing when there are no accounts #2636
## 1.59.0
### Added
@@ -37,7 +76,6 @@
## 1.58.0
### Fixes
- move WAL file together with database
and avoid using data if the database was not closed correctly before #2583

View File

@@ -1,5 +1,6 @@
cmake_minimum_required(VERSION 3.16)
project(deltachat LANGUAGES C)
include(GNUInstallDirs)
find_program(CARGO cargo)
@@ -35,7 +36,6 @@ add_custom_target(
"target/release/pkgconfig/deltachat.pc"
)
include(GNUInstallDirs)
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/libdeltachat.so" DESTINATION ${CMAKE_INSTALL_LIBDIR})

989
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,10 @@
[package]
name = "deltachat"
version = "1.60.0"
version = "1.61.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
resolver = "2"
[profile.dev]
debug = 0
@@ -15,76 +16,76 @@ lto = true
deltachat_derive = { path = "./deltachat_derive" }
ansi_term = { version = "0.12.1", optional = true }
anyhow = "1.0.42"
anyhow = "1"
async-imap = { git = "https://github.com/async-email/async-imap" }
async-native-tls = { version = "0.3.3" }
async-native-tls = { version = "0.3" }
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", features = ["socks5"] }
async-std-resolver = "0.20.3"
async-std = { version = "~1.9.0", features = ["unstable"] }
async-tar = "0.3.0"
async-trait = "0.1.50"
backtrace = "0.3.59"
async-std-resolver = "0.20"
async-std = { version = "1", features = ["unstable"] }
async-tar = "0.4"
async-trait = "0.1"
backtrace = "0.3"
base64 = "0.13"
bitflags = "1.3.1"
byteorder = "1.3.1"
chrono = "0.4.6"
dirs = { version = "3.0.2", optional=true }
bitflags = "1.3"
byteorder = "1.3"
chrono = "0.4"
dirs = { version = "4", optional=true }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
escaper = "0.1.1"
futures = "0.3.16"
escaper = "0.1"
futures = "0.3"
hex = "0.4.0"
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
indexmap = "1.7.0"
itertools = "0.10.1"
indexmap = "1.7"
itertools = "0.10"
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2.98"
libc = "0.2"
log = {version = "0.4.8", optional = true }
mailparse = "0.13.5"
native-tls = "0.2.3"
num_cpus = "1.13.0"
num-derive = "0.3.0"
num-traits = "0.2.6"
mailparse = "0.13"
native-tls = "0.2"
num_cpus = "1.13"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.8.0"
percent-encoding = "2.0"
pgp = { version = "0.7.0", default-features = false }
pretty_env_logger = { version = "0.4.0", optional = true }
quick-xml = "0.22.0"
r2d2 = "0.8.9"
r2d2_sqlite = "0.18.0"
rand = "0.7.0"
regex = "1.4.6"
pgp = { version = "0.7", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
quick-xml = "0.22"
r2d2 = "0.8"
r2d2_sqlite = "0.18"
rand = "0.7"
regex = "1.5"
rusqlite = "0.25"
rust-hsluv = "0.1.4"
rustyline = { version = "8.2.0", optional = true }
sanitize-filename = "0.3.0"
rust-hsluv = "0.1"
rustyline = { version = "9.0", optional = true }
sanitize-filename = "0.3"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.9.7"
sha2 = "0.9.5"
smallvec = "1.0.0"
stop-token = "0.2.0"
strum = "0.21.0"
strum_macros = "0.21.1"
surf = { version = "2.0.0-alpha.4", default-features = false, features = ["h1-client"] }
thiserror = "1.0.26"
toml = "0.5.6"
url = "2.2.2"
sha-1 = "0.9"
sha2 = "0.9"
smallvec = "1"
stop-token = "0.5"
strum = "0.22"
strum_macros = "0.22"
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
thiserror = "1"
toml = "0.5"
url = "2"
uuid = { version = "0.8", features = ["serde", "v4"] }
fast-socks5 = "0.4.2"
humansize = "1.1.1"
fast-socks5 = "0.4"
humansize = "1"
[dev-dependencies]
ansi_term = "0.12.0"
async-std = { version = "1.9.0", features = ["unstable", "attributes"] }
async-std = { version = "1", features = ["unstable", "attributes"] }
criterion = "0.3"
futures-lite = "1.12.0"
log = "0.4.11"
pretty_assertions = "0.7.2"
pretty_env_logger = "0.4.0"
proptest = "1.0"
tempfile = "3.0"
futures-lite = "1.12"
log = "0.4"
pretty_assertions = "1.0"
pretty_env_logger = "0.4"
proptest = "1"
tempfile = "3"
[workspace]
members = [

BIN
assets/icon-broadcast.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

149
assets/icon-broadcast.svg Normal file
View File

@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
enable-background="new 0 0 128 128"
viewBox="0 0 60 60"
version="1.1"
id="svg878"
sodipodi:docname="icon-broadcast.svg"
width="60"
height="60"
inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
inkscape:export-filename="/Users/bpetersen/projects/deltachat-core-rust/assets/icon-broadcast.png"
inkscape:export-xdpi="409.60001"
inkscape:export-ydpi="409.60001">
<metadata
id="metadata884">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs882" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1329"
inkscape:window-height="847"
id="namedview880"
showgrid="false"
inkscape:zoom="5.21875"
inkscape:cx="36.598802"
inkscape:cy="32.191617"
inkscape:window-x="111"
inkscape:window-y="205"
inkscape:window-maximized="0"
inkscape:current-layer="svg878"
inkscape:document-rotation="0" />
<radialGradient
id="c"
cx="65.25"
cy="89"
r="26.440001"
gradientTransform="matrix(0.77611266,0.11996647,-0.18999676,1.2286617,-11.305867,-60.065999)"
gradientUnits="userSpaceOnUse">
<stop
stop-color="#FFC107"
offset="0"
id="stop833" />
<stop
stop-color="#FFBD06"
offset=".3502"
id="stop835" />
<stop
stop-color="#FFB104"
offset=".6938"
id="stop837" />
<stop
stop-color="#FFA000"
offset="1"
id="stop839" />
</radialGradient>
<radialGradient
id="b"
cx="52.5"
cy="19.75"
r="92.975998"
gradientUnits="userSpaceOnUse"
gradientTransform="rotate(45.323856,68.997115,75.979538)">
<stop
stop-color="#EF5350"
offset="0"
id="stop848" />
<stop
stop-color="#EB4F4C"
offset=".246"
id="stop850" />
<stop
stop-color="#E04341"
offset=".4878"
id="stop852" />
<stop
stop-color="#CD302F"
offset=".7272"
id="stop854" />
<stop
stop-color="#C62828"
offset=".8004"
id="stop856" />
<stop
stop-color="#C62828"
offset="1"
id="stop858" />
</radialGradient>
<radialGradient
id="a"
cx="16.979"
cy="92"
r="24.165001"
gradientUnits="userSpaceOnUse"
gradientTransform="rotate(45.323856,68.997115,75.979538)"
xlink:href="#b">
<stop
stop-color="#E0E0E0"
offset="0"
id="stop863" />
<stop
stop-color="#CFCFCF"
offset=".3112"
id="stop865" />
<stop
stop-color="#A4A4A4"
offset=".9228"
id="stop867" />
<stop
stop-color="#9E9E9E"
offset="1"
id="stop869" />
</radialGradient>
<rect
y="0"
x="0"
height="60"
width="60"
id="rect1420"
style="fill:#7cc0bc;fill-opacity:1;stroke:none;stroke-width:1.29077" />
<path
id="path872"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.336872;stroke-opacity:1"
d="m 8.6780027,35.573064 0.032831,-11.910176 c 0.00138,-0.476406 0.4881282,-0.794259 0.9235226,-0.604877 l 4.1144877,2.345752 -0.02386,8.656315 -4.1268029,2.122946 C 9.1617452,36.370003 8.6766889,36.049472 8.6780027,35.573064 Z m 5.0469633,-1.508222 0.02386,-8.656314 31.145424,-9.537653 c 0.841472,-0.219211 1.65915,0.41667 1.656755,1.283728 l -0.06929,25.139995 c -0.0024,0.867062 -0.825942,1.500799 -1.663803,1.274581 z m 3.8042,6.892234 C 16.681121,40.104348 16.315444,38.819414 16.69043,37.591308 l 2.252234,-7.347193 c 0.2644,-0.861571 0.845185,-1.567441 1.641953,-1.989251 0.796769,-0.421808 1.706956,-0.509819 2.568531,-0.245419 l 7.263888,2.225804 c 1.775518,0.543235 2.780299,2.432591 2.232297,4.208094 L 30.3971,41.790532 c -0.545627,1.777887 -2.432591,2.780297 -4.208095,2.232298 l -7.263891,-2.225804 c -0.545033,-0.165864 -1.01825,-0.460162 -1.395948,-0.83995 z m 12.377693,-7.976728 c -0.07601,-0.07642 -0.17114,-0.133864 -0.280621,-0.167516 l -7.263891,-2.225803 c -0.233244,-0.07209 -0.421626,0.0013 -0.512275,0.04861 -0.09064,0.0474 -0.25772,0.166033 -0.327435,0.396899 l -2.252234,7.347191 c -0.108166,0.354628 0.09088,0.731541 0.447888,0.842099 l 7.263891,2.225802 c 0.354626,0.108174 0.731539,-0.09088 0.842099,-0.447888 l 2.249845,-7.344814 c 0.07453,-0.245145 0.0014,-0.504991 -0.167267,-0.67458 z" />
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.60.0"
version = "1.61.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -17,13 +17,13 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
deltachat = { path = "../", default-features = false }
libc = "0.2"
human-panic = "1.0.1"
num-traits = "0.2.6"
human-panic = "1"
num-traits = "0.2"
serde_json = "1.0"
async-std = "1.9.0"
anyhow = "1.0.42"
thiserror = "1.0.26"
rand = "0.7.3"
async-std = "1"
anyhow = "1"
thiserror = "1"
rand = "0.7"
[features]
default = ["vendored"]

View File

@@ -583,7 +583,7 @@ SORT_MEMBERS_CTORS_1ST = NO
# appear in their defined order.
# The default value is: NO.
SORT_GROUP_NAMES = NO
SORT_GROUP_NAMES = YES
# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
# fully-qualified names, including namespaces. If set to NO, the class list will

View File

@@ -4,4 +4,16 @@ div.fragment {
background-color: #e0e0e0;
border: 0;
padding: 1em;
border-radius: 6px;
}
code {
background-color: #e0e0e0;
padding-left: .5em;
padding-right: .5em;
border-radius: 6px;
}
li {
margin-bottom: .5em;
}

View File

@@ -351,6 +351,19 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
* 0=do not fetch existing messages on configure.
* In both cases, existing recipients are added to the contact database.
* - `download_limit` = Messages up to this number of bytes are downloaded automatically.
* For larger messages, only the header is downloaded and a placeholder is shown.
* These messages can be downloaded fully using dc_download_full_msg() later.
* The limit is compared against raw message sizes, including headers.
* The actually used limit may be corrected
* to not mess up with non-delivery-reports or read-receipts.
* 0=no limit (default).
* Changes affect future messages only.
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
* The prefix should be followed by the system and maybe subsystem,
* eg. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
* These keys go to backups and allow easy per-account settings when using @ref dc_accounts_t,
* however, are not handled by the core otherwise.
*
* If you want to retrieve a value, use dc_get_config().
*
@@ -586,6 +599,14 @@ void dc_configure (dc_context_t* context);
* Typically, for unconfigured accounts, the user is prompted
* to enter some settings and dc_configure() is called in a thread then.
*
* A once successfully configured context cannot become unconfigured again;
* if a subsequent call to dc_configure() fails,
* the prior configuration is used.
*
* However, of course, also a configuration may stop working,
* as eg. the password was changed on the server.
* To check that use eg. dc_get_connectivity().
*
* @memberof dc_context_t
* @param context The context object.
* @return 1=context is configured and can be used;
@@ -907,7 +928,7 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
* dc_send_videochat_invitation() is blocking and may take a while,
* so the UIs will typically call the function from within a thread.
* Moreover, UIs will typically enter the room directly without an additional click on the message,
* for this purpose, the function returns the message-id directly.
* for this purpose, the function returns the message id directly.
*
* As for other messages sent, this function
* sends the event #DC_EVENT_MSGS_CHANGED on success, the message has a delivery state, and so on.
@@ -1278,6 +1299,8 @@ void dc_accept_chat (dc_context_t* context, uint32_t ch
* explicitly as it may happen that oneself gets removed from a still existing
* group
*
* - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included
*
* - for mailing lists, the behavior is not documented currently, we will decide on that later.
* for now, the UI should not show the list for mailing lists.
* (we do not know all members and there is not always a global mailing list address,
@@ -1387,6 +1410,36 @@ dc_chat_t* dc_get_chat (dc_context_t* context, uint32_t ch
uint32_t dc_create_group_chat (dc_context_t* context, int protect, const char* name);
/**
* Create a new broadcast list.
*
* Broadcast lists are similar to groups on the sending device,
* however, recipients get the messages in normal one-to-one chats
* and will not be aware of other members.
*
* Replies to broadcasts go only to the sender
* and not to all broadcast recipients.
* Moreover, replies will not appear in the broadcast list
* but in the one-to-one chat with the person answering.
*
* The name and the image of the broadcast list is set automatically
* and is visible to the sender only.
* Not asking for these data allows more focused creation
* and we bypass the question who will get which data.
* Also, many users will have at most one broadcast list
* so, a generic name and image is sufficient at the first place.
*
* Later on, however, the name can be changed using dc_set_chat_name().
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
* All in all, this is also what other messengers are doing here.
*
* @memberof dc_context_t
* @param context The context object.
* @return The chat ID of the new broadcast list, 0 on errors.
*/
uint32_t dc_create_broadcast_list (dc_context_t* context);
/**
* Check if a given contact ID is a member of a group chat.
*
@@ -1584,6 +1637,28 @@ char* dc_get_msg_info (dc_context_t* context, uint32_t ms
char* dc_get_msg_html (dc_context_t* context, uint32_t msg_id);
/**
* Asks the core to start downloading a message fully.
* This function is typically called when the user hits the "Download" button
* that is shown by the UI in case dc_msg_get_download_state()
* returns @ref DC_DOWNLOAD_AVAILABLE or @ref DC_DOWNLOAD_FAILURE.
*
* On success, the @ref DC_MSG "view type of the message" may change
* or the message may be replaced completely by one or more messages with other message ids.
* That may happen eg. in cases where the message was encrypted
* and the type could not be determined without fully downloading.
* Downloaded content can be accessed as usual after download,
* eg. using dc_msg_get_file().
*
* To reflect these changes a @ref DC_EVENT_MSGS_CHANGED event will be emitted.
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id Message ID to download the content for.
*/
void dc_download_full_msg (dc_context_t* context, int msg_id);
/**
* Get the raw mime-headers of the given message.
* Raw headers are saved for incoming messages
@@ -1634,7 +1709,7 @@ void dc_forward_msgs (dc_context_t* context, const uint3
*
* - For normal chats, the IMAP state is updated, MDN is sent
* (if dc_set_config()-options `mdns_enabled` is set)
* and the internal state is changed to DC_STATE_IN_SEEN to reflect these actions.
* and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
*
* - For contact requests, no IMAP or MDNs is done
* and the internal state is not changed therefore.
@@ -2055,7 +2130,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_FPR_MISMATCH 220 // id=contact
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
#define DC_QR_ACCOUNT 250 // text1=domain
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
#define DC_QR_ADDR 320 // id=contact
#define DC_QR_TEXT 330 // text1=text
#define DC_QR_URL 332 // text1=URL
@@ -2731,13 +2806,13 @@ uint32_t dc_array_get_contact_id (const dc_array_t* array, size_t in
/**
* Return the message-id of the item at the given index.
* Return the message id of the item at the given index.
*
* @memberof dc_array_t
* @param array The array object.
* @param index Index of the item. Must be between 0 and dc_array_get_cnt()-1.
* @return Message-id of the item at the given index.
* 0 if there is no message-id bound to the given item,
* @return Message id of the item at the given index.
* 0 if there is no message id bound to the given item,
*/
uint32_t dc_array_get_msg_id (const dc_array_t* array, size_t index);
@@ -2883,7 +2958,7 @@ uint32_t dc_chatlist_get_msg_id (const dc_chatlist_t* chatlist, siz
*
* - 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.
* - dc_lot_t::state: The state of the message as one of the @ref DC_STATE constants. 0 if not applicable.
*
* @memberof dc_chatlist_t
* @param chatlist The chatlist to query as returned e.g. from dc_get_chatlist().
@@ -2899,7 +2974,7 @@ dc_lot_t* dc_chatlist_get_summary (const dc_chatlist_t* chatlist, siz
* Create a chatlist summary item when the chatlist object is already unref()'d.
*
* This function is similar to dc_chatlist_get_summary(), however,
* takes the chat-id and message-id as returned by dc_chatlist_get_chat_id() and dc_chatlist_get_msg_id()
* takes the chat-id and message id as returned by dc_chatlist_get_chat_id() and dc_chatlist_get_msg_id()
* as arguments. The chatlist object itself is not needed directly.
*
* This maybe useful if you convert the complete object into a different represenation
@@ -2937,7 +3012,7 @@ dc_context_t* dc_chatlist_get_context (dc_chatlist_t* chatlist);
* color: color of this chat
* last-message-from: who sent the last message
* last-message-text: message (truncated)
* last-message-state: DC_STATE* constant
* last-message-state: @ref DC_STATE constant
* last-message-date:
* avatar-path: path-to-blobfile
* is_verified: yes/no
@@ -2961,12 +3036,6 @@ char* dc_chat_get_info_json (dc_context_t* context, size_t chat
#define DC_CHAT_ID_LAST_SPECIAL 9 // larger chat IDs are "real" chats, their messages are "real" messages.
#define DC_CHAT_TYPE_UNDEFINED 0
#define DC_CHAT_TYPE_SINGLE 100
#define DC_CHAT_TYPE_GROUP 120
#define DC_CHAT_TYPE_MAILINGLIST 140
/**
* Free a chat object.
*
@@ -2993,21 +3062,30 @@ uint32_t dc_chat_get_id (const dc_chat_t* chat);
/**
* Get chat type.
* Get chat type as one of the @ref DC_CHAT_TYPE constants:
*
* Currently, there are two chat types:
*
* - DC_CHAT_TYPE_SINGLE (100) - a normal chat is a chat with a single contact,
* - @ref DC_CHAT_TYPE_SINGLE - a normal chat is a chat with a single contact,
* chats_contacts contains one record for the user. DC_CONTACT_ID_SELF
* (see dc_contact_t::id) is added _only_ for a self talk.
* These chats are created by dc_create_chat_by_contact_id().
*
* - DC_CHAT_TYPE_GROUP (120) - a group chat, chats_contacts contain all group
* members, incl. DC_CONTACT_ID_SELF
* - @ref DC_CHAT_TYPE_GROUP - a group chat, chats_contacts contain all group
* members, incl. DC_CONTACT_ID_SELF.
* Groups are created by dc_create_group_chat().
*
* - DC_CHAT_TYPE_MAILINGLIST (140) - a mailing list, this is similar to groups,
* - @ref DC_CHAT_TYPE_MAILINGLIST - a mailing list, this is similar to groups,
* however, the member list cannot be retrieved completely
* and cannot be changed using this api.
* moreover, for now, mailist lists are read-only.
* Mailing lists are created as needed by incoming messages
* and usually require some special server;
* they cannot be created by a function call as the other chat types.
* Moreover, for now, mailing lists are read-only.
*
* - @ref DC_CHAT_TYPE_BROADCAST - a broadcast list,
* the recipients will get messages in a one-to-one chats and
* the sender will get answers in a one-to-one as well.
* chats_contacts contain all recipients but DC_CONTACT_ID_SELF.
* Broadcasts are created by dc_create_broadcast_list().
*
* @memberof dc_chat_t
* @param chat The chat object.
@@ -3216,18 +3294,6 @@ int64_t dc_chat_get_remaining_mute_duration (const dc_chat_t* chat);
#define DC_MSG_ID_LAST_SPECIAL 9
#define DC_STATE_UNDEFINED 0
#define DC_STATE_IN_FRESH 10
#define DC_STATE_IN_NOTICED 13
#define DC_STATE_IN_SEEN 16
#define DC_STATE_OUT_PREPARING 18
#define DC_STATE_OUT_DRAFT 19
#define DC_STATE_OUT_PENDING 20
#define DC_STATE_OUT_FAILED 24
#define DC_STATE_OUT_DELIVERED 26 // to check if a mail was sent, use dc_msg_is_sent()
#define DC_STATE_OUT_MDN_RCVD 28
/**
* Create new message object. Message objects are needed e.g. for sending messages using
* dc_send_msg(). Moreover, they are returned e.g. from dc_get_msg(),
@@ -3307,28 +3373,37 @@ int dc_msg_get_viewtype (const dc_msg_t* msg);
* Get the state of a message.
*
* Incoming message states:
* - DC_STATE_IN_FRESH (10) - Incoming _fresh_ message. Fresh messages are neither noticed nor seen and are typically shown in notifications. Use dc_get_fresh_msgs() to get all fresh messages.
* - DC_STATE_IN_NOTICED (13) - Incoming _noticed_ message. E.g. chat opened but message not yet read - noticed messages are not counted as unread but were not marked as read nor resulted in MDNs. Use dc_marknoticed_chat() to mark messages as being noticed.
* - DC_STATE_IN_SEEN (16) - Incoming message, really _seen_ by the user. Marked as read on IMAP and MDN may be sent. Use dc_markseen_msgs() to mark messages as being seen.
* - @ref DC_STATE_IN_FRESH - Incoming _fresh_ message.
* Fresh messages are neither noticed nor seen and are typically shown in notifications.
* Use dc_get_fresh_msgs() to get all fresh messages.
* - @ref DC_STATE_IN_NOTICED - Incoming _noticed_ message.
* E.g. chat opened but message not yet read.
* Noticed messages are not counted as unread but were not marked as read nor resulted in MDNs.
* Use dc_marknoticed_chat() to mark messages as being noticed.
* - @ref DC_STATE_IN_SEEN - Incoming message, really _seen_ by the user.
* Marked as read on IMAP and MDN may be sent. Use dc_markseen_msgs() to mark messages as being seen.
*
* Outgoing message states:
* - DC_STATE_OUT_PREPARING (18) - For files which need time to be prepared before they can be sent,
* the message enters this state before DC_STATE_OUT_PENDING.
* - DC_STATE_OUT_DRAFT (19) - Message saved as draft using dc_set_draft()
* - DC_STATE_OUT_PENDING (20) - The user has pressed the "send" button but the
* - @ref DC_STATE_OUT_PREPARING - For files which need time to be prepared before they can be sent,
* the message enters this state before @ref DC_STATE_OUT_PENDING.
* - @ref DC_STATE_OUT_DRAFT - Message saved as draft using dc_set_draft()
* - @ref DC_STATE_OUT_PENDING - The user has pressed the "send" button but the
* message is not yet sent and is pending in some way. Maybe we're offline (no checkmark).
* - DC_STATE_OUT_FAILED (24) - _Unrecoverable_ error (_recoverable_ errors result in pending messages), you'll receive the event #DC_EVENT_MSG_FAILED.
* - DC_STATE_OUT_DELIVERED (26) - Outgoing message successfully delivered to server (one checkmark). Note, that already delivered messages may get into the state DC_STATE_OUT_FAILED if we get such a hint from the server.
* - @ref DC_STATE_OUT_FAILED - _Unrecoverable_ error (_recoverable_ errors result in pending messages),
* you'll receive the event #DC_EVENT_MSG_FAILED.
* - @ref DC_STATE_OUT_DELIVERED - Outgoing message successfully delivered to server (one checkmark).
* Note, that already delivered messages may get into the state @ref DC_STATE_OUT_FAILED if we get such a hint from the server.
* If a sent message changes to this state, you'll receive the event #DC_EVENT_MSG_DELIVERED.
* - DC_STATE_OUT_MDN_RCVD (28) - Outgoing message read by the recipient (two checkmarks; this requires goodwill on the receiver's side)
* - @ref DC_STATE_OUT_MDN_RCVD - Outgoing message read by the recipient
* (two checkmarks; this requires goodwill on the receiver's side)
* If a sent message changes to this state, you'll receive the event #DC_EVENT_MSG_READ.
* Also messages already read by some recipients
* may get into the state DC_STATE_OUT_FAILED at a later point,
* may get into the state @ref DC_STATE_OUT_FAILED at a later point,
* e.g. when in a group, delivery fails for some recipients.
*
* If you just want to check if a message is sent or not, please use dc_msg_is_sent() which regards all states accordingly.
*
* The state of just created message objects is DC_STATE_UNDEFINED (0).
* The state of just created message objects is @ref DC_STATE_UNDEFINED.
* The state is always set by the core-library, users of the library cannot set the state directly, but it is changed implicitly e.g.
* when calling dc_marknoticed_chat() or dc_markseen_msgs().
*
@@ -3592,7 +3667,7 @@ int64_t dc_msg_get_ephemeral_timestamp (const dc_msg_t* msg);
* Typically used to show dc_lot_t::text1 with different colors. 0 if not applicable.
* - dc_lot_t::text2: contains an excerpt of the message text.
* - dc_lot_t::timestamp: the timestamp of the message.
* - dc_lot_t::state: The state of the message as one of the DC_STATE_* constants (see #dc_msg_get_state()).
* - dc_lot_t::state: The state of the message as one of the @ref DC_STATE constants.
*
* Typically used to display a search result. See also dc_chatlist_get_summary() to display a list of chats.
*
@@ -3906,6 +3981,31 @@ int dc_msg_get_videochat_type (const dc_msg_t* msg);
int dc_msg_has_html (dc_msg_t* msg);
/**
* Check if the message is completely downloaded
* or if some further action is needed.
*
* Messages may be not fully downloaded
* if they are larger than the limit set by the dc_set_config()-option `download_limit`.
*
* The function returns one of:
* - @ref DC_DOWNLOAD_DONE - The message does not need any further download action
* and should be rendered as usual.
* - @ref DC_DOWNLOAD_AVAILABLE - There is additional content to download.
* In addition to the usual message rendering,
* the UI shall show a download button that calls dc_download_full_msg()
* - @ref DC_DOWNLOAD_IN_PROGRESS - Download was started with dc_download_full_msg() and is still in progress.
* If the download fails or succeeds,
* the event @ref DC_EVENT_MSGS_CHANGED is emitted.
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_download_full_msg() again.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return One of the @ref DC_DOWNLOAD values
*/
int dc_msg_get_download_state (const dc_msg_t* msg);
/**
* Set the text of a message object.
* This does not alter any information in the database; this may be done by dc_send_msg() later.
@@ -4588,6 +4688,115 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
/**
* @defgroup DC_STATE DC_STATE
*
* These constants describe the state of a message.
* The state can be retrieved using dc_msg_get_state()
* and may change by various actions reported by various events
*
* @addtogroup DC_STATE
* @{
*/
/**
* Message just created. See dc_msg_get_state() for details.
*/
#define DC_STATE_UNDEFINED 0
/**
* Incoming fresh message. See dc_msg_get_state() for details.
*/
#define DC_STATE_IN_FRESH 10
/**
* Incoming noticed message. See dc_msg_get_state() for details.
*/
#define DC_STATE_IN_NOTICED 13
/**
* Incoming seen message. See dc_msg_get_state() for details.
*/
#define DC_STATE_IN_SEEN 16
/**
* Outgoing message being prepared. See dc_msg_get_state() for details.
*/
#define DC_STATE_OUT_PREPARING 18
/**
* Outgoing message drafted. See dc_msg_get_state() for details.
*/
#define DC_STATE_OUT_DRAFT 19
/**
* Outgoing message waiting to be sent. See dc_msg_get_state() for details.
*/
#define DC_STATE_OUT_PENDING 20
/**
* Outgoing message failed sending. See dc_msg_get_state() for details.
*/
#define DC_STATE_OUT_FAILED 24
/**
* Outgoing message sent. To check if a mail was actually sent, use dc_msg_is_sent().
* See dc_msg_get_state() for details.
*/
#define DC_STATE_OUT_DELIVERED 26
/**
* Outgoing message sent and seen by recipients(s). See dc_msg_get_state() for details.
*/
#define DC_STATE_OUT_MDN_RCVD 28
/**
* @}
*/
/**
* @defgroup DC_CHAT_TYPE DC_CHAT_TYPE
*
* These constants describe the type of a chat.
* The chat type can be retrieved using dc_chat_get_type()
* and the type does not change during the chat's lifetime.
*
* @addtogroup DC_CHAT_TYPE
* @{
*/
/**
* Undefined chat type.
* Normally, this type is not returned.
*/
#define DC_CHAT_TYPE_UNDEFINED 0
/**
* A one-to-one chat with a single contact. See dc_chat_get_type() for details.
*/
#define DC_CHAT_TYPE_SINGLE 100
/**
* A group chat. See dc_chat_get_type() for details.
*/
#define DC_CHAT_TYPE_GROUP 120
/**
* A mailing list. See dc_chat_get_type() for details.
*/
#define DC_CHAT_TYPE_MAILINGLIST 140
/**
* A broadcast list. See dc_chat_get_type() for details.
*/
#define DC_CHAT_TYPE_BROADCAST 160
/**
* @}
*/
/**
* @defgroup DC_SOCKET DC_SOCKET
*
@@ -4832,7 +5041,7 @@ char* dc_event_get_data2_str(dc_event_t* event);
*
* @memberof dc_event_t
* @param event Event object as returned from dc_accounts_get_next_event().
* @return account-id belonging to the event or 0 for errors.
* @return account-id belonging to the event, 0 for account manager errors.
*/
uint32_t dc_event_get_account_id(dc_event_t* event);
@@ -4980,8 +5189,8 @@ void dc_event_unref(dc_event_t* event);
* - Chats created, deleted or archived
* - A draft has been set
*
* @param data1 (int) chat_id for single added messages
* @param data2 (int) msg_id for single added messages
* @param data1 (int) chat_id if only a single chat is affected by the changes, otherwise 0
* @param data2 (int) msg_id if only a single message is affected by the changes, otherwise 0
*/
#define DC_EVENT_MSGS_CHANGED 2000
@@ -5014,8 +5223,8 @@ void dc_event_unref(dc_event_t* event);
/**
* A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
* DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
* A single message is sent successfully. State changed from @ref DC_STATE_OUT_PENDING to
* @ref DC_STATE_OUT_DELIVERED.
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
@@ -5025,8 +5234,8 @@ void dc_event_unref(dc_event_t* event);
/**
* A single message could not be sent.
* State changed from DC_STATE_OUT_PENDING, DC_STATE_OUT_DELIVERED or DC_STATE_OUT_MDN_RCVD
* to DC_STATE_OUT_FAILED, see dc_msg_get_state().
* State changed from @ref DC_STATE_OUT_PENDING, @ref DC_STATE_OUT_DELIVERED or @ref DC_STATE_OUT_MDN_RCVD
* to @ref DC_STATE_OUT_FAILED.
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
@@ -5035,8 +5244,8 @@ void dc_event_unref(dc_event_t* event);
/**
* A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
* DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
* A single message is read by the receiver. State changed from @ref DC_STATE_OUT_DELIVERED to
* @ref DC_STATE_OUT_MDN_RCVD.
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
@@ -5057,6 +5266,9 @@ void dc_event_unref(dc_event_t* event);
/**
* Chat ephemeral timer changed.
*
* @param data1 (int) chat_id
* @param data2 (int) Timer value in seconds or 0 for disabled timer
*/
#define DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED 2021
@@ -5280,6 +5492,44 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_CHAT_VISIBILITY_PINNED 2
/**
* @}
*/
/**
* @defgroup DC_DOWNLOAD DC_DOWNLOAD
*
* These constants describe the download state of a message.
* The download state can be retrieved using dc_msg_get_download_state()
* and usually changes after calling dc_download_full_msg().
*
* @addtogroup DC_DOWNLOAD
* @{
*/
/**
* Download not needed, see dc_msg_get_download_state() for details.
*/
#define DC_DOWNLOAD_DONE 0
/**
* Download available, see dc_msg_get_download_state() for details.
*/
#define DC_DOWNLOAD_AVAILABLE 10
/**
* Download failed, see dc_msg_get_download_state() for details.
*/
#define DC_DOWNLOAD_FAILURE 20
/**
* Download in progress, see dc_msg_get_download_state() for details.
*/
#define DC_DOWNLOAD_IN_PROGRESS 1000
/**
* @}
*/
@@ -5685,6 +5935,96 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced by the percentage used
#define DC_STR_QUOTA_EXCEEDING_MSG_BODY 98
/// "%1$s message"
///
/// Used as the message body when a message
/// was not yet downloaded completely
/// (dc_msg_get_download_state() is eg. @ref DC_DOWNLOAD_AVAILABLE).
///
/// `%1$s` will be replaced by human-readable size (eg. "1.2 MiB").
#define DC_STR_PARTIAL_DOWNLOAD_MSG_BODY 99
/// "Download maximum available until %1$s"
///
/// Appended after some separator to @ref DC_STR_PARTIAL_DOWNLOAD_MSG_BODY.
///
/// `%1$s` will be replaced by human-readable date and time.
#define DC_STR_DOWNLOAD_AVAILABILITY 100
/// "Incoming Messages"
///
/// Used as a headline in the connectivity view.
#define DC_STR_INCOMING_MESSAGES 103
/// "Outgoing Messages"
///
/// Used as a headline in the connectivity view.
#define DC_STR_OUTGOING_MESSAGES 104
/// "Storage on %1$s"
///
/// Used as a headline in the connectivity view.
///
/// `%1$s` will be replaced by the domain of the configured email-address.
#define DC_STR_STORAGE_ON_DOMAIN 105
/// "One moment…"
///
/// Used in the connectivity view when some information are not yet there.
#define DC_STR_ONE_MOMENT 106
/// "Connected"
///
/// Used as status in the connectivity view.
#define DC_STR_CONNECTED 107
/// "Connecting…"
///
/// Used as status in the connectivity view.
#define DC_STR_CONNTECTING 108
/// "Updating…"
///
/// Used as status in the connectivity view.
#define DC_STR_UPDATING 109
/// "Sending…"
///
/// Used as status in the connectivity view.
#define DC_STR_SENDING 110
/// "Your last message was sent successfully."
///
/// Used as status in the connectivity view.
#define DC_STR_LAST_MSG_SENT_SUCCESSFULLY 111
/// "Error: %1$s"
///
/// Used as status in the connectivity view.
///
/// `%1$s` will be replaced by a possibly more detailed, typically english, error description.
#define DC_STR_ERROR 112
/// "Not supported by your provider."
///
/// Used in the connectivity view.
#define DC_STR_NOT_SUPPORTED_BY_PROVIDER 113
/// "Messages"
///
/// Used as a subtitle in quota context; can be plural always.
#define DC_STR_MESSAGES 114
/// "Broadcast List"
///
/// Used as the default name for broadcast lists; a number may be added.
#define DC_STR_BROADCAST_LIST 115
/// "%1$s of %2$s used"
///
/// Used for describing resource usage, resulting string will be eg. "1.2 GiB of 3 GiB used".
#define DC_STR_PART_OF_TOTAL_USED 116
/**
* @}
*/

View File

@@ -15,13 +15,15 @@ extern crate num_traits;
extern crate serde_json;
use std::collections::BTreeMap;
use std::convert::TryInto;
use std::convert::TryFrom;
use std::fmt::Write;
use std::ops::Deref;
use std::ptr;
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use anyhow::Context as _;
use async_std::sync::RwLock;
use async_std::task::{block_on, spawn};
use num_traits::{FromPrimitive, ToPrimitive};
@@ -37,6 +39,7 @@ use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
mod dc_array;
mod lot;
mod string;
use self::string::*;
@@ -132,20 +135,30 @@ pub unsafe extern "C" fn dc_set_config(
}
let ctx = &*context;
let key = to_string_lossy(key);
match config::Config::from_str(&key) {
Ok(key) => block_on(async move {
let value = to_opt_string_lossy(value);
ctx.set_config(key, value.as_deref())
let value = to_opt_string_lossy(value);
block_on(async move {
if key.starts_with("ui.") {
ctx.set_ui_config(&key, value.as_deref())
.await
.with_context(|| format!("Can't set {} to {:?}", key, value))
.log_err(ctx, "dc_set_config() failed")
.is_ok() as libc::c_int
}),
Err(_) => {
warn!(ctx, "dc_set_config(): invalid key");
0
} else {
match config::Config::from_str(&key) {
Ok(key) => ctx
.set_config(key, value.as_deref())
.await
.with_context(|| format!("Can't set {} to {:?}", key, value))
.log_err(ctx, "dc_set_config() failed")
.is_ok() as libc::c_int,
Err(_) => {
warn!(ctx, "dc_set_config(): invalid key");
0
}
}
}
}
})
}
#[no_mangle]
@@ -158,20 +171,33 @@ pub unsafe extern "C" fn dc_get_config(
return "".strdup();
}
let ctx = &*context;
match config::Config::from_str(&to_string_lossy(key)) {
Ok(key) => block_on(async move {
ctx.get_config(key)
let key = to_string_lossy(key);
block_on(async move {
if key.starts_with("ui.") {
ctx.get_ui_config(&key)
.await
.log_err(ctx, "Can't get config")
.log_err(ctx, "Can't get ui-config")
.unwrap_or_default()
.unwrap_or_default()
.strdup()
}),
Err(_) => {
warn!(ctx, "dc_get_config(): invalid key");
"".strdup()
} else {
match config::Config::from_str(&key) {
Ok(key) => ctx
.get_config(key)
.await
.log_err(ctx, "Can't get config")
.unwrap_or_default()
.unwrap_or_default()
.strdup(),
Err(_) => {
warn!(ctx, "dc_get_config(): invalid key");
"".strdup()
}
}
}
}
})
}
#[no_mangle]
@@ -1305,6 +1331,19 @@ pub unsafe extern "C" fn dc_create_group_chat(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_create_broadcast_list(context: *mut dc_context_t) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_create_broadcast_list()");
return 0;
}
let ctx = &*context;
block_on(chat::create_broadcast_list(ctx))
.log_err(ctx, "Failed to create broadcast list")
.map(|id| id.to_u32())
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_is_contact_in_chat(
context: *mut dc_context_t,
@@ -1317,8 +1356,13 @@ pub unsafe extern "C" fn dc_is_contact_in_chat(
}
let ctx = &*context;
block_on(async move { chat::is_contact_in_chat(ctx, ChatId::new(chat_id), contact_id).await })
.into()
block_on(chat::is_contact_in_chat(
ctx,
ChatId::new(chat_id),
contact_id,
))
.log_err(ctx, "is_contact_in_chat failed")
.unwrap_or_default() as libc::c_int
}
#[no_mangle]
@@ -1333,9 +1377,13 @@ pub unsafe extern "C" fn dc_add_contact_to_chat(
}
let ctx = &*context;
block_on(async move {
chat::add_contact_to_chat(ctx, ChatId::new(chat_id), contact_id).await as libc::c_int
})
block_on(chat::add_contact_to_chat(
ctx,
ChatId::new(chat_id),
contact_id,
))
.log_err(ctx, "Failed to add contact")
.is_ok() as libc::c_int
}
#[no_mangle]
@@ -1350,12 +1398,13 @@ pub unsafe extern "C" fn dc_remove_contact_from_chat(
}
let ctx = &*context;
block_on(async move {
chat::remove_contact_from_chat(ctx, ChatId::new(chat_id), contact_id)
.await
.map(|_| 1)
.unwrap_or_log_default(ctx, "Failed to remove contact")
})
block_on(chat::remove_contact_from_chat(
ctx,
ChatId::new(chat_id),
contact_id,
))
.log_err(ctx, "Failed to remove contact")
.is_ok() as libc::c_int
}
#[no_mangle]
@@ -1570,6 +1619,8 @@ pub unsafe extern "C" fn dc_delete_msgs(
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
block_on(message::delete_msgs(ctx, &msg_ids))
.log_err(ctx, "failed dc_delete_msgs() call")
.ok();
}
#[no_mangle]
@@ -1648,6 +1699,18 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_download_full_msg(context: *mut dc_context_t, msg_id: u32) {
if context.is_null() {
eprintln!("ignoring careless call to dc_download_full_msg()");
return;
}
let ctx = &*context;
block_on(MsgId::new(msg_id).download_full(ctx))
.log_err(ctx, "Failed to download message fully.")
.ok();
}
#[no_mangle]
pub unsafe extern "C" fn dc_may_be_valid_addr(addr: *const libc::c_char) -> libc::c_int {
if addr.is_null() {
@@ -1981,10 +2044,11 @@ pub unsafe extern "C" fn dc_check_qr(
}
let ctx = &*context;
block_on(async move {
let lot = qr::check_qr(ctx, &to_string_lossy(qr)).await;
Box::into_raw(Box::new(lot))
})
let lot = match block_on(qr::check_qr(ctx, &to_string_lossy(qr))) {
Ok(qr) => qr.into(),
Err(err) => err.into(),
};
Box::into_raw(Box::new(lot))
}
#[no_mangle]
@@ -2003,12 +2067,9 @@ pub unsafe extern "C" fn dc_get_securejoin_qr(
Some(ChatId::new(chat_id))
};
block_on(async move {
securejoin::dc_get_securejoin_qr(ctx, chat_id)
.await
.unwrap_or_else(|| "".to_string())
.strdup()
})
block_on(securejoin::dc_get_securejoin_qr(ctx, chat_id))
.unwrap_or_else(|_| "".to_string())
.strdup()
}
#[no_mangle]
@@ -2047,7 +2108,9 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
ctx,
ChatId::new(chat_id),
seconds as i64,
));
))
.log_err(ctx, "Failed dc_send_locations_to_chat()")
.ok();
}
#[no_mangle]
@@ -2066,7 +2129,8 @@ pub unsafe extern "C" fn dc_is_sending_locations_to_chat(
Some(ChatId::new(chat_id))
};
block_on(location::is_sending_locations_to_chat(ctx, chat_id)) as libc::c_int
block_on(location::is_sending_locations_to_chat(ctx, chat_id))
.unwrap_or_log_default(ctx, "Failed dc_is_sending_locations_to_chat()") as libc::c_int
}
#[no_mangle]
@@ -2397,13 +2461,13 @@ pub unsafe extern "C" fn dc_chatlist_get_summary(
let ctx = &*ffi_list.context;
block_on(async move {
let lot = ffi_list
let summary = ffi_list
.list
.get_summary(ctx, index as usize, maybe_chat)
.await
.log_err(ctx, "get_summary failed")
.unwrap_or_default();
Box::into_raw(Box::new(lot))
Box::into_raw(Box::new(summary.into()))
})
}
@@ -2423,13 +2487,15 @@ pub unsafe extern "C" fn dc_chatlist_get_summary2(
} else {
Some(MsgId::new(msg_id))
};
block_on(async move {
let lot = Chatlist::get_summary2(ctx, ChatId::new(chat_id), msg_id, None)
.await
.log_err(ctx, "get_summary2 failed")
.unwrap_or_default();
Box::into_raw(Box::new(lot))
})
let summary = block_on(Chatlist::get_summary2(
ctx,
ChatId::new(chat_id),
msg_id,
None,
))
.log_err(ctx, "get_summary2 failed")
.unwrap_or_default();
Box::into_raw(Box::new(summary.into()))
}
#[no_mangle]
@@ -2594,8 +2660,10 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
return 0;
}
let ffi_chat = &*chat;
let cxt = &*ffi_chat.context;
block_on(ffi_chat.chat.can_send(cxt)) as libc::c_int
let ctx = &*ffi_chat.context;
block_on(ffi_chat.chat.can_send(ctx))
.log_err(ctx, "can_send failed")
.unwrap_or_default() as libc::c_int
}
#[no_mangle]
@@ -2782,6 +2850,16 @@ pub unsafe extern "C" fn dc_msg_get_state(msg: *mut dc_msg_t) -> libc::c_int {
ffi_msg.message.get_state() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_download_state(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_download_state()");
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.download_state() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_timestamp(msg: *mut dc_msg_t) -> i64 {
if msg.is_null() {
@@ -2971,10 +3049,10 @@ pub unsafe extern "C" fn dc_msg_get_summary(
let ffi_msg = &mut *msg;
let ctx = &*ffi_msg.context;
block_on(async move {
let lot = ffi_msg.message.get_summary(ctx, maybe_chat).await;
Box::into_raw(Box::new(lot))
})
let summary = block_on(ffi_msg.message.get_summary(ctx, maybe_chat))
.log_err(ctx, "dc_msg_get_summary failed")
.unwrap_or_default();
Box::into_raw(Box::new(summary.into()))
}
#[no_mangle]
@@ -2989,12 +3067,13 @@ pub unsafe extern "C" fn dc_msg_get_summarytext(
let ffi_msg = &mut *msg;
let ctx = &*ffi_msg.context;
block_on({
ffi_msg
.message
.get_summarytext(ctx, approx_characters.try_into().unwrap_or_default())
})
.strdup()
let summary = block_on(ffi_msg.message.get_summary(ctx, None))
.log_err(ctx, "dc_msg_get_summarytext failed")
.unwrap_or_default();
match usize::try_from(approx_characters) {
Ok(chars) => summary.truncated_text(chars).strdup(),
Err(_) => summary.text.strdup(),
}
}
#[no_mangle]
@@ -3471,7 +3550,9 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l
let ffi_contact = &*contact;
let ctx = &*ffi_contact.context;
block_on(async move { ffi_contact.contact.is_verified(ctx).await as libc::c_int })
block_on(ffi_contact.contact.is_verified(ctx))
.log_err(ctx, "is_verified failed")
.unwrap_or_default() as libc::c_int
}
// dc_lot_t
@@ -3529,7 +3610,7 @@ pub unsafe extern "C" fn dc_lot_get_state(lot: *mut dc_lot_t) -> libc::c_int {
}
let lot = &*lot;
lot.get_state().to_i64().expect("impossible") as libc::c_int
lot.get_state() as libc::c_int
}
#[no_mangle]
@@ -3683,8 +3764,29 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
// -- Accounts
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
/// `dc_accounts_t` in multiple threads at once.
pub struct AccountsWrapper {
inner: RwLock<Accounts>,
}
impl Deref for AccountsWrapper {
type Target = RwLock<Accounts>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl AccountsWrapper {
fn new(accounts: Accounts) -> Self {
let inner = RwLock::new(accounts);
Self { inner }
}
}
/// Struct representing a list of deltachat accounts.
pub type dc_accounts_t = Accounts;
pub type dc_accounts_t = AccountsWrapper;
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_new(
@@ -3707,7 +3809,7 @@ pub unsafe extern "C" fn dc_accounts_new(
let accs = block_on(Accounts::new(os_name, as_path(dbfile).to_path_buf().into()));
match accs {
Ok(accs) => Box::into_raw(Box::new(accs)),
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
Err(err) => {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
eprintln!("failed to create accounts: {:#}", err);
@@ -3739,7 +3841,7 @@ pub unsafe extern "C" fn dc_accounts_get_account(
}
let accounts = &*accounts;
block_on(accounts.get_account(id))
block_on(async move { accounts.read().await.get_account(id).await })
.map(|ctx| Box::into_raw(Box::new(ctx)))
.unwrap_or_else(std::ptr::null_mut)
}
@@ -3754,7 +3856,7 @@ pub unsafe extern "C" fn dc_accounts_get_selected_account(
}
let accounts = &*accounts;
block_on(accounts.get_selected_account())
block_on(async move { accounts.read().await.get_selected_account().await })
.map(|ctx| Box::into_raw(Box::new(ctx)))
.unwrap_or_else(std::ptr::null_mut)
}
@@ -3770,9 +3872,19 @@ pub unsafe extern "C" fn dc_accounts_select_account(
}
let accounts = &*accounts;
block_on(accounts.select_account(id))
.map(|_| 1)
.unwrap_or(0)
block_on(async move {
let mut accounts = accounts.write().await;
match accounts.select_account(id).await {
Ok(()) => 1,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Failed to select account: {:#}",
err
)));
0
}
}
})
}
#[no_mangle]
@@ -3782,9 +3894,21 @@ pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -
return 0;
}
let accounts = &*accounts;
let accounts = &mut *accounts;
block_on(accounts.add_account()).unwrap_or(0)
block_on(async move {
let mut accounts = accounts.write().await;
match accounts.add_account().await {
Ok(id) => id,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Failed to add account: {:#}",
err
)));
0
}
}
})
}
#[no_mangle]
@@ -3797,11 +3921,21 @@ pub unsafe extern "C" fn dc_accounts_remove_account(
return 0;
}
let accounts = &*accounts;
let accounts = &mut *accounts;
block_on(accounts.remove_account(id))
.map(|_| 1)
.unwrap_or_else(|_| 0)
block_on(async move {
let mut accounts = accounts.write().await;
match accounts.remove_account(id).await {
Ok(()) => 1,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Failed to remove account: {:#}",
err
)));
0
}
}
})
}
#[no_mangle]
@@ -3814,12 +3948,25 @@ pub unsafe extern "C" fn dc_accounts_migrate_account(
return 0;
}
let accounts = &*accounts;
let accounts = &mut *accounts;
let dbfile = to_string_lossy(dbfile);
block_on(accounts.migrate_account(async_std::path::PathBuf::from(dbfile)))
.map(|_| 1)
.unwrap_or_else(|_| 0)
block_on(async move {
let mut accounts = accounts.write().await;
match accounts
.migrate_account(async_std::path::PathBuf::from(dbfile))
.await
{
Ok(id) => id,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Failed to migrate account: {:#}",
err
)));
0
}
}
})
}
#[no_mangle]
@@ -3830,7 +3977,7 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
}
let accounts = &*accounts;
let list = block_on(accounts.get_all());
let list = block_on(async move { accounts.read().await.get_all().await });
let array: dc_array_t = list.into();
Box::into_raw(Box::new(array))
@@ -3843,7 +3990,7 @@ pub unsafe extern "C" fn dc_accounts_all_work_done(accounts: *mut dc_accounts_t)
return 0;
}
let accounts = &*accounts;
block_on(async move { accounts.all_work_done().await as libc::c_int })
block_on(async move { accounts.read().await.all_work_done().await as libc::c_int })
}
#[no_mangle]
@@ -3854,7 +4001,7 @@ pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
}
let accounts = &*accounts;
block_on(accounts.start_io());
block_on(async move { accounts.read().await.start_io().await });
}
#[no_mangle]
@@ -3865,7 +4012,7 @@ pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *mut dc_accounts_t) {
}
let accounts = &*accounts;
block_on(accounts.stop_io());
block_on(async move { accounts.read().await.stop_io().await });
}
#[no_mangle]
@@ -3876,7 +4023,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *mut dc_accounts_t)
}
let accounts = &*accounts;
block_on(accounts.maybe_network());
block_on(async move { accounts.read().await.maybe_network().await });
}
#[no_mangle]
@@ -3887,7 +4034,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
}
let accounts = &*accounts;
block_on(accounts.maybe_network_lost());
block_on(async move { accounts.write().await.maybe_network_lost().await });
}
pub type dc_accounts_event_emitter_t = deltachat::accounts::EventEmitter;
@@ -3902,7 +4049,7 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
}
let accounts = &*accounts;
let emitter = block_on(accounts.get_event_emitter());
let emitter = block_on(async move { accounts.read().await.get_event_emitter().await });
Box::into_raw(Box::new(emitter))
}

245
deltachat-ffi/src/lot.rs Normal file
View File

@@ -0,0 +1,245 @@
//! # Legacy generic return values for C API.
use crate::message::MessageState;
use crate::qr::Qr;
use crate::summary::{Summary, SummaryPrefix};
use anyhow::Error;
use std::borrow::Cow;
/// An object containing a set of values.
/// The meaning of the values is defined by the function returning the object.
/// Lot objects are created
/// eg. by chatlist.get_summary() or dc_msg_get_summary().
///
/// *Lot* is used in the meaning *heap* here.
#[derive(Debug)]
pub enum Lot {
Summary(Summary),
Qr(Qr),
Error(String),
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Meaning {
None = 0,
Text1Draft = 1,
Text1Username = 2,
Text1Self = 3,
}
impl Default for Meaning {
fn default() -> Self {
Meaning::None
}
}
impl Lot {
pub fn get_text1(&self) -> Option<&str> {
match self {
Self::Summary(summary) => match &summary.prefix {
None => None,
Some(SummaryPrefix::Draft(text)) => Some(text),
Some(SummaryPrefix::Username(username)) => Some(username),
Some(SummaryPrefix::Me(text)) => Some(text),
},
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { .. } => None,
Qr::AskVerifyGroup { grpname, .. } => Some(grpname),
Qr::FprOk { .. } => None,
Qr::FprMismatch { .. } => None,
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
Qr::Account { domain } => Some(domain),
Qr::WebrtcInstance { domain, .. } => Some(domain),
Qr::Addr { .. } => None,
Qr::Url { url } => Some(url),
Qr::Text { text } => Some(text),
Qr::WithdrawVerifyContact { .. } => None,
Qr::WithdrawVerifyGroup { grpname, .. } => Some(grpname),
Qr::ReviveVerifyContact { .. } => None,
Qr::ReviveVerifyGroup { grpname, .. } => Some(grpname),
},
Self::Error(err) => Some(err),
}
}
pub fn get_text2(&self) -> Option<Cow<str>> {
match self {
Self::Summary(summary) => Some(summary.truncated_text(160)),
Self::Qr(_) => None,
Self::Error(_) => None,
}
}
pub fn get_text1_meaning(&self) -> Meaning {
match self {
Self::Summary(summary) => match &summary.prefix {
None => Meaning::None,
Some(SummaryPrefix::Draft(_text)) => Meaning::Text1Draft,
Some(SummaryPrefix::Username(_username)) => Meaning::Text1Username,
Some(SummaryPrefix::Me(_text)) => Meaning::Text1Self,
},
Self::Qr(_qr) => Meaning::None,
Self::Error(_err) => Meaning::None,
}
}
pub fn get_state(&self) -> LotState {
match self {
Self::Summary(summary) => summary.state.into(),
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { .. } => LotState::QrAskVerifyContact,
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
Qr::FprOk { .. } => LotState::QrFprOk,
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
Qr::Account { .. } => LotState::QrAccount,
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
Qr::Addr { .. } => LotState::QrAddr,
Qr::Url { .. } => LotState::QrUrl,
Qr::Text { .. } => LotState::QrText,
Qr::WithdrawVerifyContact { .. } => LotState::QrWithdrawVerifyContact,
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
},
Self::Error(_err) => LotState::QrError,
}
}
pub fn get_id(&self) -> u32 {
match self {
Self::Summary(_) => Default::default(),
Self::Qr(qr) => match qr {
Qr::AskVerifyContact { contact_id, .. } => *contact_id,
Qr::AskVerifyGroup { .. } => Default::default(),
Qr::FprOk { contact_id } => *contact_id,
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default(),
Qr::FprWithoutAddr { .. } => Default::default(),
Qr::Account { .. } => Default::default(),
Qr::WebrtcInstance { .. } => Default::default(),
Qr::Addr { contact_id } => *contact_id,
Qr::Url { .. } => Default::default(),
Qr::Text { .. } => Default::default(),
Qr::WithdrawVerifyContact { contact_id, .. } => *contact_id,
Qr::WithdrawVerifyGroup { .. } => Default::default(),
Qr::ReviveVerifyContact { contact_id, .. } => *contact_id,
Qr::ReviveVerifyGroup { .. } => Default::default(),
},
Self::Error(_) => Default::default(),
}
}
pub fn get_timestamp(&self) -> i64 {
match self {
Self::Summary(summary) => summary.timestamp,
Self::Qr(_) => Default::default(),
Self::Error(_) => Default::default(),
}
}
}
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LotState {
// Default
Undefined = 0,
// Qr States
/// id=contact
QrAskVerifyContact = 200,
/// text1=groupname
QrAskVerifyGroup = 202,
/// id=contact
QrFprOk = 210,
/// id=contact
QrFprMismatch = 220,
/// text1=formatted fingerprint
QrFprWithoutAddr = 230,
/// text1=domain
QrAccount = 250,
/// text1=domain, text2=instance pattern
QrWebrtcInstance = 260,
/// id=contact
QrAddr = 320,
/// text1=text
QrText = 330,
/// text1=URL
QrUrl = 332,
/// text1=error string
QrError = 400,
QrWithdrawVerifyContact = 500,
/// text1=groupname
QrWithdrawVerifyGroup = 502,
QrReviveVerifyContact = 510,
/// text1=groupname
QrReviveVerifyGroup = 512,
// Message States
MsgInFresh = 10,
MsgInNoticed = 13,
MsgInSeen = 16,
MsgOutPreparing = 18,
MsgOutDraft = 19,
MsgOutPending = 20,
MsgOutFailed = 24,
MsgOutDelivered = 26,
MsgOutMdnRcvd = 28,
}
impl Default for LotState {
fn default() -> Self {
LotState::Undefined
}
}
impl From<MessageState> for LotState {
fn from(s: MessageState) -> Self {
use MessageState::*;
match s {
Undefined => LotState::Undefined,
InFresh => LotState::MsgInFresh,
InNoticed => LotState::MsgInNoticed,
InSeen => LotState::MsgInSeen,
OutPreparing => LotState::MsgOutPreparing,
OutDraft => LotState::MsgOutDraft,
OutPending => LotState::MsgOutPending,
OutFailed => LotState::MsgOutFailed,
OutDelivered => LotState::MsgOutDelivered,
OutMdnRcvd => LotState::MsgOutMdnRcvd,
}
}
}
impl From<Summary> for Lot {
fn from(summary: Summary) -> Self {
Lot::Summary(summary)
}
}
impl From<Qr> for Lot {
fn from(qr: Qr) -> Self {
Lot::Qr(qr)
}
}
// Make it easy to convert errors into the final `Lot`.
impl From<Error> for Lot {
fn from(error: Error) -> Self {
Lot::Error(error.to_string())
}
}

View File

@@ -9,5 +9,5 @@ license = "MPL-2.0"
proc-macro = true
[dependencies]
syn = "1.0.74"
quote = "1.0.2"
syn = "1"
quote = "1"

View File

@@ -2,7 +2,7 @@ extern crate dirs;
use std::str::FromStr;
use anyhow::{bail, ensure, Error};
use anyhow::{bail, ensure, Result};
use async_std::path::Path;
use deltachat::chat::{
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
@@ -13,10 +13,10 @@ use deltachat::contact::*;
use deltachat::context::*;
use deltachat::dc_receive_imf::*;
use deltachat::dc_tools::*;
use deltachat::download::DownloadState;
use deltachat::imex::*;
use deltachat::location;
use deltachat::log::LogExt;
use deltachat::lot::LotState;
use deltachat::message::{self, Message, MessageState, MsgId};
use deltachat::peerstate::*;
use deltachat::qr::*;
@@ -98,7 +98,7 @@ async fn reset_tables(context: &Context, bits: i32) {
});
}
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<(), anyhow::Error> {
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
let data = dc_read_file(context, filename).await?;
if let Err(err) = dc_receive_imf(context, &data, "import", 0, false).await {
@@ -189,10 +189,18 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
MessageState::OutFailed => " !!",
_ => "",
};
let downloadstate = match msg.download_state() {
DownloadState::Done => "",
DownloadState::Available => " [⬇ Download available]",
DownloadState::InProgress => " [⬇ Download in progress...]",
DownloadState::Failure => " [⬇ Download failed]",
};
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
let msgtext = msg.get_text();
println!(
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{} [{}]",
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{}{} [{}]",
prefix.as_ref(),
msg.get_id(),
if msg.get_showpadlock() { "🔒" } else { "" },
@@ -226,11 +234,12 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
""
},
statestr,
downloadstate,
&temp2,
);
}
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error> {
async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
let mut lines_out = 0;
for &msg_id in msglist {
if msg_id == MsgId::new(DC_MSG_ID_DAYMARKER) {
@@ -258,59 +267,59 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<(), Error>
Ok(())
}
async fn log_contactlist(context: &Context, contacts: &[u32]) {
async fn log_contactlist(context: &Context, contacts: &[u32]) -> Result<()> {
for contact_id in contacts {
let line;
let mut line2 = "".to_string();
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
let name = contact.get_display_name();
let addr = contact.get_addr();
let verified_state = contact.is_verified(context).await;
let verified_str = if VerifiedStatus::Unverified != verified_state {
if verified_state == VerifiedStatus::BidirectVerified {
" √√"
} else {
""
}
let contact = Contact::get_by_id(context, *contact_id).await?;
let name = contact.get_display_name();
let addr = contact.get_addr();
let verified_state = contact.is_verified(context).await?;
let verified_str = if VerifiedStatus::Unverified != verified_state {
if verified_state == VerifiedStatus::BidirectVerified {
" √√"
} else {
""
};
line = format!(
"{}{} <{}>",
if !name.is_empty() {
&name
} else {
"<name unset>"
},
verified_str,
if !addr.is_empty() {
&addr
} else {
"addr unset"
}
);
let peerstate = Peerstate::from_addr(context, &addr)
.await
.expect("peerstate error");
if peerstate.is_some() && *contact_id != 1 {
line2 = format!(
", prefer-encrypt={}",
peerstate.as_ref().unwrap().prefer_encrypt
);
""
}
println!("Contact#{}: {}{}", *contact_id, line, line2);
} else {
""
};
line = format!(
"{}{} <{}>",
if !name.is_empty() {
&name
} else {
"<name unset>"
},
verified_str,
if !addr.is_empty() {
&addr
} else {
"addr unset"
}
);
let peerstate = Peerstate::from_addr(context, &addr)
.await
.expect("peerstate error");
if peerstate.is_some() && *contact_id != 1 {
line2 = format!(
", prefer-encrypt={}",
peerstate.as_ref().unwrap().prefer_encrypt
);
}
println!("Contact#{}: {}{}", *contact_id, line, line2);
}
Ok(())
}
fn chat_prefix(chat: &Chat) -> &'static str {
chat.typ.into()
}
pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Result<(), Error> {
pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Result<()> {
let mut sel_chat = if !chat_id.is_unset() {
Chat::load_from_db(&context, *chat_id).await.ok()
Some(Chat::load_from_db(&context, *chat_id).await?)
} else {
None
};
@@ -361,6 +370,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat [<chat-id>|0]\n\
createchat <contact-id>\n\
creategroup <name>\n\
createbroadcast\n\
createprotected <name>\n\
addmember <contact-id>\n\
removemember <contact-id>\n\
@@ -394,6 +404,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
===========================Message commands==\n\
listmsgs <query>\n\
msginfo <msg-id>\n\
download <msg-id>\n\
html <msg-id>\n\
listfresh\n\
forward <msg-id> <chat-id>\n\
@@ -450,7 +461,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
!arg1.is_empty() && !arg2.is_empty(),
"Arguments <msg-id> <setup-code> expected"
);
continue_key_transfer(&context, MsgId::new(arg1.parse()?), &arg2).await?;
continue_key_transfer(&context, MsgId::new(arg1.parse()?), arg2).await?;
}
"has-backup" => {
has_backup(&context, blobdir).await?;
@@ -497,13 +508,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"set" => {
ensure!(!arg1.is_empty(), "Argument <key> missing.");
let key = config::Config::from_str(&arg1)?;
let key = config::Config::from_str(arg1)?;
let value = if arg2.is_empty() { None } else { Some(arg2) };
context.set_config(key, value).await?;
}
"get" => {
ensure!(!arg1.is_empty(), "Argument <key> missing.");
let key = config::Config::from_str(&arg1)?;
let key = config::Config::from_str(arg1)?;
let val = context.get_config(key).await;
println!("{}={:?}", key, val);
}
@@ -569,26 +580,25 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
""
},
);
let lot = chatlist.get_summary(&context, i, Some(&chat)).await?;
let summary = chatlist.get_summary(&context, i, Some(&chat)).await?;
let statestr = if chat.visibility == ChatVisibility::Archived {
" [Archived]"
} else {
match lot.get_state() {
LotState::MsgOutPending => " o",
LotState::MsgOutDelivered => "",
LotState::MsgOutMdnRcvd => " √√",
LotState::MsgOutFailed => " !!",
match summary.state {
MessageState::OutPending => " o",
MessageState::OutDelivered => "",
MessageState::OutMdnRcvd => " √√",
MessageState::OutFailed => " !!",
_ => "",
}
};
let timestr = dc_timestamp_to_str(lot.get_timestamp());
let text1 = lot.get_text1();
let text2 = lot.get_text2();
let timestr = dc_timestamp_to_str(summary.timestamp);
println!(
"{}{}{}{} [{}]{}",
text1.unwrap_or(""),
if text1.is_some() { ": " } else { "" },
text2.unwrap_or(""),
"{}{}{} [{}]{}",
summary
.prefix
.map_or_else(String::new, |prefix| format!("{}: ", prefix)),
summary.text,
statestr,
&timestr,
if chat.is_sending_locations() {
@@ -602,7 +612,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
);
}
}
if location::is_sending_locations_to_chat(&context, None).await {
if location::is_sending_locations_to_chat(&context, None).await? {
println!("Location streaming enabled.");
}
println!("{} chats", cnt);
@@ -705,6 +715,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Group#{} created successfully.", chat_id);
}
"createbroadcast" => {
let chat_id = chat::create_broadcast_list(&context).await?;
println!("Broadcast#{} created successfully.", chat_id);
}
"createprotected" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
@@ -717,17 +732,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
let contact_id_0: u32 = arg1.parse()?;
if chat::add_contact_to_chat(
&context,
sel_chat.as_ref().unwrap().get_id(),
contact_id_0,
)
.await
{
println!("Contact added to chat.");
} else {
bail!("Cannot add contact to chat.");
}
chat::add_contact_to_chat(&context, sel_chat.as_ref().unwrap().get_id(), contact_id_0)
.await?;
println!("Contact added to chat.");
}
"removemember" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -765,7 +772,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
println!("Memberlist:");
log_contactlist(&context, &contacts).await;
log_contactlist(&context, &contacts).await?;
println!(
"{} contacts\nLocation streaming: {}",
contacts.len(),
@@ -773,7 +780,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
&context,
Some(sel_chat.as_ref().unwrap().get_id())
)
.await,
.await?,
);
}
"getlocations" => {
@@ -818,7 +825,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
sel_chat.as_ref().unwrap().get_id(),
seconds,
)
.await;
.await?;
println!(
"Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.",
sel_chat.as_ref().unwrap().get_id(),
@@ -895,12 +902,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"listmsgs" => {
ensure!(!arg1.is_empty(), "Argument <query> missing.");
let chat = if let Some(ref sel_chat) = sel_chat {
Some(sel_chat.get_id())
} else {
None
};
let chat = sel_chat.as_ref().map(|sel_chat| sel_chat.get_id());
let time_start = std::time::SystemTime::now();
let msglist = context.search_msgs(chat, arg1).await?;
let time_needed = time_start.elapsed().unwrap_or_default();
@@ -1030,6 +1032,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let res = message::get_msg_info(&context, id).await?;
println!("{}", res);
}
"download" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?);
println!("Scheduling download for {:?}", id);
id.download_full(&context).await?;
}
"html" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?);
@@ -1067,7 +1075,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
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).await;
message::delete_msgs(&context, &ids).await?;
}
"listcontacts" | "contacts" | "listverified" => {
let contacts = Contact::get_all(
@@ -1080,7 +1088,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Some(arg1),
)
.await?;
log_contactlist(&context, &contacts).await;
log_contactlist(&context, &contacts).await?;
println!("{} contacts.", contacts.len());
}
"addcontact" => {
@@ -1145,19 +1153,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"listblocked" => {
let contacts = Contact::get_all_blocked(&context).await?;
log_contactlist(&context, &contacts).await;
log_contactlist(&context, &contacts).await?;
println!("{} blocked contacts.", contacts.len());
}
"checkqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
let res = check_qr(&context, arg1).await;
println!(
"state={}, id={}, text1={:?}, text2={:?}",
res.get_state(),
res.get_id(),
res.get_text1(),
res.get_text2()
);
let qr = check_qr(&context, arg1).await?;
println!("qr={:?}", qr);
}
"setqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");

View File

@@ -167,13 +167,14 @@ const DB_COMMANDS: [&str; 10] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 33] = [
const CHAT_COMMANDS: [&str; 34] = [
"listchats",
"listarchived",
"chat",
"createchat",
"creategroup",
"createverified",
"createbroadcast",
"createprotected",
"addmember",
"removemember",
"groupname",
@@ -202,13 +203,14 @@ const CHAT_COMMANDS: [&str; 33] = [
"accept",
"blockchat",
];
const MESSAGE_COMMANDS: [&str; 6] = [
const MESSAGE_COMMANDS: [&str; 7] = [
"listmsgs",
"msginfo",
"listfresh",
"forward",
"markseen",
"delmsg",
"download",
];
const CONTACT_COMMANDS: [&str; 9] = [
"listcontacts",
@@ -325,7 +327,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
loop {
let p = "> ";
let readline = rl.readline(&p);
let readline = rl.readline(p);
match readline {
Ok(line) => {
@@ -409,19 +411,18 @@ async fn handle_cmd(
"getqr" | "getbadqr" => {
ctx.start_io().await;
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
if let Some(mut qr) = dc_get_securejoin_qr(&ctx, group).await {
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
qr.replace_range(12..22, "0000000000")
}
println!("{}", qr);
let output = Command::new("qrencode")
.args(&["-t", "ansiutf8", qr.as_str(), "-o", "-"])
.output()
.expect("failed to execute process");
io::stdout().write_all(&output.stdout).unwrap();
io::stderr().write_all(&output.stderr).unwrap();
let mut qr = dc_get_securejoin_qr(&ctx, group).await?;
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
qr.replace_range(12..22, "0000000000")
}
println!("{}", qr);
let output = Command::new("qrencode")
.args(&["-t", "ansiutf8", qr.as_str(), "-o", "-"])
.output()
.expect("failed to execute process");
io::stdout().write_all(&output.stdout).unwrap();
io::stderr().write_all(&output.stderr).unwrap();
}
}
"joinqr" => {

View File

@@ -973,7 +973,7 @@ class TestOnlineAccount:
ac1._evtracker.wait_msg_delivered(msg1)
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_messages_changed()
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message1"
assert not msg2.is_forwarded()
assert msg2.get_sender_contact().display_name == ac1.get_config("displayname")
@@ -1125,7 +1125,7 @@ class TestOnlineAccount:
group1.add_contact(ac2)
group1.send_text("hello")
msg2 = ac2._evtracker.wait_next_messages_changed()
msg2 = ac2._evtracker.wait_next_incoming_message()
group2 = msg2.create_chat()
assert group2.get_name() == group1.get_name()
@@ -1188,7 +1188,7 @@ class TestOnlineAccount:
chat.send_text("message1")
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_messages_changed()
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message1"
lp.sec("create new chat with contact and send back (encrypted) message")
@@ -1468,7 +1468,7 @@ class TestOnlineAccount:
assert not msg1.is_encrypted()
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_messages_changed()
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message1"
assert not msg2.is_encrypted()
@@ -1517,7 +1517,7 @@ class TestOnlineAccount:
chat1.send_text("hi")
lp.sec("ac2 receives contact request from ac1")
received_message = ac2._evtracker.wait_next_messages_changed()
received_message = ac2._evtracker.wait_next_incoming_message()
assert received_message.text == "hi"
basename = "attachment.txt"
@@ -1552,7 +1552,7 @@ class TestOnlineAccount:
assert msg_out.get_mime_headers() is None
lp.sec("wait for ac2 to receive message")
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
in_id = ev.data2
mime = ac2.get_message_by_id(in_id).get_mime_headers()
assert mime.get_all("From")
@@ -1851,7 +1851,7 @@ class TestOnlineAccount:
ac1.create_chat(ac2).send_text("with avatar!")
lp.sec("ac2: wait for receiving message and avatar from ac1")
msg2 = ac2._evtracker.wait_next_messages_changed()
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.chat.is_contact_request()
received_path = msg2.get_sender_contact().get_profile_image()
assert open(received_path, "rb").read() == open(p, "rb").read()
@@ -2681,7 +2681,7 @@ class TestOnlineAccount:
lp.sec("receive a message")
ac2.create_group_chat("group name", contacts=[ac1]).send_text("incoming, unencrypted group message")
ac1._evtracker.wait_next_messages_changed()
ac1._evtracker.wait_next_incoming_message()
lp.sec("send out message with bcc to ourselves")
ac1.direct_imap.idle_start()
@@ -2712,6 +2712,22 @@ class TestOnlineAccount:
# We can't decrypt the message in this chat, so the chat is empty:
assert len(private_messages) == 0
def test_delete_deltachat_folder(self, acfactory):
"""Test that DeltaChat folder is recreated if user deletes it manually."""
ac1 = acfactory.get_online_configuring_account(mvbox=True)
ac2 = acfactory.get_online_configuring_account()
acfactory.wait_configure(ac1)
ac1.direct_imap.conn.delete_folder("DeltaChat")
assert len(ac1.direct_imap.conn.list_folders(pattern="DeltaChat")) == 0
acfactory.wait_configure_and_start_io()
ac2.create_chat(ac1).send_text("hello")
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
assert len(ac1.direct_imap.conn.list_folders(pattern="DeltaChat")) == 1
class TestGroupStressTests:
def test_group_many_members_add_leave_remove(self, acfactory, lp):

View File

@@ -6,8 +6,6 @@ import shutil
import pytest
from filecmp import cmp
from deltachat import const
def wait_msg_delivered(account, msg_list):
""" wait for one or more MSG_DELIVERED events to match msg_list contents. """
@@ -102,14 +100,10 @@ class TestOnlineInCreation:
])
lp.sec("wait1 for original or forwarded messages to arrive")
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)
received_original = ac2._evtracker.wait_next_incoming_message()
assert cmp(received_original.filename, orig, shallow=False)
lp.sec("wait2 for original or forwarded messages to arrive")
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)
received_copy = ac2._evtracker.wait_next_incoming_message()
assert received_copy.id != received_original.id
assert cmp(received_copy.filename, orig, shallow=False)

View File

@@ -86,6 +86,8 @@ def main():
print("")
print(" git tag -a {}".format(newversion))
print(" git push origin {}".format(newversion))
print(" git tag -a py-{}".format(newversion))
print(" git push origin py-{}".format(newversion))
print("")

View File

@@ -13,22 +13,18 @@ use anyhow::{ensure, Context as _, Result};
use serde::{Deserialize, Serialize};
use crate::context::Context;
use crate::events::Event;
use crate::events::{Event, EventType, Events};
/// Account manager, that can handle multiple accounts in a single place.
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct Accounts {
dir: PathBuf,
config: Config,
accounts: Arc<RwLock<BTreeMap<u32, Context>>>,
accounts: BTreeMap<u32, Context>,
emitter: EventEmitter,
/// Sender side of the fake event channel.
///
/// We never send any events over this channel, but hold it during the account manager lifetime
/// to prevent `EventEmitter` from returning `None` as long as account manager is alive, even if
/// it holds no accounts which could emit events.
fake_sender: Sender<crate::events::Event>,
/// Event channel to emit account manager errors.
events: Events,
}
impl Accounts {
@@ -65,9 +61,9 @@ impl Accounts {
let emitter = EventEmitter::new();
// Fake event stream to prevent event emitter from closing.
let (fake_sender, fake_receiver) = channel::bounded(1);
emitter.sender.send(fake_receiver).await?;
let events = Events::default();
emitter.sender.send(events.get_emitter()).await?;
for account in accounts.values() {
emitter.add_account(account).await?;
@@ -76,21 +72,21 @@ impl Accounts {
Ok(Self {
dir,
config,
accounts: Arc::new(RwLock::new(accounts)),
accounts,
emitter,
fake_sender,
events,
})
}
/// Get an account by its `id`:
pub async fn get_account(&self, id: u32) -> Option<Context> {
self.accounts.read().await.get(&id).cloned()
self.accounts.get(&id).cloned()
}
/// Get the currently selected account.
pub async fn get_selected_account(&self) -> Option<Context> {
let id = self.config.get_selected_account().await;
self.accounts.read().await.get(&id).cloned()
self.accounts.get(&id).cloned()
}
/// Returns the currently selected account's id or None if no account is selected.
@@ -102,27 +98,27 @@ impl Accounts {
}
/// Select the given account.
pub async fn select_account(&self, id: u32) -> Result<()> {
pub async fn select_account(&mut self, id: u32) -> Result<()> {
self.config.select_account(id).await?;
Ok(())
}
/// Add a new account.
pub async fn add_account(&self) -> Result<u32> {
pub async fn add_account(&mut self) -> Result<u32> {
let os_name = self.config.os_name().await;
let account_config = self.config.new_account(&self.dir).await?;
let ctx = Context::new(os_name, account_config.dbfile().into(), account_config.id).await?;
self.emitter.add_account(&ctx).await?;
self.accounts.write().await.insert(account_config.id, ctx);
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
}
/// Remove an account.
pub async fn remove_account(&self, id: u32) -> Result<()> {
let ctx = self.accounts.write().await.remove(&id);
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
let ctx = self.accounts.remove(&id);
ensure!(ctx.is_some(), "no account with this id: {}", id);
let ctx = ctx.unwrap();
ctx.stop_io().await;
@@ -139,7 +135,7 @@ impl Accounts {
}
/// Migrate an existing account into this structure.
pub async fn migrate_account(&self, dbfile: PathBuf) -> Result<u32> {
pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
let blobdir = Context::derive_blobdir(&dbfile);
let walfile = Context::derive_walfile(&dbfile);
@@ -195,7 +191,7 @@ impl Accounts {
)
.await?;
self.emitter.add_account(&ctx).await?;
self.accounts.write().await.insert(account_config.id, ctx);
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
}
Err(err) => {
@@ -216,7 +212,7 @@ impl Accounts {
/// Get a list of all account ids.
pub async fn get_all(&self) -> Vec<u32> {
self.accounts.read().await.keys().copied().collect()
self.accounts.keys().copied().collect()
}
/// This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
@@ -230,7 +226,7 @@ impl Accounts {
/// - while dc_accounts_all_work_done() returns false:
/// - Wait for DC_EVENT_CONNECTIVITY_CHANGED
pub async fn all_work_done(&self) -> bool {
for account in self.accounts.read().await.values() {
for account in self.accounts.values() {
if !account.all_work_done().await {
return false;
}
@@ -239,33 +235,34 @@ impl Accounts {
}
pub async fn start_io(&self) {
let accounts = &*self.accounts.read().await;
for account in accounts.values() {
for account in self.accounts.values() {
account.start_io().await;
}
}
pub async fn stop_io(&self) {
let accounts = &*self.accounts.read().await;
for account in accounts.values() {
for account in self.accounts.values() {
account.stop_io().await;
}
}
pub async fn maybe_network(&self) {
let accounts = &*self.accounts.read().await;
for account in accounts.values() {
for account in self.accounts.values() {
account.maybe_network().await;
}
}
pub async fn maybe_network_lost(&self) {
let accounts = &*self.accounts.read().await;
for account in accounts.values() {
for account in self.accounts.values() {
account.maybe_network_lost().await;
}
}
/// Emits a single event.
pub fn emit_event(&self, event: EventType) {
self.events.emit(Event { id: 0, typ: event })
}
/// Returns unified event emitter.
pub async fn get_event_emitter(&self) -> EventEmitter {
self.emitter.clone()
@@ -276,13 +273,13 @@ impl Accounts {
#[derive(Debug, Clone)]
pub struct EventEmitter {
/// Aggregate stream of events from all accounts.
stream: Arc<RwLock<futures::stream::SelectAll<Receiver<crate::events::Event>>>>,
stream: Arc<RwLock<futures::stream::SelectAll<crate::events::EventEmitter>>>,
/// Sender for the channel where new account emitters will be pushed.
sender: Sender<Receiver<crate::events::Event>>,
sender: Sender<crate::events::EventEmitter>,
/// Receiver for the channel where new account emitters will be pushed.
receiver: Receiver<Receiver<crate::events::Event>>,
receiver: Receiver<crate::events::EventEmitter>,
}
impl EventEmitter {
@@ -315,9 +312,7 @@ impl EventEmitter {
/// Add event emitter of a new account to the aggregate event emitter.
pub async fn add_account(&self, context: &Context) -> Result<()> {
self.sender
.send(context.get_event_emitter().into_inner())
.await?;
self.sender.send(context.get_event_emitter()).await?;
Ok(())
}
}
@@ -343,12 +338,15 @@ pub const CONFIG_NAME: &str = "accounts.toml";
pub const DB_NAME: &str = "dc.db";
/// Account manager configuration file.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
file: PathBuf,
inner: Arc<RwLock<InnerConfig>>,
inner: InnerConfig,
}
/// Account manager configuration file contents.
///
/// This is serialized into TOML.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct InnerConfig {
pub os_name: String,
@@ -360,14 +358,15 @@ struct InnerConfig {
impl Config {
pub async fn new(os_name: String, dir: &PathBuf) -> Result<Self> {
let inner = InnerConfig {
os_name,
accounts: Vec::new(),
selected_account: 0,
next_id: 1,
};
let cfg = Config {
file: dir.join(CONFIG_NAME),
inner: Arc::new(RwLock::new(InnerConfig {
os_name,
accounts: Vec::new(),
selected_account: 0,
next_id: 1,
})),
inner,
};
cfg.sync().await?;
@@ -376,17 +375,14 @@ impl Config {
}
pub async fn os_name(&self) -> String {
self.inner.read().await.os_name.clone()
self.inner.os_name.clone()
}
/// Sync the inmemory representation to disk.
async fn sync(&self) -> Result<()> {
fs::write(
&self.file,
toml::to_string_pretty(&*self.inner.read().await)?,
)
.await
.context("failed to write config")
fs::write(&self.file, toml::to_string_pretty(&self.inner)?)
.await
.context("failed to write config")
}
/// Read a configuration from the given file into memory.
@@ -394,18 +390,14 @@ impl Config {
let bytes = fs::read(&file).await.context("failed to read file")?;
let inner: InnerConfig = toml::from_slice(&bytes).context("failed to parse config")?;
Ok(Config {
file,
inner: Arc::new(RwLock::new(inner)),
})
Ok(Config { file, inner })
}
pub async fn load_accounts(&self) -> Result<BTreeMap<u32, Context>> {
let cfg = &*self.inner.read().await;
let mut accounts = BTreeMap::new();
for account_config in &cfg.accounts {
for account_config in &self.inner.accounts {
let ctx = Context::new(
cfg.os_name.clone(),
self.inner.os_name.clone(),
account_config.dbfile().into(),
account_config.id,
)
@@ -417,19 +409,18 @@ impl Config {
}
/// Create a new account in the given root directory.
async fn new_account(&self, dir: &PathBuf) -> Result<AccountConfig> {
async fn new_account(&mut self, dir: &PathBuf) -> Result<AccountConfig> {
let id = {
let inner = &mut self.inner.write().await;
let id = inner.next_id;
let id = self.inner.next_id;
let uuid = Uuid::new_v4();
let target_dir = dir.join(uuid.to_simple_ref().to_string());
inner.accounts.push(AccountConfig {
self.inner.accounts.push(AccountConfig {
id,
dir: target_dir.into(),
uuid,
});
inner.next_id += 1;
self.inner.next_id += 1;
id
};
@@ -441,16 +432,16 @@ impl Config {
}
/// Removes an existing acccount entirely.
pub async fn remove_account(&self, id: u32) -> Result<()> {
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
{
let inner = &mut *self.inner.write().await;
if let Some(idx) = inner.accounts.iter().position(|e| e.id == id) {
if let Some(idx) = self.inner.accounts.iter().position(|e| e.id == id) {
// remove account from the configs
inner.accounts.remove(idx);
self.inner.accounts.remove(idx);
}
if inner.selected_account == id {
if self.inner.selected_account == id {
// reset selected account
inner.selected_account = inner.accounts.get(0).map(|e| e.id).unwrap_or_default();
self.inner.selected_account =
self.inner.accounts.get(0).map(|e| e.id).unwrap_or_default();
}
}
@@ -458,29 +449,22 @@ impl Config {
}
async fn get_account(&self, id: u32) -> Option<AccountConfig> {
self.inner
.read()
.await
.accounts
.iter()
.find(|e| e.id == id)
.cloned()
self.inner.accounts.iter().find(|e| e.id == id).cloned()
}
pub async fn get_selected_account(&self) -> u32 {
self.inner.read().await.selected_account
self.inner.selected_account
}
pub async fn select_account(&self, id: u32) -> Result<()> {
pub async fn select_account(&mut self, id: u32) -> Result<()> {
{
let inner = &mut *self.inner.write().await;
ensure!(
inner.accounts.iter().any(|e| e.id == id),
self.inner.accounts.iter().any(|e| e.id == id),
"invalid account id: {}",
id
);
inner.selected_account = id;
self.inner.selected_account = id;
}
self.sync().await?;
@@ -514,23 +498,17 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts1").into();
let accounts1 = Accounts::new("my_os".into(), p.clone()).await.unwrap();
let mut accounts1 = Accounts::new("my_os".into(), p.clone()).await.unwrap();
accounts1.add_account().await.unwrap();
let accounts2 = Accounts::open(p).await.unwrap();
assert_eq!(accounts1.accounts.read().await.len(), 1);
assert_eq!(accounts1.accounts.len(), 1);
assert_eq!(accounts1.config.get_selected_account().await, 1);
assert_eq!(accounts1.dir, accounts2.dir);
assert_eq!(
&*accounts1.config.inner.read().await,
&*accounts2.config.inner.read().await,
);
assert_eq!(
accounts1.accounts.read().await.len(),
accounts2.accounts.read().await.len()
);
assert_eq!(accounts1.config, accounts2.config,);
assert_eq!(accounts1.accounts.len(), accounts2.accounts.len());
}
#[async_std::test]
@@ -538,26 +516,26 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
assert_eq!(accounts.accounts.read().await.len(), 0);
let mut accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account().await, 0);
let id = accounts.add_account().await.unwrap();
assert_eq!(id, 1);
assert_eq!(accounts.accounts.read().await.len(), 1);
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account().await, 1);
let id = accounts.add_account().await.unwrap();
assert_eq!(id, 2);
assert_eq!(accounts.config.get_selected_account().await, id);
assert_eq!(accounts.accounts.read().await.len(), 2);
assert_eq!(accounts.accounts.len(), 2);
accounts.select_account(1).await.unwrap();
assert_eq!(accounts.config.get_selected_account().await, 1);
accounts.remove_account(1).await.unwrap();
assert_eq!(accounts.config.get_selected_account().await, 2);
assert_eq!(accounts.accounts.read().await.len(), 1);
assert_eq!(accounts.accounts.len(), 1);
}
#[async_std::test]
@@ -565,14 +543,14 @@ mod tests {
let dir = tempfile::tempdir()?;
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
let mut accounts = Accounts::new("my_os".into(), p.clone()).await?;
assert!(accounts.get_selected_account().await.is_none());
assert_eq!(accounts.config.get_selected_account().await, 0);
let id = accounts.add_account().await?;
assert!(accounts.get_selected_account().await.is_some());
assert_eq!(id, 1);
assert_eq!(accounts.accounts.read().await.len(), 1);
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account().await, id);
accounts.remove_account(id).await?;
@@ -586,8 +564,8 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
assert_eq!(accounts.accounts.read().await.len(), 0);
let mut accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account().await, 0);
let extern_dbfile: PathBuf = dir.path().join("other").into();
@@ -604,7 +582,7 @@ mod tests {
.migrate_account(extern_dbfile.clone())
.await
.unwrap();
assert_eq!(accounts.accounts.read().await.len(), 1);
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account().await, 1);
let ctx = accounts.get_selected_account().await.unwrap();
@@ -623,7 +601,7 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
let mut accounts = Accounts::new("my_os".into(), p.clone()).await.unwrap();
for expected_id in 1..10 {
let id = accounts.add_account().await.unwrap();
@@ -643,7 +621,7 @@ mod tests {
let dummy_accounts = 10;
let (id0, id1, id2) = {
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
let mut accounts = Accounts::new("my_os".into(), p.clone()).await?;
accounts.add_account().await?;
let ids = accounts.get_all().await;
assert_eq!(ids.len(), 1);
@@ -726,7 +704,7 @@ mod tests {
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
// Make sure there are no accounts.
assert_eq!(accounts.accounts.read().await.len(), 0);
assert_eq!(accounts.accounts.len(), 0);
// Create event emitter.
let mut event_emitter = accounts.get_event_emitter().await;

View File

@@ -2,12 +2,12 @@
//!
//! Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).
use anyhow::{bail, format_err, Error, Result};
use std::collections::BTreeMap;
use std::str::FromStr;
use std::{fmt, str};
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, SignedPublicKey};
@@ -37,13 +37,13 @@ impl fmt::Display for EncryptPreference {
}
impl str::FromStr for EncryptPreference {
type Err = ();
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
fn from_str(s: &str) -> Result<Self> {
match s {
"mutual" => Ok(EncryptPreference::Mutual),
"nopreference" => Ok(EncryptPreference::NoPreference),
_ => Err(()),
_ => bail!("Cannot parse encryption preference {}", s),
}
}
}
@@ -70,28 +70,27 @@ impl Aheader {
}
}
/// Tries to parse Autocrypt header.
///
/// If there is none, returns None. If the header is present but cannot be parsed, returns an
/// error.
pub fn from_headers(
context: &Context,
wanted_from: &str,
headers: &[mailparse::MailHeader<'_>],
) -> Option<Self> {
) -> Result<Option<Self>> {
if let Some(value) = headers.get_header_value(HeaderDef::Autocrypt) {
match Self::from_str(&value) {
Ok(header) => {
if addr_cmp(&header.addr, wanted_from) {
return Some(header);
}
}
Err(err) => {
warn!(
context,
"found invalid autocrypt header {}: {:?}", value, err
);
}
let header = Self::from_str(&value)?;
if !addr_cmp(&header.addr, wanted_from) {
bail!(
"Autocrypt header address {:?} is not {:?}",
header.addr,
wanted_from
);
}
Ok(Some(header))
} else {
Ok(None)
}
None
}
}
@@ -120,9 +119,9 @@ impl fmt::Display for Aheader {
}
impl str::FromStr for Aheader {
type Err = ();
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
fn from_str(s: &str) -> Result<Self> {
let mut attributes: BTreeMap<String, String> = s
.split(';')
.filter_map(|a| {
@@ -136,15 +135,20 @@ impl str::FromStr for Aheader {
let addr = match attributes.remove("addr") {
Some(addr) => addr,
None => {
return Err(());
}
None => bail!("Autocrypt header has no addr"),
};
let public_key: SignedPublicKey = attributes
.remove("keydata")
.ok_or(())
.and_then(|raw| SignedPublicKey::from_base64(&raw).or(Err(())))
.and_then(|key| key.verify().and(Ok(key)).or(Err(())))?;
.ok_or_else(|| format_err!("keydata attribute is not found"))
.and_then(|raw| {
SignedPublicKey::from_base64(&raw)
.map_err(|_| format_err!("Autocrypt key cannot be decoded"))
})
.and_then(|key| {
key.verify()
.and(Ok(key))
.map_err(|_| format_err!("Autocrypt key cannot be verified"))
})?;
let prefer_encrypt = attributes
.remove("prefer-encrypt")
@@ -154,7 +158,7 @@ impl str::FromStr for Aheader {
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
// Autocrypt-Level0: unknown attribute, treat the header as invalid
if attributes.keys().any(|k| !k.starts_with('_')) {
return Err(());
bail!("Unknown Autocrypt attribute found");
}
Ok(Aheader {
@@ -172,38 +176,40 @@ mod tests {
const RAWKEY: &str = "xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJgWL7O3y/g4HdDW/dN2z26Y6W13IMzx9gLInn1KQZChtqWAcr/ReUucXcymwcfg1mdkBGk3TSLeLihN6CJx8Wsv8ig+kgAzte4f5rqEEAJVQ9WZHuti7UiYs6oRzqTo06CRe9owVXxzdMf0VDQtf7ZFm9dpzKKbhH7Lu8880iiotQ9/yRCkDGp9fNThsrLdZiK6OIAcIBAqi2rI89aS1dAmnRbktQieCx5izzyYkR1KvVL3gTTllHOzfKVEC2asmtWu2e4se/+O4WMIS1eGrn7GeWVb0Vwc5ABEBAAHNETxhQEBiLmV4YW1wbGUuZGU+wsCJBBABCAAzAhkBBQJcxt5FAhsDBAsJCAcGFQgJCgsCAxYCARYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiML938H/18F+3Wf9/JaAy/8hCO1v4S2PVBhxaKCokaNFtkfaMRne2l087LscCFPiFNyb4mv6Z3YeK8Xpxlp2sI0ecvdiqLUOGfnxS6tQrj+83EjtIrZ/hXOk1h121QFWH9Zg2VNHtODXjAgdLDC0NWUrclR0ZOqEDQHeo0ibTILdokVfXFN25wakPmGaYJP2y729cb1ve7RzvIvwn+Dddfxo3ao72rBfLi7l4NQ4S0KsY4cw+/6l5bRCKYCP77wZtvCwUvfVVosLdT43agtSiBI49+ayqvZ8OCvSJa61i+v81brTiEy9GBod4eAp45Ibsuemkw+gon4ZOvUXHTjwFB+h63MrozOwE0EXMbePQEIAL/vauf1zK8JgCu3V+G+SOX0iWw5xUlCPX+ERpBbWfwu3uAqn4wYXD3JDE/fVAF668xiV4eTPtlSUd5h0mn+G7uXMMOtkb+20SoEt50f8zw8TrL9t+ZsV11GKZWJpCar5AhXWsn6EEi8I2hLL5vn55ZZmHuGgN4jjmkRl3ToKCLhaXwTBjCJem7N5EH7F75wErEITa55v4Lb4Nfca7vnvtYrI1OA446xa8gHra0SINelTD09/JM/Fw4sWVPBaRZmJK/Tnu79N23No9XBUubmFPv1pNexZsQclicnTpt/BEWhiun7d6lfGB63K1aoHRTR1pcrWvBuALuuz0gqar2zlI0AEQEAAcLAdgQYAQgAIAUCXMbeRQIbDBYhBI4xxYKBgH3ANh5cufaKrc9mtiMLAAoJEPaKrc9mtiMLKSEIAIyLCRO2OyZ0IYRvRPpMn4p7E+7Pfcz/0mSkOy+1hshgJnqivXurm8zwGrwdMqeV4eslKR9H1RUdWGUQJNbtwmmjrt5DHpIhYHl5t3FpCBaGbV20Omo00Q38lBl9MtrmZkZw+ktEk6X+0xCKssMF+2MADkSOIufbR5HrDVB89VZOHCO9DeXvCUUAw2hyJiL/LHmLzJ40zYoTmb+F//f0k0j+tRdbkefyRoCmwG7YGiT+2hnCdgcezswnzah5J3ZKlrg7jOGo1LxtbvNUzxNBbC6S/aNgwm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc=";
#[test]
fn test_from_str() {
fn test_from_str() -> Result<()> {
let h: Aheader = format!(
"addr=me@mail.com; prefer-encrypt=mutual; keydata={}",
RAWKEY
)
.parse()
.expect("failed to parse");
.parse()?;
assert_eq!(h.addr, "me@mail.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
Ok(())
}
// EncryptPreference::Reset is an internal value, parser should never return it
#[test]
fn test_from_str_reset() {
fn test_from_str_reset() -> Result<()> {
let raw = format!(
"addr=reset@example.com; prefer-encrypt=reset; keydata={}",
RAWKEY
);
let h: Aheader = raw.parse().expect("failed to parse");
let h: Aheader = raw.parse()?;
assert_eq!(h.addr, "reset@example.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
Ok(())
}
#[test]
fn test_from_str_non_critical() {
fn test_from_str_non_critical() -> Result<()> {
let raw = format!("addr=me@mail.com; _foo=one; _bar=two; keydata={}", RAWKEY);
let h: Aheader = raw.parse().expect("failed to parse");
let h: Aheader = raw.parse()?;
assert_eq!(h.addr, "me@mail.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::NoPreference);
Ok(())
}
#[test]
@@ -216,7 +222,7 @@ mod tests {
}
#[test]
fn test_good_headers() {
fn test_good_headers() -> Result<()> {
let fixed_header = concat!(
"addr=a@b.example.org; prefer-encrypt=mutual; ",
"keydata=xsBNBFzG3j0BCAC6iNhT8zydvCXi8LI/gFnkadMbfmSE/rTJskRRra/utGbLyDta/yTrJg",
@@ -242,7 +248,7 @@ mod tests {
" wm6qxo7xegRhmEl5uZ16zwyj4qz+xkjGy25Of5mWfUDoNw7OT7sjUbHOOMc="
);
let ah = Aheader::from_str(fixed_header).expect("failed to parse");
let ah = Aheader::from_str(fixed_header)?;
assert_eq!(ah.addr, "a@b.example.org");
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
assert_eq!(format!("{}", ah), fixed_header);
@@ -250,18 +256,17 @@ mod tests {
let rendered = ah.to_string();
assert_eq!(rendered, fixed_header);
let ah = Aheader::from_str(&format!(" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {}", RAWKEY)).expect("failed to parse");
let ah = Aheader::from_str(&format!(" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {}", RAWKEY))?;
assert_eq!(ah.addr, "a@b.example.org");
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
Aheader::from_str(&format!(
"addr=a@b.example.org; prefer-encrypt=ignoreUnknownValues; keydata={}",
RAWKEY
))
.expect("failed to parse");
))?;
Aheader::from_str(&format!("addr=a@b.example.org; keydata={}", RAWKEY))
.expect("failed to parse");
Aheader::from_str(&format!("addr=a@b.example.org; keydata={}", RAWKEY))?;
Ok(())
}
#[test]

File diff suppressed because it is too large Load Diff

View File

@@ -11,9 +11,9 @@ use crate::constants::{
use crate::contact::Contact;
use crate::context::Context;
use crate::ephemeral::delete_expired_messages;
use crate::lot::Lot;
use crate::message::{Message, MessageState, MsgId};
use crate::stock_str;
use crate::summary::Summary;
/// An object representing a single chatlist in memory.
///
@@ -288,26 +288,13 @@ impl Chatlist {
}
}
/// Get a summary for a chatlist index.
///
/// The summary is returned by a dc_lot_t object with the following fields:
///
/// - dc_lot_t::text1: contains the username or the strings "Me", "Draft" and so on.
/// The string may be colored by having a look at text1_meaning.
/// If there is no such name or it should not be displayed, the element is NULL.
/// - dc_lot_t::text1_meaning: one of DC_TEXT1_USERNAME, DC_TEXT1_SELF or DC_TEXT1_DRAFT.
/// Typically used to show dc_lot_t::text1 with different colors. 0 if not applicable.
/// - dc_lot_t::text2: contains an excerpt of the message text or strings as
/// "No messages". May be NULL of there is no such text (eg. for the archive link)
/// - 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.
/// Returns a summary for a given chatlist index.
pub async fn get_summary(
&self,
context: &Context,
index: usize,
chat: Option<&Chat>,
) -> Result<Lot> {
) -> Result<Summary> {
// 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".
@@ -320,14 +307,13 @@ impl Chatlist {
Chatlist::get_summary2(context, *chat_id, *lastmsg_id, chat).await
}
/// Returns a summary for a given chatlist item.
pub async fn get_summary2(
context: &Context,
chat_id: ChatId,
lastmsg_id: Option<MsgId>,
chat: Option<&Chat>,
) -> Result<Lot> {
let mut ret = Lot::new();
) -> Result<Summary> {
let chat_loaded: Chat;
let chat = if let Some(chat) = chat {
chat
@@ -343,10 +329,9 @@ impl Chatlist {
(Some(lastmsg), None)
} else {
match chat.typ {
Chattype::Group | Chattype::Mailinglist => {
let lastcontact =
Contact::load_from_db(context, lastmsg.from_id).await.ok();
(Some(lastmsg), lastcontact)
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
let lastcontact = Contact::load_from_db(context, lastmsg.from_id).await?;
(Some(lastmsg), Some(lastcontact))
}
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
}
@@ -356,17 +341,15 @@ impl Chatlist {
};
if chat.id.is_archived_link() {
ret.text2 = None;
} else if let Some(mut lastmsg) =
lastmsg.filter(|msg| msg.from_id != DC_CONTACT_ID_UNDEFINED)
{
ret.fill(&mut lastmsg, chat, lastcontact.as_ref(), context)
.await;
Ok(Default::default())
} else if let Some(lastmsg) = lastmsg.filter(|msg| msg.from_id != DC_CONTACT_ID_UNDEFINED) {
Ok(Summary::new(context, &lastmsg, chat, lastcontact.as_ref()).await)
} else {
ret.text2 = Some(stock_str::no_messages(context).await);
Ok(Summary {
text: stock_str::no_messages(context).await,
..Default::default()
})
}
Ok(ret)
}
pub fn get_index_for_id(&self, id: ChatId) -> Option<usize> {
@@ -637,6 +620,6 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
let summary = chats.get_summary(&t, 0, None).await.unwrap();
assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary
assert_eq!(summary.text, "foo: bar test"); // the linebreak should be removed from summary
}
}

View File

@@ -1,6 +1,6 @@
//! # Key-value configuration management.
use anyhow::Result;
use anyhow::{ensure, Result};
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
@@ -170,6 +170,11 @@ pub enum Config {
/// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely.
#[strum(props(default = "60"))]
ScanAllFoldersDebounceSecs,
/// Defines the max. size (in bytes) of messages downloaded automatically.
/// 0 = no limit.
#[strum(props(default = "0"))]
DownloadLimit,
}
impl Context {
@@ -317,7 +322,7 @@ impl Context {
.set_raw_config(key, value)
.await
.map_err(Into::into);
job::schedule_resync(self).await;
job::schedule_resync(self).await?;
ret
}
_ => {
@@ -332,6 +337,21 @@ impl Context {
.await?;
Ok(())
}
/// Sets an ui-specific key-value pair.
/// Keys must be prefixed by `ui.`
/// and should be followed by the name of the system and maybe subsystem,
/// eg. `ui.desktop.linux.foo`, `ui.desktop.macos.bar`, `ui.ios.foobar`.
pub async fn set_ui_config(&self, key: &str, value: Option<&str>) -> Result<()> {
ensure!(key.starts_with("ui."), "set_ui_config(): prefix missing.");
self.sql.set_raw_config(key, value).await
}
/// Gets an ui-specific value set by set_ui_config().
pub async fn get_ui_config(&self, key: &str) -> Result<Option<String>> {
ensure!(key.starts_with("ui."), "get_ui_config(): prefix missing.");
self.sql.get_raw_config(key).await
}
}
/// Returns all available configuration keys concated together.
@@ -384,4 +404,25 @@ mod tests {
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
assert_eq!(media_quality, constants::MediaQuality::Worse);
}
#[async_std::test]
async fn test_ui_config() -> Result<()> {
let t = TestContext::new().await;
assert_eq!(t.get_ui_config("ui.desktop.linux.systray").await?, None);
t.set_ui_config("ui.android.screen_security", Some("safe"))
.await?;
assert_eq!(
t.get_ui_config("ui.android.screen_security").await?,
Some("safe".to_string())
);
t.set_ui_config("ui.android.screen_security", None).await?;
assert_eq!(t.get_ui_config("ui.android.screen_security").await?, None);
assert!(t.set_ui_config("configured", Some("bar")).await.is_err());
Ok(())
}
}

View File

@@ -14,8 +14,7 @@ use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use crate::dc_tools::EmailAddress;
use crate::imap::Imap;
use crate::login_param::Socks5Config;
use crate::login_param::{LoginParam, ServerLoginParam};
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam, Socks5Config};
use crate::message::Message;
use crate::oauth2::dc_get_oauth2_addr;
use crate::provider::{Protocol, Socket, UsernamePattern};
@@ -250,6 +249,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
}
}
},
strict_tls: Some(provider.strict_tls),
})
.collect();
@@ -290,6 +290,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
port: param.imap.port,
socket: param.imap.security,
username: param.imap.user.clone(),
strict_tls: None,
})
}
if !servers
@@ -302,8 +303,24 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
port: param.smtp.port,
socket: param.smtp.security,
username: param.smtp.user.clone(),
strict_tls: None,
})
}
// respect certificate setting from function parameters
for mut server in &mut servers {
let certificate_checks = match server.protocol {
Protocol::Imap => param.imap.certificate_checks,
Protocol::Smtp => param.smtp.certificate_checks,
};
server.strict_tls = match certificate_checks {
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => Some(false),
CertificateChecks::Strict => Some(true),
CertificateChecks::Automatic => server.strict_tls,
};
}
let servers = expand_param_vector(servers, &param.addr, &param_domain);
progress!(ctx, 550);
@@ -319,7 +336,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
.filter(|params| params.protocol == Protocol::Smtp)
.cloned()
.collect();
let provider_strict_tls = param.provider.map_or(false, |provider| provider.strict_tls);
let provider_strict_tls = param
.provider
.map_or(socks5_config.is_some(), |provider| provider.strict_tls);
let smtp_config_task = task::spawn(async move {
let mut smtp_configured = false;
@@ -329,6 +348,11 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
smtp_param.server = smtp_server.hostname.clone();
smtp_param.port = smtp_server.port;
smtp_param.security = smtp_server.socket;
smtp_param.certificate_checks = match smtp_server.strict_tls {
Some(true) => CertificateChecks::Strict,
Some(false) => CertificateChecks::AcceptInvalidCertificates,
None => CertificateChecks::Automatic,
};
match try_smtp_one_param(
&context_smtp,
@@ -372,6 +396,11 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
param.imap.server = imap_server.hostname.clone();
param.imap.port = imap_server.port;
param.imap.security = imap_server.socket;
param.imap.certificate_checks = match imap_server.strict_tls {
Some(true) => CertificateChecks::Strict,
Some(false) => CertificateChecks::AcceptInvalidCertificates,
None => CertificateChecks::Automatic,
};
match try_imap_one_param(
ctx,
@@ -442,7 +471,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
ctx,
job::Job::new(Action::FetchExistingMsgs, 0, Params::new(), 0),
)
.await;
.await?;
progress!(ctx, 940);
update_device_chats_handle.await?;

View File

@@ -243,6 +243,7 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerPar
hostname: server.hostname,
port: server.port,
username: server.username,
strict_tls: None,
})
})
.collect();

View File

@@ -180,6 +180,7 @@ fn protocols_to_serverparams(protocols: Vec<ProtocolTag>) -> Vec<ServerParams> {
hostname: protocol.server,
port: protocol.port,
username: String::new(),
strict_tls: None,
})
})
.collect()

View File

@@ -22,6 +22,9 @@ pub(crate) struct ServerParams {
/// Username, empty if unknown.
pub username: String,
/// Whether TLS certificates should be strictly checked or not, `None` for automatic.
pub strict_tls: Option<bool>,
}
impl ServerParams {
@@ -128,6 +131,23 @@ impl ServerParams {
vec![self]
}
}
fn expand_strict_tls(self) -> Vec<ServerParams> {
if self.strict_tls.is_none() {
vec![
Self {
strict_tls: Some(true), // Strict.
..self.clone()
},
Self {
strict_tls: None, // Automatic.
..self
},
]
} else {
vec![self]
}
}
}
/// Expands vector of `ServerParams`, replacing placeholders with
@@ -138,10 +158,32 @@ pub(crate) fn expand_param_vector(
domain: &str,
) -> Vec<ServerParams> {
v.into_iter()
// The order of expansion is important: ports are expanded the
// last, so they are changed the first. Username is only
// changed if default value (address with domain) didn't work
// for all available hosts and ports.
.map(|params| {
if params.socket == Socket::Plain {
ServerParams {
// Avoid expanding plaintext configuration into configuration with and without
// `strict_tls` if `strict_tls` is set to `None` as `strict_tls` is not used for
// plaintext connections. Always setting it to "enabled", just in case.
strict_tls: Some(true),
..params
}
} else {
params
}
})
// The order of expansion is important.
//
// Ports are expanded the last, so they are changed the first. Username is only changed if
// default value (address with domain) didn't work for all available hosts and ports.
//
// Strict TLS must be expanded first, so we try all configurations with strict TLS first
// and only then try again without strict TLS. Otherwise we may lock to wrong hostname
// without strict TLS when another hostname with strict TLS is available. For example, if
// both smtp.example.net and mail.example.net are running an SMTP server, but both use a
// certificate that is only valid for mail.example.net, we want to skip smtp.example.net
// and use mail.example.net with strict TLS instead of using smtp.example.net without
// strict TLS.
.flat_map(|params| params.expand_strict_tls().into_iter())
.flat_map(|params| params.expand_usernames(addr).into_iter())
.flat_map(|params| params.expand_hostnames(domain).into_iter())
.flat_map(|params| params.expand_ports().into_iter())
@@ -161,6 +203,7 @@ mod tests {
port: 0,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",
@@ -174,6 +217,7 @@ mod tests {
port: 993,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
}],
);
@@ -184,6 +228,7 @@ mod tests {
port: 123,
socket: Socket::Automatic,
username: "foobar".to_string(),
strict_tls: None,
}],
"foobar@example.net",
"example.net",
@@ -197,16 +242,59 @@ mod tests {
hostname: "example.net".to_string(),
port: 123,
socket: Socket::Ssl,
username: "foobar".to_string()
username: "foobar".to_string(),
strict_tls: Some(true),
},
ServerParams {
protocol: Protocol::Smtp,
hostname: "example.net".to_string(),
port: 123,
socket: Socket::Starttls,
username: "foobar".to_string()
username: "foobar".to_string(),
strict_tls: Some(true)
},
ServerParams {
protocol: Protocol::Smtp,
hostname: "example.net".to_string(),
port: 123,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: None,
},
ServerParams {
protocol: Protocol::Smtp,
hostname: "example.net".to_string(),
port: 123,
socket: Socket::Starttls,
username: "foobar".to_string(),
strict_tls: None
}
],
);
// Test that strict_tls is not expanded for plaintext connections.
let v = expand_param_vector(
vec![ServerParams {
protocol: Protocol::Smtp,
hostname: "example.net".to_string(),
port: 123,
socket: Socket::Plain,
username: "foobar".to_string(),
strict_tls: None,
}],
"foobar@example.net",
"example.net",
);
assert_eq!(
v,
vec![ServerParams {
protocol: Protocol::Smtp,
hostname: "example.net".to_string(),
port: 123,
socket: Socket::Plain,
username: "foobar".to_string(),
strict_tls: Some(true)
}],
);
}
}

View File

@@ -153,6 +153,7 @@ pub enum Chattype {
Single = 100,
Group = 120,
Mailinglist = 140,
Broadcast = 160,
}
impl Default for Chattype {
@@ -348,6 +349,7 @@ mod tests {
assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap());
assert_eq!(Chattype::Group, Chattype::from_i32(120).unwrap());
assert_eq!(Chattype::Mailinglist, Chattype::from_i32(140).unwrap());
assert_eq!(Chattype::Broadcast, Chattype::from_i32(160).unwrap());
}
#[test]

View File

@@ -2,7 +2,7 @@
use std::convert::{TryFrom, TryInto};
use anyhow::{bail, ensure, format_err, Result};
use anyhow::{bail, ensure, Context as _, Result};
use async_std::path::PathBuf;
use deltachat_derive::{FromSql, ToSql};
use itertools::Itertools;
@@ -174,6 +174,12 @@ pub enum VerifiedStatus {
BidirectVerified = 2,
}
impl Default for VerifiedStatus {
fn default() -> Self {
Self::Unverified
}
}
impl Contact {
pub async fn load_from_db(context: &Context, contact_id: u32) -> Result<Self> {
let mut contact = context
@@ -229,11 +235,9 @@ impl Contact {
}
/// Check if a contact is blocked.
pub async fn is_blocked_load(context: &Context, id: u32) -> bool {
Self::load_from_db(context, id)
.await
.map(|contact| contact.blocked)
.unwrap_or_default()
pub async fn is_blocked_load(context: &Context, id: u32) -> Result<bool> {
let blocked = Self::load_from_db(context, id).await?.blocked;
Ok(blocked)
}
/// Block the given contact.
@@ -263,7 +267,7 @@ impl Contact {
let (contact_id, sth_modified) =
Contact::add_or_lookup(context, &name, &addr, Origin::ManuallyCreated).await?;
let blocked = Contact::is_blocked_load(context, contact_id).await;
let blocked = Contact::is_blocked_load(context, contact_id).await?;
match sth_modified {
Modifier::None => {}
Modifier::Modified | Modifier::Created => {
@@ -754,12 +758,9 @@ impl Contact {
/// Get blocked contacts.
pub async fn get_all_blocked(context: &Context) -> Result<Vec<u32>> {
if let Err(e) = Contact::update_blocked_mailinglist_contacts(context).await {
warn!(
context,
"Cannot update blocked mailinglist contacts: {:?}", e
);
}
Contact::update_blocked_mailinglist_contacts(context)
.await
.context("cannot update blocked mailinglist contacts")?;
let list = context
.sql
@@ -1018,7 +1019,7 @@ impl Contact {
///
/// The UI may draw a checkbox or something like that beside verified contacts.
///
pub async fn is_verified(&self, context: &Context) -> VerifiedStatus {
pub async fn is_verified(&self, context: &Context) -> Result<VerifiedStatus> {
self.is_verified_ex(context, None).await
}
@@ -1029,54 +1030,46 @@ impl Contact {
&self,
context: &Context,
peerstate: Option<&Peerstate>,
) -> VerifiedStatus {
) -> Result<VerifiedStatus> {
// We're always sort of secured-verified as we could verify the key on this device any time with the key
// on this device
if self.id == DC_CONTACT_ID_SELF {
return VerifiedStatus::BidirectVerified;
return Ok(VerifiedStatus::BidirectVerified);
}
if let Some(peerstate) = peerstate {
if peerstate.verified_key.is_some() {
return VerifiedStatus::BidirectVerified;
return Ok(VerifiedStatus::BidirectVerified);
}
}
let peerstate = match Peerstate::from_addr(context, &self.addr).await {
Ok(peerstate) => peerstate,
Err(err) => {
warn!(
context,
"Failed to load peerstate for address {}: {}", self.addr, err
);
return VerifiedStatus::Unverified;
}
};
if let Some(ps) = peerstate {
if ps.verified_key.is_some() {
return VerifiedStatus::BidirectVerified;
if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
if peerstate.verified_key.is_some() {
return Ok(VerifiedStatus::BidirectVerified);
}
}
VerifiedStatus::Unverified
Ok(VerifiedStatus::Unverified)
}
pub async fn addr_equals_contact(context: &Context, addr: &str, contact_id: u32) -> bool {
pub async fn addr_equals_contact(
context: &Context,
addr: &str,
contact_id: u32,
) -> Result<bool> {
if addr.is_empty() {
return false;
return Ok(false);
}
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
if !contact.addr.is_empty() {
let normalized_addr = addr_normalize(addr);
if contact.addr == normalized_addr {
return true;
}
let contact = Contact::load_from_db(context, contact_id).await?;
if !contact.addr.is_empty() {
let normalized_addr = addr_normalize(addr);
if contact.addr == normalized_addr {
return Ok(true);
}
}
false
Ok(false)
}
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
@@ -1094,30 +1087,34 @@ impl Contact {
Ok(count)
}
pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> bool {
if !context.sql.is_open().await || contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return false;
pub async fn real_exists_by_id(context: &Context, contact_id: u32) -> Result<bool> {
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
return Ok(false);
}
context
let exists = context
.sql
.exists(
"SELECT COUNT(*) FROM contacts WHERE id=?;",
paramsv![contact_id as i32],
)
.await
.unwrap_or_default()
.await?;
Ok(exists)
}
pub async fn scaleup_origin_by_id(context: &Context, contact_id: u32, origin: Origin) -> bool {
pub async fn scaleup_origin_by_id(
context: &Context,
contact_id: u32,
origin: Origin,
) -> Result<()> {
context
.sql
.execute(
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
paramsv![origin, contact_id as i32, origin],
)
.await
.is_ok()
.await?;
Ok(())
}
}
@@ -1342,12 +1339,11 @@ fn cat_fingerprint(
impl Context {
/// determine whether the specified addr maps to the/a self addr
pub async fn is_self_addr(&self, addr: &str) -> Result<bool> {
let self_addr = self
.get_config(Config::ConfiguredAddr)
.await?
.ok_or_else(|| format_err!("Not configured"))?;
Ok(addr_cmp(self_addr, addr))
if let Some(self_addr) = self.get_config(Config::ConfiguredAddr).await? {
Ok(addr_cmp(self_addr, addr))
} else {
Ok(false)
}
}
}
@@ -1494,7 +1490,7 @@ mod tests {
#[async_std::test]
async fn test_is_self_addr() -> Result<()> {
let t = TestContext::new().await;
assert!(t.is_self_addr("me@me.org").await.is_err());
assert_eq!(t.is_self_addr("me@me.org").await?, false);
let addr = t.configure_alice().await;
assert_eq!(t.is_self_addr("me@me.org").await?, false);

View File

@@ -371,6 +371,12 @@ impl Context {
"show_emails",
self.get_config_int(Config::ShowEmails).await?.to_string(),
);
res.insert(
"download_limit",
self.get_config_int(Config::DownloadLimit)
.await?
.to_string(),
);
res.insert("inbox_watch", inbox_watch.to_string());
res.insert("sentbox_watch", sentbox_watch.to_string());
res.insert("mvbox_watch", mvbox_watch.to_string());

File diff suppressed because it is too large Load Diff

View File

@@ -84,9 +84,9 @@ pub(crate) fn dc_gm2local_offset() -> i64 {
// but at max `MAX_SECONDS_TO_LEND_FROM_FUTURE`
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!
/// Returns the current smeared timestamp,
///
/// The returned timestamp MUST NOT be sent out.
pub(crate) async fn dc_smeared_time(context: &Context) -> i64 {
let mut now = time();
let ts = *context.last_smeared_timestamp.read().await;
@@ -97,7 +97,7 @@ pub(crate) async fn dc_smeared_time(context: &Context) -> i64 {
now
}
// returns a timestamp that is guaranteed to be unique.
/// Returns a timestamp that is guaranteed to be unique.
pub(crate) async fn dc_create_smeared_timestamp(context: &Context) -> i64 {
let now = time();
let mut ret = now;

346
src/download.rs Normal file
View File

@@ -0,0 +1,346 @@
//! # Download large messages manually.
use anyhow::{anyhow, Result};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use crate::config::Config;
use crate::constants::Viewtype;
use crate::context::Context;
use crate::dc_tools::time;
use crate::imap::{Imap, ImapActionResult};
use crate::job::{self, Action, Job, Status};
use crate::message::{Message, MsgId};
use crate::mimeparser::{MimeMessage, Part};
use crate::param::Params;
use crate::{job_try, stock_str, EventType};
use std::cmp::max;
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
///
/// Some messages as non-delivery-reports (NDN) or read-receipts (MDN)
/// need to be downloaded completely to handle them correctly,
/// eg. to assign them to the correct chat.
/// As these messages are typically small,
/// they're catched by `MIN_DOWNLOAD_LIMIT`.
const MIN_DOWNLOAD_LIMIT: u32 = 32768;
/// If a message is downloaded only partially
/// and `delete_server_after` is set to small timeouts (eg. "at once"),
/// the user might have no chance to actually download that message.
/// `MIN_DELETE_SERVER_AFTER` increases the timeout in this case.
pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60;
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
Serialize,
Deserialize,
)]
#[repr(u32)]
pub enum DownloadState {
Done = 0,
Available = 10,
Failure = 20,
InProgress = 1000,
}
impl Default for DownloadState {
fn default() -> Self {
DownloadState::Done
}
}
impl Context {
// Returns validated download limit or `None` for "no limit".
pub(crate) async fn download_limit(&self) -> Result<Option<u32>> {
let download_limit = self.get_config_int(Config::DownloadLimit).await?;
if download_limit <= 0 {
Ok(None)
} else {
Ok(Some(max(MIN_DOWNLOAD_LIMIT, download_limit as u32)))
}
}
}
impl MsgId {
/// Schedules full message download for partially downloaded message.
pub async fn download_full(self, context: &Context) -> Result<()> {
let msg = Message::load_from_db(context, self).await?;
match msg.download_state() {
DownloadState::Done => return Err(anyhow!("Nothing to download.")),
DownloadState::InProgress => return Err(anyhow!("Download already in progress.")),
DownloadState::Available | DownloadState::Failure => {
self.update_download_state(context, DownloadState::InProgress)
.await?;
job::add(
context,
Job::new(Action::DownloadMsg, self.to_u32(), Params::new(), 0),
)
.await?;
}
}
Ok(())
}
pub(crate) async fn update_download_state(
self,
context: &Context,
download_state: DownloadState,
) -> Result<()> {
let msg = Message::load_from_db(context, self).await?;
context
.sql
.execute(
"UPDATE msgs SET download_state=? WHERE id=?;",
paramsv![download_state, self],
)
.await?;
context.emit_event(EventType::MsgsChanged {
chat_id: msg.chat_id,
msg_id: self,
});
Ok(())
}
}
impl Message {
/// Returns the download state of the message.
pub fn download_state(&self) -> DownloadState {
self.download_state
}
}
impl Job {
/// Actually download a message.
/// Called in response to `Action::DownloadMsg`.
pub(crate) async fn download_msg(&self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
warn!(context, "download: could not connect: {:?}", err);
return Status::RetryNow;
}
let msg = job_try!(Message::load_from_db(context, MsgId::new(self.foreign_id)).await);
let server_folder = msg.server_folder.unwrap_or_default();
match imap
.fetch_single_msg(context, &server_folder, msg.server_uid)
.await
{
ImapActionResult::RetryLater | ImapActionResult::Failed => {
job_try!(
msg.id
.update_download_state(context, DownloadState::Failure)
.await
);
Status::Finished(Err(anyhow!("Call download_full() again to try over.")))
}
ImapActionResult::Success | ImapActionResult::AlreadyDone => {
// update_download_state() not needed as receive_imf() already
// set the state and emitted the event.
Status::Finished(Ok(()))
}
}
}
}
impl Imap {
/// Download a single message and pipe it to receive_imf().
///
/// receive_imf() is not directly aware that this is a result of a call to download_msg(),
/// however, implicitly knows that as the existing message is flagged as being partly.
async fn fetch_single_msg(
&mut self,
context: &Context,
folder: &str,
uid: u32,
) -> ImapActionResult {
if let Some(imapresult) = self
.prepare_imap_operation_on_msg(context, folder, uid)
.await
{
return imapresult;
}
// we are connected, and the folder is selected
info!(context, "Downloading message {}/{} fully...", folder, uid);
let (_, error_cnt) = self
.fetch_many_msgs(context, folder, vec![uid], false, false)
.await;
if error_cnt > 0 {
return ImapActionResult::Failed;
}
ImapActionResult::Success
}
}
impl MimeMessage {
/// Creates a placeholder part and add that to `parts`.
///
/// To create the placeholder, only the outermost header can be used,
/// the mime-structure itself is not available.
///
/// The placeholder part currently contains a text with size and availability of the message;
/// in the future, we may do more advanced things as previews here.
pub(crate) async fn create_stub_from_partial_download(
&mut self,
context: &Context,
org_bytes: u32,
) -> Result<()> {
let mut text = format!(
"[{}]",
stock_str::partial_download_msg_body(context, org_bytes).await
);
if let Some(delete_server_after) = context.get_config_delete_server_after().await? {
let until = stock_str::download_availability(
context,
time() + max(delete_server_after, MIN_DELETE_SERVER_AFTER),
)
.await;
text += format!(" [{}]", until).as_str();
};
info!(context, "Partial download: {}", text);
self.parts.push(Part {
typ: Viewtype::Text,
msg: text,
..Default::default()
});
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::send_msg;
use crate::constants::Viewtype;
use crate::dc_receive_imf::dc_receive_imf_inner;
use crate::test_utils::TestContext;
use num_traits::FromPrimitive;
#[test]
fn test_downloadstate_values() {
// values may be written to disk and must not change
assert_eq!(DownloadState::Done, DownloadState::default());
assert_eq!(DownloadState::Done, DownloadState::from_i32(0).unwrap());
assert_eq!(
DownloadState::Available,
DownloadState::from_i32(10).unwrap()
);
assert_eq!(DownloadState::Failure, DownloadState::from_i32(20).unwrap());
assert_eq!(
DownloadState::InProgress,
DownloadState::from_i32(1000).unwrap()
);
}
#[async_std::test]
async fn test_download_limit() -> Result<()> {
let t = TestContext::new_alice().await;
assert_eq!(t.download_limit().await?, None);
t.set_config(Config::DownloadLimit, Some("200000")).await?;
assert_eq!(t.download_limit().await?, Some(200000));
t.set_config(Config::DownloadLimit, Some("20000")).await?;
assert_eq!(t.download_limit().await?, Some(MIN_DOWNLOAD_LIMIT));
t.set_config(Config::DownloadLimit, None).await?;
assert_eq!(t.download_limit().await?, None);
for val in &["0", "-1", "-100", "", "foo"] {
t.set_config(Config::DownloadLimit, Some(val)).await?;
assert_eq!(t.download_limit().await?, None);
}
Ok(())
}
#[async_std::test]
async fn test_update_download_state() -> Result<()> {
let t = TestContext::new_alice().await;
let chat = t.create_chat_with_contact("Bob", "bob@example.org").await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("Hi Bob".to_owned()));
let msg_id = send_msg(&t, chat.id, &mut msg).await?;
let msg = Message::load_from_db(&t, msg_id).await?;
assert_eq!(msg.download_state(), DownloadState::Done);
for s in &[
DownloadState::Available,
DownloadState::InProgress,
DownloadState::Failure,
DownloadState::Done,
] {
msg_id.update_download_state(&t, *s).await?;
let msg = Message::load_from_db(&t, msg_id).await?;
assert_eq!(msg.download_state(), *s);
}
Ok(())
}
#[async_std::test]
async fn test_partial_receive_imf() -> Result<()> {
let t = TestContext::new_alice().await;
let header =
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: bob@example.com\n\
To: alice@example.org\n\
Subject: foo\n\
Message-ID: <Mr.12345678901@example.com>\n\
Chat-Version: 1.0\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\
Content-Type: text/plain";
dc_receive_imf_inner(
&t,
header.as_bytes(),
"INBOX",
1,
false,
Some(100000),
false,
)
.await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.download_state(), DownloadState::Available);
assert_eq!(msg.get_subject(), "foo");
assert!(msg
.get_text()
.unwrap()
.contains(&stock_str::partial_download_msg_body(&t, 100000).await));
dc_receive_imf_inner(
&t,
format!("{}\n\n100k text...", header).as_bytes(),
"INBOX",
1,
false,
None,
false,
)
.await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.download_state(), DownloadState::Done);
assert_eq!(msg.get_subject(), "foo");
assert_eq!(msg.get_text(), Some("100k text...".to_string()));
Ok(())
}
}

View File

@@ -161,15 +161,19 @@ pub async fn try_decrypt(
let mut peerstate = Peerstate::from_addr(context, &from).await?;
// Apply Autocrypt header
if let Some(ref header) = Aheader::from_headers(context, &from, &mail.headers) {
if let Some(ref mut peerstate) = peerstate {
peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else {
let p = Peerstate::from_header(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
match Aheader::from_headers(&from, &mail.headers) {
Ok(Some(ref header)) => {
if let Some(ref mut peerstate) = peerstate {
peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
} else {
let p = Peerstate::from_header(header, message_time);
p.save_to_db(&context.sql, true).await?;
peerstate = Some(p);
}
}
Ok(None) => {}
Err(err) => warn!(context, "Failed to parse Autocrypt header: {}", err),
}
// Possibly perform decryption

View File

@@ -71,11 +71,13 @@ use crate::constants::{
};
use crate::context::Context;
use crate::dc_tools::time;
use crate::download::MIN_DELETE_SERVER_AFTER;
use crate::events::EventType;
use crate::job;
use crate::message::{Message, MessageState, MsgId};
use crate::mimeparser::SystemMessage;
use crate::stock_str;
use std::cmp::max;
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
pub enum Timer {
@@ -279,7 +281,7 @@ impl MsgId {
/// Starts ephemeral message timer for the message if it is not started yet.
pub(crate) async fn start_ephemeral_timer(self, context: &Context) -> anyhow::Result<()> {
if let Timer::Enabled { duration } = self.ephemeral_timer(context).await? {
let ephemeral_timestamp = time() + i64::from(duration);
let ephemeral_timestamp = time().saturating_add(duration.into());
context
.sql
@@ -416,24 +418,18 @@ pub async fn schedule_ephemeral_task(context: &Context) {
let context1 = context.clone();
let ephemeral_task = task::spawn(async move {
async_std::task::sleep(duration).await;
emit_event!(
context1,
EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0)
}
);
context1.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
});
*context.ephemeral_task.write().await = Some(ephemeral_task);
} else {
// Emit event immediately
emit_event!(
context,
EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0)
}
);
context.emit_event(EventType::MsgsChanged {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
}
}
}
@@ -445,23 +441,32 @@ pub async fn schedule_ephemeral_task(context: &Context) {
pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> anyhow::Result<Option<MsgId>> {
let now = time();
let threshold_timestamp = match context.get_config_delete_server_after().await? {
None => 0,
Some(delete_server_after) => now - delete_server_after,
};
let (threshold_timestamp, threshold_timestamp_extended) =
match context.get_config_delete_server_after().await? {
None => (0, 0),
Some(delete_server_after) => (
now - delete_server_after,
now - max(delete_server_after, MIN_DELETE_SERVER_AFTER),
),
};
context
.sql
.query_row_optional(
"SELECT id FROM msgs \
WHERE ( \
timestamp < ? \
((download_state = 0 AND timestamp < ?) OR (download_state != 0 AND timestamp < ?)) \
OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \
) \
AND server_uid != 0 \
AND NOT id IN (SELECT foreign_id FROM jobs WHERE action = ?)
LIMIT 1",
paramsv![threshold_timestamp, now, job::Action::DeleteMsgOnImap],
paramsv![
threshold_timestamp,
threshold_timestamp_extended,
now,
job::Action::DeleteMsgOnImap
],
|row| {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
@@ -506,6 +511,9 @@ mod tests {
use async_std::task::sleep;
use super::*;
use crate::config::Config;
use crate::dc_receive_imf::dc_receive_imf;
use crate::download::DownloadState;
use crate::test_utils::TestContext;
use crate::{
chat::{self, Chat, ChatItem},
@@ -641,8 +649,83 @@ mod tests {
);
}
/// Test enabling and disabling ephemeral timer remotely.
#[async_std::test]
async fn test_ephemeral_timer() -> anyhow::Result<()> {
async fn test_ephemeral_enable_disable() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_alice = alice.create_chat(&bob).await.id;
let chat_bob = bob.create_chat(&alice).await.id;
chat_alice
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
.await?;
let sent = alice.pop_sent_msg().await;
bob.recv_msg(&sent).await;
assert_eq!(
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
Timer::Enabled { duration: 60 }
);
chat_alice
.set_ephemeral_timer(&alice.ctx, Timer::Disabled)
.await?;
let sent = alice.pop_sent_msg().await;
bob.recv_msg(&sent).await;
assert_eq!(
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
Timer::Disabled
);
Ok(())
}
/// Test that timer is enabled even if the message explicitly enabling the timer is lost.
#[async_std::test]
async fn test_ephemeral_enable_lost() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_alice = alice.create_chat(&bob).await.id;
let chat_bob = bob.create_chat(&alice).await.id;
// Alice enables the timer.
chat_alice
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
.await?;
assert_eq!(
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
Timer::Enabled { duration: 60 }
);
// The message enabling the timer is lost.
let _sent = alice.pop_sent_msg().await;
assert_eq!(
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
Timer::Disabled,
);
// Alice sends a text message.
let mut msg = Message::new(Viewtype::Text);
chat::prepare_msg(&alice.ctx, chat_alice, &mut msg).await?;
chat::send_msg(&alice.ctx, chat_alice, &mut msg).await?;
let sent = alice.pop_sent_msg().await;
// Bob receives text message and enables the timer, even though explicit timer update was
// lost previously.
bob.recv_msg(&sent).await;
assert_eq!(
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
Timer::Enabled { duration: 60 }
);
Ok(())
}
/// Test that Alice replying to the chat without a timer at the same time as Bob enables the
/// timer does not result in disabling the timer on the Bob's side.
#[async_std::test]
async fn test_ephemeral_timer_rollback() -> anyhow::Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
@@ -700,6 +783,18 @@ mod tests {
Timer::Enabled { duration: 60 }
);
// Bob disables the chat timer.
// Note that the last message in the Bob's chat is from Alice and has no timer,
// but the chat timer is enabled.
chat_bob
.set_ephemeral_timer(&bob.ctx, Timer::Disabled)
.await?;
alice.recv_msg(&bob.pop_sent_msg().await).await;
assert_eq!(
chat_alice.get_ephemeral_timer(&alice.ctx).await?,
Timer::Disabled
);
Ok(())
}
@@ -777,4 +872,149 @@ mod tests {
assert!(rawtxt.is_none_or_empty(), "{:?}", rawtxt);
}
}
#[async_std::test]
async fn test_load_imap_deletion_msgid() -> Result<()> {
let t = TestContext::new_alice().await;
const HOUR: i64 = 60 * 60;
let now = time();
for (id, timestamp, ephemeral_timestamp) in &[
(900, now - 2 * HOUR, 0),
(1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
(1010, now - 23 * HOUR, 0),
(1020, now - 21 * HOUR, 0),
(1030, now - 19 * HOUR, 0),
(2000, now - 18 * HOUR, now - HOUR),
(2020, now - 17 * HOUR, now + HOUR),
] {
t.sql
.execute(
"INSERT INTO msgs (id, server_uid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
paramsv![id, id, timestamp, ephemeral_timestamp],
)
.await?;
}
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(2000)));
MsgId::new(2000).delete_from_db(&t).await?;
assert_eq!(load_imap_deletion_msgid(&t).await?, None);
t.set_config(Config::DeleteServerAfter, Some(&*(25 * HOUR).to_string()))
.await?;
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(1000)));
MsgId::new(1000)
.update_download_state(&t, DownloadState::Available)
.await?;
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(1000))); // delete downloadable anyway
MsgId::new(1000).delete_from_db(&t).await?;
assert_eq!(load_imap_deletion_msgid(&t).await?, None);
t.set_config(Config::DeleteServerAfter, Some(&*(22 * HOUR).to_string()))
.await?;
assert_eq!(load_imap_deletion_msgid(&t).await?, Some(MsgId::new(1010)));
MsgId::new(1010)
.update_download_state(&t, DownloadState::Available)
.await?;
assert_eq!(load_imap_deletion_msgid(&t).await?, None); // keep downloadable for now
MsgId::new(1010).delete_from_db(&t).await?;
assert_eq!(load_imap_deletion_msgid(&t).await?, None);
Ok(())
}
// Regression test for a bug in the timer rollback protection.
#[async_std::test]
async fn test_ephemeral_timer_references() -> Result<()> {
let alice = TestContext::new_alice().await;
// Message with Message-ID <first@example.com> and no timer is received.
dc_receive_imf(
&alice,
b"From: Bob <bob@example.com>\n\
To: Alice <alice@example.com>\n\
Chat-Version: 1.0\n\
Subject: Subject\n\
Message-ID: <first@example.com>\n\
Date: Sun, 22 Mar 2020 00:10:00 +0000\n\
\n\
hello\n",
"INBOX",
1,
false,
)
.await?;
let msg = alice.get_last_msg().await;
let chat_id = msg.chat_id;
assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled);
// Message with Message-ID <second@example.com> is received.
dc_receive_imf(
&alice,
b"From: Bob <bob@example.com>\n\
To: Alice <alice@example.com>\n\
Chat-Version: 1.0\n\
Subject: Subject\n\
Message-ID: <second@example.com>\n\
Date: Sun, 22 Mar 2020 00:11:00 +0000\n\
Ephemeral-Timer: 60\n\
\n\
second message\n",
"INBOX",
2,
false,
)
.await?;
assert_eq!(
chat_id.get_ephemeral_timer(&alice).await?,
Timer::Enabled { duration: 60 }
);
let msg = alice.get_last_msg().await;
// Message is deleted from the database when its timer expires.
msg.id.delete_from_db(&alice).await?;
// Message with Message-ID <third@example.com>, referencing <first@example.com> and
// <second@example.com>, is received. The message <second@example.come> is not in the
// database anymore, so the timer should be applied unconditionally without rollback
// protection.
//
// Previously Delta Chat fallen back to using <first@example.com> in this case and
// compared received timer value to the timer value of the <first@examle.com>. Because
// their timer values are the same ("disabled"), Delta Chat assumed that the timer was not
// changed explicitly and the change should be ignored.
//
// The message also contains a quote of the first message to test that only References:
// header and not In-Reply-To: is consulted by the rollback protection.
dc_receive_imf(
&alice,
b"From: Bob <bob@example.com>\n\
To: Alice <alice@example.com>\n\
Chat-Version: 1.0\n\
Subject: Subject\n\
Message-ID: <third@example.com>\n\
Date: Sun, 22 Mar 2020 00:12:00 +0000\n\
References: <first@example.com> <second@example.com>\n\
In-Reply-To: <first@example.com>\n\
\n\
> hello\n",
"INBOX",
3,
false,
)
.await?;
let msg = alice.get_last_msg().await;
assert_eq!(
msg.chat_id.get_ephemeral_timer(&alice).await?,
Timer::Disabled
);
Ok(())
}
}

View File

@@ -62,10 +62,6 @@ impl Events {
pub struct EventEmitter(Receiver<Event>);
impl EventEmitter {
pub(crate) fn into_inner(self) -> Receiver<Event> {
self.0
}
/// 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())
@@ -203,7 +199,8 @@ pub enum EventType {
/// - Chats created, deleted or archived
/// - A draft has been set
///
/// The `chat_id` and `msg_id` values will be 0 if more than one message is changed.
/// `chat_id` is set if only a single chat is affected by the changes, otherwise 0.
/// `msg_id` is set if only a single message is affected by the changes, otherwise 0.
#[strum(props(id = "2000"))]
MsgsChanged { chat_id: ChatId, msg_id: MsgId },

View File

@@ -1,9 +1,8 @@
//! # List of email headers.
use crate::strum::AsStaticRef;
use mailparse::{MailHeader, MailHeaderMap};
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, AsStaticStr)]
#[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, IntoStaticStr)]
#[strum(serialize_all = "kebab_case")]
pub enum HeaderDef {
MessageId,
@@ -67,9 +66,9 @@ pub enum HeaderDef {
}
impl HeaderDef {
/// Returns the corresponding Event id.
/// Returns the corresponding header string.
pub fn get_headername(&self) -> &'static str {
self.as_static()
self.into()
}
}

View File

@@ -65,7 +65,7 @@ pub enum ImapActionResult {
/// - Chat-Version to check if a message is a chat message
/// - Autocrypt-Setup-Message to check if a message is an autocrypt setup message,
/// not necessarily sent by Delta Chat.
const PREFETCH_FLAGS: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
const PREFETCH_FLAGS: &str = "(UID RFC822.SIZE BODY.PEEK[HEADER.FIELDS (\
MESSAGE-ID \
FROM \
IN-REPLY-TO REFERENCES \
@@ -81,15 +81,14 @@ const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
X-MICROSOFT-ORIGINAL-MESSAGE-ID\
)])";
const JUST_UID: &str = "(UID)";
const BODY_FLAGS: &str = "(FLAGS BODY.PEEK[])";
const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
#[derive(Debug)]
pub struct Imap {
idle_interrupt: Receiver<InterruptInfo>,
config: ImapConfig,
session: Option<Session>,
connected: bool,
interrupt: Option<stop_token::StopSource>,
should_reconnect: bool,
login_failed_once: bool,
@@ -200,8 +199,6 @@ impl Imap {
idle_interrupt,
config,
session: None,
connected: false,
interrupt: None,
should_reconnect: false,
login_failed_once: false,
connectivity: Default::default(),
@@ -228,7 +225,11 @@ impl Imap {
param.socks5_config.clone(),
&param.addr,
param.server_flags & DC_LP_AUTH_OAUTH2 != 0,
param.provider.map_or(false, |provider| provider.strict_tls),
param
.provider
.map_or(param.socks5_config.is_some(), |provider| {
provider.strict_tls
}),
idle_interrupt,
)
.await?;
@@ -250,7 +251,7 @@ impl Imap {
if self.should_reconnect() {
self.disconnect(context).await;
self.should_reconnect = false;
} else if self.is_connected() {
} else if self.session.is_some() {
return Ok(());
}
@@ -343,13 +344,12 @@ impl Imap {
match login_res {
Ok(session) => {
// needs to be set here to ensure it is set on reconnects.
self.connected = true;
self.session = Some(session);
self.login_failed_once = false;
emit_event!(
context,
EventType::ImapConnected(format!("IMAP-LOGIN as {}", self.config.lp.user))
);
context.emit_event(EventType::ImapConnected(format!(
"IMAP-LOGIN as {}",
self.config.lp.user
)));
Ok(())
}
@@ -438,16 +438,11 @@ impl Imap {
warn!(context, "failed to logout: {:?}", err);
}
}
self.connected = false;
self.capabilities_determined = false;
self.config.selected_folder = None;
self.config.selected_mailbox = None;
}
pub fn is_connected(&self) -> bool {
self.connected
}
pub fn should_reconnect(&self) -> bool {
self.should_reconnect
}
@@ -465,7 +460,7 @@ impl Imap {
self.prepare(context).await?;
while self
.fetch_new_messages(context, &watch_folder, false)
.fetch_new_messages(context, watch_folder, false)
.await?
{
// We fetch until no more new messages are there.
@@ -555,7 +550,7 @@ impl Imap {
context: &Context,
folder: &str,
) -> Result<bool> {
let newly_selected = self.select_folder(context, Some(folder)).await?;
let newly_selected = self.select_or_create_folder(context, folder).await?;
let mailbox = &mut self.config.selected_mailbox.as_ref();
let mailbox =
@@ -583,7 +578,7 @@ impl Imap {
folder, old_uid_next, uid_next, new_uid_validity,
);
set_uid_next(context, folder, uid_next).await?;
job::schedule_resync(context).await;
job::schedule_resync(context).await?;
}
uid_next != old_uid_next // If uid_next changed, there are new emails
} else {
@@ -636,7 +631,7 @@ impl Imap {
set_uid_next(context, folder, new_uid_next).await?;
set_uidvalidity(context, folder, new_uid_validity).await?;
if old_uid_validity != 0 || old_uid_next != 0 {
job::schedule_resync(context).await;
job::schedule_resync(context).await?;
}
info!(
context,
@@ -650,25 +645,24 @@ impl Imap {
Ok(false)
}
pub(crate) async fn fetch_new_messages<S: AsRef<str>>(
pub(crate) async fn fetch_new_messages(
&mut self,
context: &Context,
folder: S,
folder: &str,
fetch_existing_msgs: bool,
) -> Result<bool> {
let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?)
.unwrap_or_default();
let download_limit = context.download_limit().await?;
let new_emails = self
.select_with_uidvalidity(context, folder.as_ref())
.await?;
let new_emails = self.select_with_uidvalidity(context, folder).await?;
if !new_emails && !fetch_existing_msgs {
info!(context, "No new emails in folder {}", folder.as_ref());
info!(context, "No new emails in folder {}", folder);
return Ok(false);
}
let old_uid_next = get_uid_next(context, folder.as_ref()).await?;
let old_uid_next = get_uid_next(context, folder).await?;
let msgs = if fetch_existing_msgs {
self.prefetch_existing_msgs().await?
@@ -676,10 +670,10 @@ impl Imap {
self.prefetch(old_uid_next).await?
};
let read_cnt = msgs.len();
let folder: &str = folder.as_ref();
let mut read_errors = 0;
let mut uids = Vec::with_capacity(msgs.len());
let mut uids_fetch_fully = Vec::with_capacity(msgs.len());
let mut uids_fetch_partially = Vec::with_capacity(msgs.len());
let mut largest_uid_skipped = None;
for (current_uid, msg) in msgs.into_iter() {
@@ -706,7 +700,16 @@ impl Imap {
)
.await
{
uids.push(current_uid);
match download_limit {
Some(download_limit) => {
if msg.size.unwrap_or_default() > download_limit {
uids_fetch_partially.push(current_uid);
} else {
uids_fetch_fully.push(current_uid)
}
}
None => uids_fetch_fully.push(current_uid),
}
} else if read_errors == 0 {
// If there were errors (`read_errors != 0`), stop updating largest_uid_skipped so that uid_next will
// not be updated and we will retry prefetching next time
@@ -714,12 +717,29 @@ impl Imap {
}
}
if !uids.is_empty() {
if !uids_fetch_fully.is_empty() || !uids_fetch_partially.is_empty() {
self.connectivity.set_working(context).await;
}
let (largest_uid_processed, error_cnt) = self
.fetch_many_msgs(context, folder, uids, fetch_existing_msgs)
let (largest_uid_fully_fetched, error_cnt) = self
.fetch_many_msgs(
context,
folder,
uids_fetch_fully,
false,
fetch_existing_msgs,
)
.await;
read_errors += error_cnt;
let (largest_uid_partially_fetched, error_cnt) = self
.fetch_many_msgs(
context,
folder,
uids_fetch_partially,
true,
fetch_existing_msgs,
)
.await;
read_errors += error_cnt;
@@ -730,7 +750,10 @@ impl Imap {
// So: Update the uid_next to the largest uid that did NOT recoverably fail. Not perfect because if there was
// another message afterwards that succeeded, we will not retry. The upside is that we will not retry an infinite amount of times.
let largest_uid_without_errors = max(
largest_uid_processed.unwrap_or(0),
max(
largest_uid_fully_fetched.unwrap_or(0),
largest_uid_partially_fetched.unwrap_or(0),
),
largest_uid_skipped.unwrap_or(0),
);
let new_uid_next = largest_uid_without_errors + 1;
@@ -867,30 +890,25 @@ impl Imap {
/// Fetches a list of messages by server UID.
///
/// Returns the last uid fetch successfully and an error count.
async fn fetch_many_msgs(
pub(crate) async fn fetch_many_msgs(
&mut self,
context: &Context,
folder: &str,
server_uids: Vec<u32>,
fetch_partially: bool,
fetching_existing_messages: bool,
) -> (Option<u32>, usize) {
if server_uids.is_empty() {
return (None, 0);
}
if !self.is_connected() {
warn!(context, "Not connected");
return (None, server_uids.len());
}
if self.session.is_none() {
// we could not get a valid imap session, this should be retried
self.trigger_reconnect(context).await;
warn!(context, "Could not get IMAP session");
return (None, server_uids.len());
}
let session = self.session.as_mut().unwrap();
let session = match self.session.as_mut() {
Some(session) => session,
None => {
warn!(context, "Not connected");
return (None, server_uids.len());
}
};
let sets = build_sequence_sets(server_uids.clone());
let mut read_errors = 0;
@@ -898,7 +916,17 @@ impl Imap {
let mut last_uid = None;
for set in sets.iter() {
let mut msgs = match session.uid_fetch(&set, BODY_FLAGS).await {
let mut msgs = match session
.uid_fetch(
&set,
if fetch_partially {
BODY_PARTIAL
} else {
BODY_FULL
},
)
.await
{
Ok(msgs) => msgs,
Err(err) => {
// TODO: maybe differentiate between IO and input/parsing problems
@@ -933,7 +961,13 @@ impl Imap {
count += 1;
let is_deleted = msg.flags().any(|flag| flag == Flag::Deleted);
if is_deleted || msg.body().is_none() {
let (body, partial) = if fetch_partially {
(msg.header(), msg.size) // `BODY.PEEK[HEADER]` goes to header() ...
} else {
(msg.body(), None) // ... while `BODY.PEEK[]` goes to body() - and includes header()
};
if is_deleted || body.is_none() {
info!(
context,
"Not processing deleted or empty msg {}", server_uid
@@ -947,7 +981,7 @@ impl Imap {
let folder = folder.clone();
// safe, as we checked above that there is a body.
let body = msg.body().unwrap();
let body = body.unwrap();
let is_seen = msg.flags().any(|flag| flag == Flag::Seen);
match dc_receive_imf_inner(
@@ -956,6 +990,7 @@ impl Imap {
&folder,
server_uid,
is_seen,
partial,
fetching_existing_messages,
)
.await
@@ -1011,13 +1046,10 @@ impl Imap {
if let Some(ref mut session) = &mut self.session {
match session.uid_mv(&set, &dest_folder).await {
Ok(_) => {
emit_event!(
context,
EventType::ImapMessageMoved(format!(
"IMAP Message {} moved to {}",
display_folder_id, dest_folder
))
);
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP Message {} moved to {}",
display_folder_id, dest_folder
)));
return ImapActionResult::Success;
}
Err(err) => {
@@ -1055,23 +1087,17 @@ impl Imap {
if !self.add_flag_finalized(context, uid, "\\Deleted").await {
warn!(context, "Cannot mark {} as \"Deleted\" after copy.", uid);
emit_event!(
context,
EventType::ImapMessageMoved(format!(
"IMAP Message {} copied to {} (delete FAILED)",
display_folder_id, dest_folder
))
);
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP Message {} copied to {} (delete FAILED)",
display_folder_id, dest_folder
)));
ImapActionResult::Failed
} else {
self.config.selected_folder_needs_expunge = true;
emit_event!(
context,
EventType::ImapMessageMoved(format!(
"IMAP Message {} copied to {} (delete successfull)",
display_folder_id, dest_folder
))
);
context.emit_event(EventType::ImapMessageMoved(format!(
"IMAP Message {} copied to {} (delete successfull)",
display_folder_id, dest_folder
)));
ImapActionResult::Success
}
}
@@ -1128,7 +1154,7 @@ impl Imap {
if uid == 0 {
return Some(ImapActionResult::RetryLater);
}
if !self.is_connected() {
if self.session.is_none() {
// currently jobs are only performed on the INBOX thread
// TODO: make INBOX/SENT/MVBOX perform the jobs on their
// respective folders to avoid select_folder network traffic
@@ -1265,13 +1291,10 @@ impl Imap {
);
ImapActionResult::RetryLater
} else {
emit_event!(
context,
EventType::ImapMessageDeleted(format!(
"IMAP Message {} marked as deleted [{}]",
display_imap_id, message_id
))
);
context.emit_event(EventType::ImapMessageDeleted(format!(
"IMAP Message {} marked as deleted [{}]",
display_imap_id, message_id
)));
self.config.selected_folder_needs_expunge = true;
ImapActionResult::Success
}
@@ -1291,115 +1314,114 @@ impl Imap {
}
pub async fn configure_folders(&mut self, context: &Context, create_mvbox: bool) -> Result<()> {
if !self.is_connected() {
bail!("IMAP No Connection established");
}
let session = match self.session {
Some(ref mut session) => session,
None => bail!("no IMAP connection established"),
};
if let Some(ref mut session) = &mut self.session {
let mut folders = match session.list(Some(""), Some("*")).await {
Ok(f) => f,
Err(err) => {
bail!("list_folders failed: {}", err);
}
};
let mut folders = match session.list(Some(""), Some("*")).await {
Ok(f) => f,
Err(err) => {
bail!("list_folders failed: {}", err);
}
};
let mut delimiter = ".".to_string();
let mut delimiter_is_default = true;
let mut mvbox_folder = None;
let mut folder_configs = BTreeMap::new();
let mut fallback_folder = get_fallback_folder(&delimiter);
let mut delimiter = ".".to_string();
let mut delimiter_is_default = true;
let mut mvbox_folder = None;
let mut folder_configs = BTreeMap::new();
let mut fallback_folder = get_fallback_folder(&delimiter);
while let Some(folder) = folders.next().await {
let folder = folder?;
info!(context, "Scanning folder: {:?}", folder);
while let Some(folder) = folders.next().await {
let folder = folder?;
info!(context, "Scanning folder: {:?}", folder);
// Update the delimiter iff there is a different one, but only once.
if let Some(d) = folder.delimiter() {
if delimiter_is_default && !d.is_empty() && delimiter != d {
delimiter = d.to_string();
fallback_folder = get_fallback_folder(&delimiter);
delimiter_is_default = false;
}
}
let folder_meaning = get_folder_meaning(&folder);
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
if folder.name() == "DeltaChat" {
// Always takes precedence
mvbox_folder = Some(folder.name().to_string());
} else if folder.name() == fallback_folder {
// only set if none has been already set
if mvbox_folder.is_none() {
mvbox_folder = Some(folder.name().to_string());
}
} else if let Some(config) = folder_meaning.to_config() {
// Always takes precedence
folder_configs.insert(config, folder.name().to_string());
} else if let Some(config) = folder_name_meaning.to_config() {
// only set if none has been already set
folder_configs
.entry(config)
.or_insert_with(|| folder.name().to_string());
// Update the delimiter iff there is a different one, but only once.
if let Some(d) = folder.delimiter() {
if delimiter_is_default && !d.is_empty() && delimiter != d {
delimiter = d.to_string();
fallback_folder = get_fallback_folder(&delimiter);
delimiter_is_default = false;
}
}
drop(folders);
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
let folder_meaning = get_folder_meaning(&folder);
let folder_name_meaning = get_folder_meaning_by_name(folder.name());
if folder.name() == "DeltaChat" {
// Always takes precedence
mvbox_folder = Some(folder.name().to_string());
} else if folder.name() == fallback_folder {
// only set if none has been already set
if mvbox_folder.is_none() {
mvbox_folder = Some(folder.name().to_string());
}
} else if let Some(config) = folder_meaning.to_config() {
// Always takes precedence
folder_configs.insert(config, folder.name().to_string());
} else if let Some(config) = folder_name_meaning.to_config() {
// only set if none has been already set
folder_configs
.entry(config)
.or_insert_with(|| folder.name().to_string());
}
}
drop(folders);
if mvbox_folder.is_none() && create_mvbox {
info!(context, "Creating MVBOX-folder \"DeltaChat\"...",);
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
match session.create("DeltaChat").await {
Ok(_) => {
mvbox_folder = Some("DeltaChat".into());
info!(context, "MVBOX-folder created.",);
}
Err(err) => {
warn!(
context,
"Cannot create MVBOX-folder, trying to create INBOX subfolder. ({})",
err
);
if mvbox_folder.is_none() && create_mvbox {
info!(context, "Creating MVBOX-folder \"DeltaChat\"...",);
match session.create(&fallback_folder).await {
Ok(_) => {
mvbox_folder = Some(fallback_folder);
info!(
context,
"MVBOX-folder created as INBOX subfolder. ({})", err
);
}
Err(err) => {
warn!(context, "Cannot create MVBOX-folder. ({})", err);
}
match session.create("DeltaChat").await {
Ok(_) => {
mvbox_folder = Some("DeltaChat".into());
info!(context, "MVBOX-folder created.",);
}
Err(err) => {
warn!(
context,
"Cannot create MVBOX-folder, trying to create INBOX subfolder. ({})", err
);
match session.create(&fallback_folder).await {
Ok(_) => {
mvbox_folder = Some(fallback_folder);
info!(
context,
"MVBOX-folder created as INBOX subfolder. ({})", err
);
}
Err(err) => {
warn!(context, "Cannot create MVBOX-folder. ({})", err);
}
}
}
// SUBSCRIBE is needed to make the folder visible to the LSUB command
// that may be used by other MUAs to list folders.
// for the LIST command, the folder is always visible.
if let Some(ref mvbox) = mvbox_folder {
if let Err(err) = session.subscribe(mvbox).await {
warn!(context, "could not subscribe to {:?}: {:?}", mvbox, err);
}
}
// SUBSCRIBE is needed to make the folder visible to the LSUB command
// that may be used by other MUAs to list folders.
// for the LIST command, the folder is always visible.
if let Some(ref mvbox) = mvbox_folder {
if let Err(err) = session.subscribe(mvbox).await {
warn!(context, "could not subscribe to {:?}: {:?}", mvbox, err);
}
}
}
context
.set_config(Config::ConfiguredInboxFolder, Some("INBOX"))
.await?;
if let Some(ref mvbox_folder) = mvbox_folder {
context
.set_config(Config::ConfiguredInboxFolder, Some("INBOX"))
.await?;
if let Some(ref mvbox_folder) = mvbox_folder {
context
.set_config(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
.await?;
}
for (config, name) in folder_configs {
context.set_config(config, Some(&name)).await?;
}
context
.sql
.set_raw_config_int("folders_configured", DC_FOLDERS_CONFIGURED_VERSION)
.set_config(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
.await?;
}
for (config, name) in folder_configs {
context.set_config(config, Some(&name)).await?;
}
context
.sql
.set_raw_config_int("folders_configured", DC_FOLDERS_CONFIGURED_VERSION)
.await?;
info!(context, "FINISHED configuring IMAP-folders.");
Ok(())
}
@@ -1587,13 +1609,13 @@ async fn precheck_imf(
context,
job::Job::new(Action::MoveMsg, msg_id.to_u32(), Params::new(), 0),
)
.await;
.await?;
} else {
job::add(
context,
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
)
.await;
.await?;
}
}
} else if old_server_folder != server_folder {
@@ -1636,7 +1658,7 @@ async fn precheck_imf(
context,
job::Job::new(Action::MarkseenMsgOnImap, msg_id.to_u32(), Params::new(), 0),
)
.await;
.await?;
}
}
}

View File

@@ -15,6 +15,9 @@ pub enum Error {
#[error("IMAP Folder name invalid: {0}")]
BadFolderName(String),
#[error("IMAP folder does not exist: {0}")]
NoFolder(String),
#[error("IMAP close/expunge failed")]
CloseExpungeFailed(#[from] async_imap::error::Error),
@@ -110,6 +113,9 @@ impl Imap {
Err(async_imap::error::Error::Validate(_)) => {
Err(Error::BadFolderName(folder.to_string()))
}
Err(async_imap::error::Error::No(_)) => {
Err(Error::NoFolder(folder.to_string()))
}
Err(err) => {
self.config.selected_folder = None;
self.trigger_reconnect(context).await;
@@ -123,6 +129,28 @@ impl Imap {
Ok(NewlySelected::No)
}
}
/// Selects a folder. Tries to create it once and select again if the folder does not exist.
pub(super) async fn select_or_create_folder(
&mut self,
context: &Context,
folder: &str,
) -> Result<NewlySelected> {
match self.select_folder(context, Some(folder)).await {
Ok(newly_selected) => Ok(newly_selected),
Err(err) => match err {
Error::NoFolder(_) => {
if let Some(ref mut session) = self.session {
session.create(folder).await?;
} else {
return Err(Error::NoSession);
}
self.select_folder(context, Some(folder)).await
}
_ => Err(err),
},
}
}
}
#[derive(PartialEq, Debug, Copy, Clone, Eq)]
pub(super) enum NewlySelected {

View File

@@ -234,7 +234,7 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
msg = Message::default();
msg.viewtype = Viewtype::File;
msg.param.set(Param::File, setup_file_blob.as_name());
msg.subject = stock_str::ac_setup_msg_subject(context).await;
msg.param
.set(Param::MimeType, "application/autocrypt-setup");
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
@@ -759,7 +759,7 @@ async fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> Result<(
let progress = 1000 * written_files / count;
if progress != last_progress && progress > 10 && progress < 1000 {
// We already emitted ImexProgress(10) above
emit_event!(context, EventType::ImexProgress(progress));
context.emit_event(EventType::ImexProgress(progress));
last_progress = progress;
}
}

View File

@@ -2,12 +2,11 @@
//!
//! This module implements a job queue maintained in the SQLite database
//! and job types.
use std::fmt;
use std::future::Future;
use std::{fmt, time::Duration};
use anyhow::{bail, ensure, format_err, Context as _, Error, Result};
use async_smtp::smtp::response::{Category, Code, Detail};
use async_std::task::sleep;
use deltachat_derive::{FromSql, ToSql};
use itertools::Itertools;
use rand::{thread_rng, Rng};
@@ -103,6 +102,12 @@ pub enum Action {
MoveMsg = 200,
DeleteMsgOnImap = 210,
// This job will download partially downloaded messages completely
// and is added when download_full() is called.
// Most messages are downloaded automatically on fetch
// and do not go through this job.
DownloadMsg = 250,
// UID synchronization is high-priority to make sure correct UIDs
// are used by message moving/deletion.
ResyncFolders = 300,
@@ -134,6 +139,7 @@ impl From<Action> for Thread {
MarkseenMsgOnImap => Thread::Imap,
MoveMsg => Thread::Imap,
UpdateRecentQuota => Thread::Imap,
DownloadMsg => Thread::Imap,
MaybeSendLocations => Thread::Smtp,
MaybeSendLocationsEnded => Thread::Smtp,
@@ -709,7 +715,7 @@ impl Job {
Config::ConfiguredSentboxFolder,
] {
if let Some(folder) = job_try!(context.get_config(*config).await) {
if let Err(e) = imap.fetch_new_messages(context, folder, true).await {
if let Err(e) = imap.fetch_new_messages(context, &folder, true).await {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
warn!(context, "Could not fetch messages, retrying: {:#}", e);
return Status::RetryLater;
@@ -815,12 +821,12 @@ impl Job {
}
/// Delete all pending jobs with the given action.
pub async fn kill_action(context: &Context, action: Action) -> bool {
pub async fn kill_action(context: &Context, action: Action) -> Result<()> {
context
.sql
.execute("DELETE FROM jobs WHERE action=?;", paramsv![action])
.await
.is_ok()
.await?;
Ok(())
}
/// Remove jobs with specified IDs.
@@ -836,15 +842,15 @@ async fn kill_ids(context: &Context, job_ids: &[u32]) -> Result<()> {
Ok(())
}
pub async fn action_exists(context: &Context, action: Action) -> bool {
context
pub async fn action_exists(context: &Context, action: Action) -> Result<bool> {
let exists = context
.sql
.exists(
"SELECT COUNT(*) FROM jobs WHERE action=?;",
paramsv![action],
)
.await
.unwrap_or_default()
.await?;
Ok(exists)
}
async fn set_delivered(context: &Context, msg_id: MsgId) -> Result<()> {
@@ -1078,7 +1084,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
"{} thread increases job {} tries to {}", &connection, job, tries
);
job.tries = tries;
let time_offset = get_backoff_time_offset(tries);
let time_offset = get_backoff_time_offset(tries, job.action);
job.desired_timestamp = time() + time_offset;
info!(
context,
@@ -1156,6 +1162,7 @@ async fn perform_job_action(
Ok(status) => status,
Err(err) => Status::Finished(Err(err)),
},
Action::DownloadMsg => job.download_msg(context, connection.inbox()).await,
};
info!(context, "Finished immediate try {} of job {}", tries, job);
@@ -1163,33 +1170,43 @@ async fn perform_job_action(
try_res
}
fn get_backoff_time_offset(tries: u32) -> i64 {
let n = 2_i32.pow(tries - 1) * 60;
let mut rng = thread_rng();
let r: i32 = rng.gen();
let mut seconds = r % (n + 1);
if seconds < 1 {
seconds = 1;
fn get_backoff_time_offset(tries: u32, action: Action) -> i64 {
match action {
// Just try every 10s to update the quota
// If all retries are exhausted, a new job will be created when the quota information is needed
Action::UpdateRecentQuota => 10,
_ => {
// Exponential backoff
let n = 2_i32.pow(tries - 1) * 60;
let mut rng = thread_rng();
let r: i32 = rng.gen();
let mut seconds = r % (n + 1);
if seconds < 1 {
seconds = 1;
}
seconds as i64
}
}
seconds as i64
}
async fn send_mdn(context: &Context, msg: &Message) -> Result<()> {
let mut param = Params::new();
param.set(Param::MsgId, msg.id.to_u32().to_string());
add(context, Job::new(Action::SendMdn, msg.from_id, param, 0)).await;
add(context, Job::new(Action::SendMdn, msg.from_id, param, 0)).await?;
Ok(())
}
pub(crate) async fn schedule_resync(context: &Context) {
kill_action(context, Action::ResyncFolders).await;
pub(crate) async fn schedule_resync(context: &Context) -> Result<()> {
kill_action(context, Action::ResyncFolders).await?;
add(
context,
Job::new(Action::ResyncFolders, 0, Params::new(), 0),
)
.await;
.await?;
Ok(())
}
/// Creates a job.
@@ -1203,12 +1220,10 @@ pub fn create(action: Action, foreign_id: u32, param: Params, delay_seconds: i64
}
/// Adds a job to the database, scheduling it.
pub async fn add(context: &Context, job: Job) {
pub async fn add(context: &Context, job: Job) -> Result<()> {
let action = job.action;
let delay_seconds = job.delay_seconds();
job.save(context).await.unwrap_or_else(|err| {
error!(context, "failed to save job: {}", err);
});
job.save(context).await.context("failed to save job")?;
if delay_seconds == 0 {
match action {
@@ -1219,7 +1234,8 @@ pub async fn add(context: &Context, job: Job) {
| Action::MarkseenMsgOnImap
| Action::FetchExistingMsgs
| Action::MoveMsg
| Action::UpdateRecentQuota => {
| Action::UpdateRecentQuota
| Action::DownloadMsg => {
info!(context, "interrupt: imap");
context
.interrupt_inbox(InterruptInfo::new(false, None))
@@ -1236,23 +1252,18 @@ pub async fn add(context: &Context, job: Job) {
}
}
}
Ok(())
}
async fn load_housekeeping_job(context: &Context) -> Option<Job> {
let last_time = match context.get_config_i64(Config::LastHousekeeping).await {
Ok(last_time) => last_time,
Err(err) => {
warn!(context, "failed to load housekeeping config: {:?}", err);
return None;
}
};
async fn load_housekeeping_job(context: &Context) -> Result<Option<Job>> {
let last_time = context.get_config_i64(Config::LastHousekeeping).await?;
let next_time = last_time + (60 * 60 * 24);
if next_time <= time() {
kill_action(context, Action::Housekeeping).await;
Some(Job::new(Action::Housekeeping, 0, Params::new(), 0))
kill_action(context, Action::Housekeeping).await?;
Ok(Some(Job::new(Action::Housekeeping, 0, Params::new(), 0)))
} else {
None
Ok(None)
}
}
@@ -1266,20 +1277,9 @@ pub(crate) async fn load_next(
context: &Context,
thread: Thread,
info: &InterruptInfo,
) -> Option<Job> {
) -> Result<Option<Job>> {
info!(context, "loading job for {}-thread", thread);
while !context.sql.is_open().await {
// The db is closed, which means that this thread should not be running.
// Wait until the db is re-opened (if we returned None, this thread might do further damage)
warn!(
context,
"{}: load_next() was called but the db was not opened, THIS SHOULD NOT HAPPEN. Waiting...",
thread
);
sleep(Duration::from_millis(500)).await;
}
let query;
let params;
let t = time();
@@ -1346,51 +1346,38 @@ LIMIT 1;
info!(context, "cleaning up job, because of {}", err);
// TODO: improve by only doing a single query
match context
let id = context
.sql
.query_row(query, params.clone(), |row| row.get::<_, i32>(0))
.await
{
Ok(id) => {
if let Err(err) = context
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
.await
{
warn!(context, "failed to delete job {}: {:?}", id, err);
}
}
Err(err) => {
error!(context, "failed to retrieve invalid job from DB: {}", err);
break None;
}
}
.context("Failed to retrieve invalid job ID from the database")?;
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
.await
.with_context(|| format!("Failed to delete invalid job {}", id))?;
}
}
};
match thread {
Thread::Unknown => {
error!(context, "unknown thread for job");
None
bail!("unknown thread for job")
}
Thread::Imap => {
if let Some(job) = job {
if job.action < Action::DeleteMsgOnImap {
load_imap_deletion_job(context)
.await
.unwrap_or_default()
.or(Some(job))
Ok(load_imap_deletion_job(context).await?.or(Some(job)))
} else {
Some(job)
Ok(Some(job))
}
} else if let Some(job) = load_imap_deletion_job(context).await.unwrap_or_default() {
Some(job)
} else if let Some(job) = load_imap_deletion_job(context).await? {
Ok(Some(job))
} else {
load_housekeeping_job(context).await
Ok(load_housekeeping_job(context).await?)
}
}
Thread::Smtp => job,
Thread::Smtp => Ok(job),
}
}
@@ -1422,7 +1409,7 @@ mod tests {
}
#[async_std::test]
async fn test_load_next_job_two() {
async fn test_load_next_job_two() -> Result<()> {
// We want to ensure that loading jobs skips over jobs which
// fails to load from the database instead of failing to load
// all jobs.
@@ -1433,7 +1420,7 @@ mod tests {
Thread::from(Action::MoveMsg),
&InterruptInfo::new(false, None),
)
.await;
.await?;
// The housekeeping job should be loaded as we didn't run housekeeping in the last day:
assert_eq!(jobs.unwrap().action, Action::Housekeeping);
@@ -1443,12 +1430,13 @@ mod tests {
Thread::from(Action::MoveMsg),
&InterruptInfo::new(false, None),
)
.await;
.await?;
assert!(jobs.is_some());
Ok(())
}
#[async_std::test]
async fn test_load_next_job_one() {
async fn test_load_next_job_one() -> Result<()> {
let t = TestContext::new().await;
insert_job(&t, 1, true).await;
@@ -1458,7 +1446,8 @@ mod tests {
Thread::from(Action::MoveMsg),
&InterruptInfo::new(false, None),
)
.await;
.await?;
assert!(jobs.is_some());
Ok(())
}
}

View File

@@ -55,6 +55,7 @@ mod configure;
pub mod constants;
pub mod contact;
pub mod context;
pub mod download;
mod e2ee;
pub mod ephemeral;
mod imap;
@@ -67,7 +68,6 @@ pub mod key;
mod keyring;
pub mod location;
mod login_param;
pub mod lot;
pub mod message;
mod mimefactory;
pub mod mimeparser;
@@ -83,11 +83,13 @@ mod simplify;
mod smtp;
pub mod stock_str;
mod token;
mod update_helper;
#[macro_use]
mod dehtml;
mod color;
pub mod html;
pub mod plaintext;
pub mod summary;
pub mod dc_receive_imf;
pub mod dc_tools;

View File

@@ -1,7 +1,7 @@
//! Location handling.
use std::convert::TryFrom;
use anyhow::{ensure, Error};
use anyhow::{ensure, Result};
use bitflags::bitflags;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
@@ -63,7 +63,7 @@ impl Kml {
Default::default()
}
pub fn parse(context: &Context, to_parse: &[u8]) -> Result<Self, Error> {
pub fn parse(context: &Context, to_parse: &[u8]) -> Result<Self> {
ensure!(to_parse.len() <= 1024 * 1024, "kml-file is too large");
let mut reader = quick_xml::Reader::from_reader(to_parse);
@@ -191,89 +191,97 @@ impl Kml {
}
/// Enables location streaming in chat identified by `chat_id` for `seconds` seconds.
pub async 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,
) -> Result<()> {
ensure!(seconds >= 0);
ensure!(!chat_id.is_special());
let now = time();
if !(seconds < 0 || chat_id.is_special()) {
let is_sending_locations_before =
is_sending_locations_to_chat(context, Some(chat_id)).await;
if context
.sql
.execute(
"UPDATE chats \
SET locations_send_begin=?, \
locations_send_until=? \
WHERE id=?",
paramsv![
if 0 != seconds { now } else { 0 },
if 0 != seconds { now + seconds } else { 0 },
chat_id,
],
)
let is_sending_locations_before = is_sending_locations_to_chat(context, Some(chat_id)).await?;
context
.sql
.execute(
"UPDATE chats \
SET locations_send_begin=?, \
locations_send_until=? \
WHERE id=?",
paramsv![
if 0 != seconds { now } else { 0 },
if 0 != seconds { now + seconds } else { 0 },
chat_id,
],
)
.await?;
if 0 != seconds && !is_sending_locations_before {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_str::msg_location_enabled(context).await);
msg.param.set_cmd(SystemMessage::LocationStreamingEnabled);
chat::send_msg(context, chat_id, &mut msg)
.await
.is_ok()
{
if 0 != seconds && !is_sending_locations_before {
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_str::msg_location_enabled(context).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 = stock_str::msg_location_disabled(context).await;
chat::add_info_msg(context, chat_id, stock_str, now).await;
}
context.emit_event(EventType::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;
}
}
.unwrap_or_default();
} else if 0 == seconds && is_sending_locations_before {
let stock_str = stock_str::msg_location_disabled(context).await;
chat::add_info_msg(context, chat_id, stock_str, now).await?;
}
context.emit_event(EventType::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?;
}
Ok(())
}
async fn schedule_maybe_send_locations(context: &Context, force_schedule: bool) {
if force_schedule || !job::action_exists(context, job::Action::MaybeSendLocations).await {
async fn schedule_maybe_send_locations(context: &Context, force_schedule: bool) -> Result<()> {
if force_schedule || !job::action_exists(context, job::Action::MaybeSendLocations).await? {
job::add(
context,
job::Job::new(job::Action::MaybeSendLocations, 0, Params::new(), 60),
)
.await;
.await?;
};
Ok(())
}
/// Returns whether `chat_id` or any chat is sending locations.
///
/// If `chat_id` is `Some` only that chat is checked, otherwise returns `true` if any chat
/// is sending locations.
pub async fn is_sending_locations_to_chat(context: &Context, chat_id: Option<ChatId>) -> bool {
match chat_id {
Some(chat_id) => context
.sql
.exists(
"SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;",
paramsv![chat_id, time()],
)
.await
.unwrap_or_default(),
None => context
.sql
.exists(
"SELECT COUNT(id) FROM chats WHERE locations_send_until>?;",
paramsv![time()],
)
.await
.unwrap_or_default(),
}
pub async fn is_sending_locations_to_chat(
context: &Context,
chat_id: Option<ChatId>,
) -> Result<bool> {
let exists = match chat_id {
Some(chat_id) => {
context
.sql
.exists(
"SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;",
paramsv![chat_id, time()],
)
.await?
}
None => {
context
.sql
.exists(
"SELECT COUNT(id) FROM chats WHERE locations_send_until>?;",
paramsv![time()],
)
.await?
}
};
Ok(exists)
}
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
@@ -288,7 +296,11 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
"SELECT id FROM chats WHERE locations_send_until>?;",
paramsv![time()],
|row| row.get::<_, i32>(0),
|chats| chats.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|chats| {
chats
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await
{
@@ -313,7 +325,7 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
if continue_streaming {
context.emit_event(EventType::LocationChanged(Some(DC_CONTACT_ID_SELF)));
};
schedule_maybe_send_locations(context, false).await;
schedule_maybe_send_locations(context, false).await.ok();
}
continue_streaming
@@ -325,7 +337,7 @@ pub async fn get_range(
contact_id: Option<u32>,
timestamp_from: i64,
mut timestamp_to: i64,
) -> Result<Vec<Location>, Error> {
) -> Result<Vec<Location>> {
if timestamp_to == 0 {
timestamp_to = time() + 10;
}
@@ -400,7 +412,7 @@ fn is_marker(txt: &str) -> bool {
}
/// Deletes all locations from the database.
pub async fn delete_all(context: &Context) -> Result<(), Error> {
pub async fn delete_all(context: &Context) -> Result<()> {
context
.sql
.execute("DELETE FROM locations;", paramsv![])
@@ -409,7 +421,7 @@ pub async fn delete_all(context: &Context) -> Result<(), Error> {
Ok(())
}
pub async 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)> {
let mut last_added_location_id = 0;
let self_addr = context
@@ -517,7 +529,7 @@ pub async fn set_kml_sent_timestamp(
context: &Context,
chat_id: ChatId,
timestamp: i64,
) -> Result<(), Error> {
) -> Result<()> {
context
.sql
.execute(
@@ -528,11 +540,7 @@ pub async fn set_kml_sent_timestamp(
Ok(())
}
pub async fn set_msg_location_id(
context: &Context,
msg_id: MsgId,
location_id: u32,
) -> Result<(), Error> {
pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id: u32) -> Result<()> {
context
.sql
.execute(
@@ -550,7 +558,7 @@ pub async fn save(
contact_id: u32,
locations: &[Location],
independent: bool,
) -> Result<u32, Error> {
) -> Result<u32> {
ensure!(!chat_id.is_special(), "Invalid chat id");
let mut newest_timestamp = 0;
@@ -630,7 +638,7 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
},
|rows| {
rows.filter_map(|v| v.transpose())
.collect::<Result<Vec<_>, _>>()
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
@@ -690,7 +698,7 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j
}
if continue_streaming {
schedule_maybe_send_locations(context, true).await;
job_try!(schedule_maybe_send_locations(context, true).await);
}
job::Status::Finished(Ok(()))
}
@@ -736,7 +744,7 @@ pub(crate) async fn job_maybe_send_locations_ended(
);
let stock_str = stock_str::msg_location_disabled(context).await;
chat::add_info_msg(context, chat_id, stock_str, now).await;
job_try!(chat::add_info_msg(context, chat_id, stock_str, now).await);
context.emit_event(EventType::ChatModified(chat_id));
}
}

View File

@@ -13,7 +13,7 @@ macro_rules! info {
file = file!(),
line = line!(),
msg = &formatted);
emit_event!($ctx, $crate::EventType::Info(full));
$ctx.emit_event($crate::EventType::Info(full));
}};
}
@@ -28,7 +28,7 @@ macro_rules! warn {
file = file!(),
line = line!(),
msg = &formatted);
emit_event!($ctx, $crate::EventType::Warning(full));
$ctx.emit_event($crate::EventType::Warning(full));
}};
}
@@ -39,17 +39,10 @@ macro_rules! error {
};
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
let formatted = format!($msg, $($args),*);
emit_event!($ctx, $crate::EventType::Error(formatted));
$ctx.emit_event($crate::EventType::Error(formatted));
}};
}
#[macro_export]
macro_rules! emit_event {
($ctx:expr, $event:expr) => {
$ctx.emit_event($event);
};
}
pub trait LogExt<T, E>
where
Self: std::marker::Sized,
@@ -136,7 +129,7 @@ impl<T, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
);
// We can't use the warn!() macro here as the file!() and line!() macros
// don't work with #[track_caller]
emit_event!(context, crate::EventType::Warning(full));
context.emit_event(crate::EventType::Warning(full));
};
self
}

View File

@@ -1,141 +0,0 @@
//! # Legacy generic return values for C API.
use deltachat_derive::{FromSql, ToSql};
use crate::key::Fingerprint;
/// An object containing a set of values.
/// The meaning of the values is defined by the function returning the object.
/// Lot objects are created
/// eg. by chatlist.get_summary() or dc_msg_get_summary().
///
/// *Lot* is used in the meaning *heap* here.
#[derive(Default, Debug, Clone)]
pub struct Lot {
pub(crate) text1_meaning: Meaning,
pub(crate) text1: Option<String>,
pub(crate) text2: Option<String>,
pub(crate) timestamp: i64,
pub(crate) state: LotState,
pub(crate) id: u32,
pub(crate) fingerprint: Option<Fingerprint>,
pub(crate) invitenumber: Option<String>,
pub(crate) auth: Option<String>,
}
#[repr(u8)]
#[derive(
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
)]
pub enum Meaning {
None = 0,
Text1Draft = 1,
Text1Username = 2,
Text1Self = 3,
}
impl Default for Meaning {
fn default() -> Self {
Meaning::None
}
}
impl Lot {
pub fn new() -> Self {
Default::default()
}
pub fn get_text1(&self) -> Option<&str> {
self.text1.as_deref()
}
pub fn get_text2(&self) -> Option<&str> {
self.text2.as_deref()
}
pub fn get_text1_meaning(&self) -> Meaning {
self.text1_meaning
}
pub fn get_state(&self) -> LotState {
self.state
}
pub fn get_id(&self) -> u32 {
self.id
}
pub fn get_timestamp(&self) -> i64 {
self.timestamp
}
}
#[repr(u32)]
#[derive(
Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
)]
pub enum LotState {
// Default
Undefined = 0,
// Qr States
/// id=contact
QrAskVerifyContact = 200,
/// text1=groupname
QrAskVerifyGroup = 202,
/// id=contact
QrFprOk = 210,
/// id=contact
QrFprMismatch = 220,
/// test1=formatted fingerprint
QrFprWithoutAddr = 230,
/// text1=domain
QrAccount = 250,
/// text1=domain, text2=instance pattern
QrWebrtcInstance = 260,
/// id=contact
QrAddr = 320,
/// text1=text
QrText = 330,
/// text1=URL
QrUrl = 332,
/// text1=error string
QrError = 400,
QrWithdrawVerifyContact = 500,
/// text1=groupname
QrWithdrawVerifyGroup = 502,
QrReviveVerifyContact = 510,
/// text1=groupname
QrReviveVerifyGroup = 512,
// Message States
MsgInFresh = 10,
MsgInNoticed = 13,
MsgInSeen = 16,
MsgOutPreparing = 18,
MsgOutDraft = 19,
MsgOutPending = 20,
MsgOutFailed = 24,
MsgOutDelivered = 26,
MsgOutMdnRcvd = 28,
}
impl Default for LotState {
fn default() -> Self {
LotState::Undefined
}
}

View File

@@ -3,10 +3,9 @@
use std::collections::BTreeMap;
use std::convert::TryInto;
use anyhow::{ensure, format_err, Result};
use anyhow::{ensure, format_err, Context as _, Result};
use async_std::path::{Path, PathBuf};
use deltachat_derive::{FromSql, ToSql};
use itertools::Itertools;
use rusqlite::types::ValueRef;
use serde::{Deserialize, Serialize};
@@ -22,19 +21,16 @@ use crate::dc_tools::{
dc_create_smeared_timestamp, dc_get_filebytes, dc_get_filemeta, dc_gm2local_offset,
dc_read_file, dc_timestamp_to_str, dc_truncate, time,
};
use crate::download::DownloadState;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::events::EventType;
use crate::job::{self, Action};
use crate::log::LogExt;
use crate::lot::{Lot, LotState, Meaning};
use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage};
use crate::param::{Param, Params};
use crate::pgp::split_armored_data;
use crate::stock_str;
// In practice, the user additionally cuts the string themselves
// pixel-accurate.
const SUMMARY_CHARACTERS: usize = 160;
use crate::summary::Summary;
/// Message ID, including reserved IDs.
///
@@ -301,6 +297,7 @@ pub struct Message {
pub(crate) chat_id: ChatId,
pub(crate) viewtype: Viewtype,
pub(crate) state: MessageState,
pub(crate) download_state: DownloadState,
pub(crate) hidden: bool,
pub(crate) timestamp_sort: i64,
pub(crate) timestamp_sent: i64,
@@ -355,6 +352,7 @@ impl Message {
" m.ephemeral_timestamp AS ephemeral_timestamp,",
" m.type AS type,",
" m.state AS state,",
" m.download_state AS download_state,",
" m.error AS error,",
" m.msgrmsg AS msgrmsg,",
" m.mime_modified AS mime_modified,",
@@ -406,6 +404,7 @@ impl Message {
ephemeral_timestamp: row.get("ephemeral_timestamp")?,
viewtype: row.get("type")?,
state: row.get("state")?,
download_state: row.get("download_state")?,
error: Some(row.get::<_, String>("error")?)
.filter(|error| !error.is_empty()),
is_dc_message: row.get("msgrmsg")?,
@@ -592,23 +591,21 @@ impl Message {
self.ephemeral_timestamp
}
pub async fn get_summary(&mut self, context: &Context, chat: Option<&Chat>) -> Lot {
let mut ret = Lot::new();
/// Returns message summary for display in the search results.
pub async fn get_summary(&self, context: &Context, chat: Option<&Chat>) -> Result<Summary> {
let chat_loaded: Chat;
let chat = if let Some(chat) = chat {
chat
} else if let Ok(chat) = Chat::load_from_db(context, self.chat_id).await {
} else {
let chat = Chat::load_from_db(context, self.chat_id).await?;
chat_loaded = chat;
&chat_loaded
} else {
return ret;
};
let contact = if self.from_id != DC_CONTACT_ID_SELF {
match chat.typ {
Chattype::Group | Chattype::Mailinglist => {
Contact::get_by_id(context, self.from_id).await.ok()
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
Some(Contact::get_by_id(context, self.from_id).await?)
}
Chattype::Single | Chattype::Undefined => None,
}
@@ -616,21 +613,7 @@ impl Message {
None
};
ret.fill(self, chat, contact.as_ref(), context).await;
ret
}
pub async fn get_summarytext(&self, context: &Context, approx_characters: usize) -> String {
get_summarytext_by_raw(
self.viewtype,
self.text.as_ref(),
self.is_forwarded(),
&self.param,
approx_characters,
context,
)
.await
Ok(Summary::new(context, self, chat, contact.as_ref()).await)
}
// It's a little unfortunate that the UI has to first call dc_msg_get_override_sender_name() and then if it was NULL, call
@@ -876,7 +859,11 @@ impl Message {
Param::Quote,
if text.is_empty() {
// Use summary, similar to "Image" to avoid sending empty quote.
quote.get_summarytext(context, 500).await
quote
.get_summary(context, None)
.await?
.truncated_text(500)
.to_string()
} else {
text
},
@@ -1029,24 +1016,6 @@ impl std::fmt::Display for MessageState {
}
}
impl From<MessageState> for LotState {
fn from(s: MessageState) -> Self {
use MessageState::*;
match s {
Undefined => LotState::Undefined,
InFresh => LotState::MsgInFresh,
InNoticed => LotState::MsgInNoticed,
InSeen => LotState::MsgInSeen,
OutPreparing => LotState::MsgOutPreparing,
OutDraft => LotState::MsgOutDraft,
OutPending => LotState::MsgOutPending,
OutFailed => LotState::MsgOutFailed,
OutDelivered => LotState::MsgOutDelivered,
OutMdnRcvd => LotState::MsgOutMdnRcvd,
}
}
}
impl MessageState {
pub fn can_fail(self) -> bool {
use MessageState::*;
@@ -1064,68 +1033,6 @@ impl MessageState {
}
}
impl Lot {
/* library-internal */
/* in practice, the user additionally cuts the string himself pixel-accurate */
pub async fn fill(
&mut self,
msg: &mut Message,
chat: &Chat,
contact: Option<&Contact>,
context: &Context,
) {
if msg.state == MessageState::OutDraft {
self.text1 = Some(stock_str::draft(context).await);
self.text1_meaning = Meaning::Text1Draft;
} else if msg.from_id == DC_CONTACT_ID_SELF {
if msg.is_info() || chat.is_self_talk() {
self.text1 = None;
self.text1_meaning = Meaning::None;
} else {
self.text1 = Some(stock_str::self_msg(context).await);
self.text1_meaning = Meaning::Text1Self;
}
} else {
match chat.typ {
Chattype::Group | Chattype::Mailinglist => {
if msg.is_info() || contact.is_none() {
self.text1 = None;
self.text1_meaning = Meaning::None;
} else {
self.text1 = msg
.get_override_sender_name()
.or_else(|| contact.map(|contact| msg.get_sender_name(contact)));
self.text1_meaning = Meaning::Text1Username;
}
}
Chattype::Single | Chattype::Undefined => {
self.text1 = None;
self.text1_meaning = Meaning::None;
}
}
}
let mut text2 = get_summarytext_by_raw(
msg.viewtype,
msg.text.as_ref(),
msg.is_forwarded(),
&msg.param,
SUMMARY_CHARACTERS,
context,
)
.await;
if text2.is_empty() && msg.quoted_text().is_some() {
text2 = stock_str::reply_noun(context).await
}
self.text2 = Some(text2);
self.timestamp = msg.get_timestamp();
self.state = msg.state.into();
}
}
pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
let msg = Message::load_from_db(context, msg_id).await?;
let rawtxt: Option<String> = context
@@ -1366,21 +1273,21 @@ pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8
Ok(headers)
}
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
for msg_id in msg_ids.iter() {
if let Ok(msg) = Message::load_from_db(context, *msg_id).await {
if msg.location_id > 0 {
delete_poi_location(context, msg.location_id).await;
}
}
if let Err(err) = msg_id.trash(context).await {
error!(context, "Unable to trash message {}: {}", msg_id, err);
let msg = Message::load_from_db(context, *msg_id).await?;
if msg.location_id > 0 {
delete_poi_location(context, msg.location_id).await;
}
msg_id
.trash(context)
.await
.with_context(|| format!("Unable to trash message {}", msg_id))?;
job::add(
context,
job::Job::new(Action::DeleteMsgOnImap, msg_id.to_u32(), Params::new(), 0),
)
.await;
.await?;
}
if !msg_ids.is_empty() {
@@ -1388,13 +1295,14 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) {
chat_id: ChatId::new(0),
msg_id: MsgId::new(0),
});
job::kill_action(context, Action::Housekeeping).await;
job::kill_action(context, Action::Housekeeping).await?;
job::add(
context,
job::Job::new(Action::Housekeeping, 0, Params::new(), 10),
)
.await;
.await?;
}
Ok(())
}
async fn delete_poi_location(context: &Context, location_id: u32) -> bool {
@@ -1467,7 +1375,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
context,
job::Job::new(Action::MarkseenMsgOnImap, id.to_u32(), Params::new(), 0),
)
.await;
.await?;
updated_chat_ids.insert(curr_chat_id, true);
}
}
@@ -1490,88 +1398,6 @@ pub async fn update_msg_state(context: &Context, msg_id: MsgId, state: MessageSt
.is_ok()
}
/// Returns a summary text.
pub async fn get_summarytext_by_raw(
viewtype: Viewtype,
text: Option<impl AsRef<str>>,
was_forwarded: bool,
param: &Params,
approx_characters: usize,
context: &Context,
) -> String {
let mut append_text = true;
let prefix = match viewtype {
Viewtype::Image => stock_str::image(context).await,
Viewtype::Gif => stock_str::gif(context).await,
Viewtype::Sticker => stock_str::sticker(context).await,
Viewtype::Video => stock_str::video(context).await,
Viewtype::Voice => stock_str::voice_message(context).await,
Viewtype::Audio | Viewtype::File => {
if param.get_cmd() == SystemMessage::AutocryptSetupMessage {
append_text = false;
stock_str::ac_setup_msg_subject(context).await
} else {
let file_name: String = param
.get_path(Param::File, context)
.unwrap_or(None)
.and_then(|path| {
path.file_name()
.map(|fname| fname.to_string_lossy().into_owned())
})
.unwrap_or_else(|| String::from("ErrFileName"));
let label = if viewtype == Viewtype::Audio {
stock_str::audio(context).await
} else {
stock_str::file(context).await
};
format!("{} {}", label, file_name)
}
}
Viewtype::VideochatInvitation => {
append_text = false;
stock_str::videochat_invitation(context).await
}
_ => {
if param.get_cmd() != SystemMessage::LocationOnly {
"".to_string()
} else {
append_text = false;
stock_str::location(context).await
}
}
};
if !append_text {
return prefix;
}
let summary_content = if let Some(text) = text {
if text.as_ref().is_empty() {
prefix
} else if prefix.is_empty() {
dc_truncate(text.as_ref(), approx_characters).to_string()
} else {
let tmp = format!("{} {}", prefix, text.as_ref());
dc_truncate(&tmp, approx_characters).to_string()
}
} else {
prefix
};
let summary = if was_forwarded {
let tmp = format!(
"{}: {}",
stock_str::forwarded(context).await,
summary_content
);
dc_truncate(&tmp, approx_characters).to_string()
} else {
summary_content
};
summary.split_whitespace().join(" ")
}
// as we do not cut inside words, this results in about 32-42 characters.
// Do not use too long subjects - we add a tag after the subject which gets truncated by the clients otherwise.
// It should also be very clear, the subject is _not_ the whole message.
@@ -1635,6 +1461,17 @@ pub async fn handle_mdn(
rfc724_mid: &str,
timestamp_sent: i64,
) -> Result<Option<(ChatId, MsgId)>> {
if from_id == DC_CONTACT_ID_SELF {
warn!(
context,
"ignoring MDN sent to self, this is a bug on the sender device"
);
// This is not an error on our side,
// we successfully ignored an invalid MDN and return `Ok`.
return Ok(None);
}
let res = context
.sql
.query_row_optional(
@@ -1642,7 +1479,6 @@ pub async fn handle_mdn(
"SELECT",
" m.id AS msg_id,",
" c.id AS chat_id,",
" c.type AS type,",
" m.state AS state",
" FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id",
" WHERE rfc724_mid=? AND from_id=1",
@@ -1653,14 +1489,13 @@ pub async fn handle_mdn(
Ok((
row.get::<_, MsgId>("msg_id")?,
row.get::<_, ChatId>("chat_id")?,
row.get::<_, Chattype>("type")?,
row.get::<_, MessageState>("state")?,
))
},
)
.await?;
let (msg_id, chat_id, chat_type, msg_state) = if let Some(res) = res {
let (msg_id, chat_id, msg_state) = if let Some(res) = res {
res
} else {
info!(
@@ -1671,63 +1506,28 @@ pub async fn handle_mdn(
return Ok(None);
};
let mut read_by_all = false;
if !context
.sql
.exists(
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=? AND contact_id=?;",
paramsv![msg_id, from_id as i32,],
)
.await?
{
context
.sql
.execute(
"INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);",
paramsv![msg_id, from_id as i32, timestamp_sent],
)
.await?;
}
if msg_state == MessageState::OutPreparing
|| msg_state == MessageState::OutPending
|| msg_state == MessageState::OutDelivered
{
let mdn_already_in_table = context
.sql
.exists(
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=? AND contact_id=?;",
paramsv![msg_id, from_id as i32,],
)
.await?;
if !mdn_already_in_table {
context
.sql
.execute(
"INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);",
paramsv![msg_id, from_id as i32, timestamp_sent],
)
.await?;
}
// Normal chat? that's quite easy.
if chat_type == Chattype::Single {
update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await;
read_by_all = true;
} else {
// send event about new state
let ist_cnt = context
.sql
.count(
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?;",
paramsv![msg_id],
)
.await?;
// Groupsize: Min. MDNs
// 1 S n/a
// 2 SR 1
// 3 SRR 2
// 4 SRRR 2
// 5 SRRRR 3
// 6 SRRRRR 3
//
// (S=Sender, R=Recipient)
// for rounding, SELF is already included!
let soll_cnt = (chat::get_chat_contact_cnt(context, chat_id).await? + 1) / 2;
if ist_cnt >= soll_cnt {
update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await;
read_by_all = true;
} // else wait for more receipts
}
}
if read_by_all {
update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await;
Ok(Some((chat_id, msg_id)))
} else {
Ok(None)
@@ -1791,7 +1591,7 @@ async fn ndn_maybe_add_info_msg(
chat_type: Chattype,
) -> Result<()> {
match chat_type {
Chattype::Group => {
Chattype::Group | Chattype::Broadcast => {
if let Some(failed_recipient) = &failed.failed_recipient {
let contact_id =
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown)
@@ -1809,7 +1609,7 @@ async fn ndn_maybe_add_info_msg(
text,
dc_create_smeared_timestamp(context).await,
)
.await;
.await?;
context.emit_event(EventType::ChatModified(chat_id));
}
}
@@ -2238,203 +2038,6 @@ mod tests {
assert!(chat::prepare_msg(ctx, chat.id, &mut msg).await.is_err());
}
#[async_std::test]
async fn test_get_summarytext_by_raw() {
let d = test::TestContext::new().await;
let ctx = &d.ctx;
let some_text = Some(" bla \t\n\tbla\n\t".to_string());
let empty_text = Some("".to_string());
let no_text: Option<String> = None;
let mut some_file = Params::new();
some_file.set(Param::File, "foo.bar");
assert_eq!(
get_summarytext_by_raw(
Viewtype::Text,
some_text.as_ref(),
false,
&Params::new(),
50,
ctx
)
.await,
"bla bla" // for simple text, the type is not added to the summary
);
assert_eq!(
get_summarytext_by_raw(
Viewtype::Image,
no_text.as_ref(),
false,
&some_file,
50,
ctx
)
.await,
"Image" // file names are not added for images
);
assert_eq!(
get_summarytext_by_raw(
Viewtype::Video,
no_text.as_ref(),
false,
&some_file,
50,
ctx
)
.await,
"Video" // file names are not added for videos
);
assert_eq!(
get_summarytext_by_raw(Viewtype::Gif, no_text.as_ref(), false, &some_file, 50, ctx,)
.await,
"GIF" // file names are not added for GIFs
);
assert_eq!(
get_summarytext_by_raw(
Viewtype::Sticker,
no_text.as_ref(),
false,
&some_file,
50,
ctx,
)
.await,
"Sticker" // file names are not added for stickers
);
assert_eq!(
get_summarytext_by_raw(
Viewtype::Voice,
empty_text.as_ref(),
false,
&some_file,
50,
ctx,
)
.await,
"Voice message" // file names are not added for voice messages, empty text is skipped
);
assert_eq!(
get_summarytext_by_raw(
Viewtype::Voice,
no_text.as_ref(),
false,
&some_file,
50,
ctx
)
.await,
"Voice message" // file names are not added for voice messages
);
assert_eq!(
get_summarytext_by_raw(
Viewtype::Voice,
some_text.as_ref(),
false,
&some_file,
50,
ctx
)
.await,
"Voice message \u{2013} bla bla" // `\u{2013}` explicitly checks for "EN DASH"
);
assert_eq!(
get_summarytext_by_raw(
Viewtype::Audio,
no_text.as_ref(),
false,
&some_file,
50,
ctx
)
.await,
"Audio \u{2013} foo.bar" // file name is added for audio
);
assert_eq!(
get_summarytext_by_raw(
Viewtype::Audio,
empty_text.as_ref(),
false,
&some_file,
50,
ctx,
)
.await,
"Audio \u{2013} foo.bar" // file name is added for audio, empty text is not added
);
assert_eq!(
get_summarytext_by_raw(
Viewtype::Audio,
some_text.as_ref(),
false,
&some_file,
50,
ctx
)
.await,
"Audio \u{2013} foo.bar \u{2013} bla bla" // file name and text added for audio
);
assert_eq!(
get_summarytext_by_raw(
Viewtype::File,
some_text.as_ref(),
false,
&some_file,
50,
ctx
)
.await,
"File \u{2013} foo.bar \u{2013} bla bla" // file name is added for files
);
// Forwarded
assert_eq!(
get_summarytext_by_raw(
Viewtype::Text,
some_text.as_ref(),
true,
&Params::new(),
50,
ctx
)
.await,
"Forwarded: bla bla" // for simple text, the type is not added to the summary
);
assert_eq!(
get_summarytext_by_raw(
Viewtype::File,
some_text.as_ref(),
true,
&some_file,
50,
ctx
)
.await,
"Forwarded: File \u{2013} foo.bar \u{2013} bla bla"
);
let mut asm_file = Params::new();
asm_file.set(Param::File, "foo.bar");
asm_file.set_cmd(SystemMessage::AutocryptSetupMessage);
assert_eq!(
get_summarytext_by_raw(Viewtype::File, no_text.as_ref(), false, &asm_file, 50, ctx)
.await,
"Autocrypt Setup Message" // file name is not added for autocrypt setup messages
);
}
#[async_std::test]
async fn test_parse_webrtc_instance() {
let (webrtc_type, url) = Message::parse_webrtc_instance("basicwebrtc:https://foo/bar");

View File

@@ -316,6 +316,10 @@ impl<'a> MimeFactory<'a> {
Loaded::Message { chat } => {
if chat.is_protected() {
false
} else if chat.typ == Chattype::Broadcast {
// encryption may disclose recipients;
// this is probably a worse issue than not opportunistically (!) encrypting
true
} else {
self.msg
.param
@@ -388,17 +392,11 @@ impl<'a> MimeFactory<'a> {
let subject = match self.loaded {
Loaded::Message { ref chat } => {
if self.msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
return Ok(stock_str::ac_setup_msg_subject(context).await);
}
if !self.msg.subject.is_empty() {
return Ok(self.msg.subject.clone());
}
if chat.typ == Chattype::Group && quoted_msg_subject.is_none_or_empty() {
// If we have a `quoted_msg_subject`, we use the subject of the quoted message
// instead of the group name
let re = if self.in_reply_to.is_empty() {
""
} else {
@@ -407,22 +405,22 @@ impl<'a> MimeFactory<'a> {
return Ok(format!("{}{}", re, chat.name));
}
let parent_subject = if quoted_msg_subject.is_none_or_empty() {
chat.param.get(Param::LastSubject)
} else {
quoted_msg_subject.as_deref()
};
if let Some(last_subject) = parent_subject {
format!("Re: {}", remove_subject_prefix(last_subject))
} else {
let self_name = match context.get_config(Config::Displayname).await? {
Some(name) => name,
None => context.get_config(Config::Addr).await?.unwrap_or_default(),
if chat.typ != Chattype::Broadcast {
let parent_subject = if quoted_msg_subject.is_none_or_empty() {
chat.param.get(Param::LastSubject)
} else {
quoted_msg_subject.as_deref()
};
stock_str::subject_for_new_contact(context, self_name).await
if let Some(last_subject) = parent_subject {
return Ok(format!("Re: {}", remove_subject_prefix(last_subject)));
}
}
let self_name = match context.get_config(Config::Displayname).await? {
Some(name) => name,
None => context.get_config(Config::Addr).await?.unwrap_or_default(),
};
stock_str::subject_for_new_contact(context, self_name).await
}
Loaded::Mdn { .. } => stock_str::read_rcpt(context).await,
};
@@ -559,9 +557,21 @@ impl<'a> MimeFactory<'a> {
render_rfc724_mid(&rfc724_mid),
));
headers
.unprotected
.push(Header::new_with_value("To".into(), to).unwrap());
let undisclosed_recipients = match &self.loaded {
Loaded::Message { chat } => chat.typ == Chattype::Broadcast,
Loaded::Mdn { .. } => false,
};
if undisclosed_recipients {
headers
.unprotected
.push(Header::new("To".into(), "hidden-recipients: ;".to_string()));
} else {
headers
.unprotected
.push(Header::new_with_value("To".into(), to).unwrap());
}
headers
.unprotected
.push(Header::new_with_value("From".into(), vec![from]).unwrap());
@@ -1084,7 +1094,7 @@ impl<'a> MimeFactory<'a> {
parts.push(msg_kml_part);
}
if location::is_sending_locations_to_chat(context, Some(self.msg.chat_id)).await {
if location::is_sending_locations_to_chat(context, Some(self.msg.chat_id)).await? {
match self.get_location_kml_part(context).await {
Ok(part) => parts.push(part),
Err(err) => {
@@ -1147,7 +1157,11 @@ impl<'a> MimeFactory<'a> {
{
stock_str::encrypted_msg(context).await
} else {
self.msg.get_summarytext(context, 32).await
self.msg
.get_summary(context, None)
.await?
.truncated_text(32)
.to_string()
};
let p2 = stock_str::read_rcpt_mail_body(context, p1).await;
let message_text = format!("{}\r\n", format_flowed(&p2));
@@ -1609,27 +1623,30 @@ mod tests {
}
#[async_std::test]
async fn test_subject_in_group() {
async fn test_subject_in_group() -> Result<()> {
async fn send_msg_get_subject(
t: &TestContext,
group_id: ChatId,
quote: Option<&Message>,
) -> String {
) -> Result<String> {
let mut new_msg = Message::new(Viewtype::Text);
new_msg.set_text(Some("Hi".to_string()));
if let Some(q) = quote {
new_msg.set_quote(t, q).await.unwrap();
new_msg.set_quote(t, q).await?;
}
let sent = t.send_msg(group_id, &mut new_msg).await;
get_subject(t, sent).await
}
async fn get_subject(t: &TestContext, sent: crate::test_utils::SentMessage) -> String {
async fn get_subject(
t: &TestContext,
sent: crate::test_utils::SentMessage,
) -> Result<String> {
let parsed_subject = t.parse_msg(&sent).await.get_subject().unwrap();
let sent_msg = Message::load_from_db(t, sent.sender_msg_id).await.unwrap();
let sent_msg = Message::load_from_db(t, sent.sender_msg_id).await?;
assert_eq!(parsed_subject, sent_msg.subject);
parsed_subject
Ok(parsed_subject)
}
// 6. Test that in a group, replies also take the quoted message's subject, while non-replies use the group title as subject
@@ -1638,13 +1655,13 @@ mod tests {
chat::create_group_chat(&t, chat::ProtectionStatus::Unprotected, "groupname") // TODO encodings, ä
.await
.unwrap();
let bob = Contact::create(&t, "", "bob@example.org").await.unwrap();
chat::add_contact_to_chat(&t, group_id, bob).await;
let bob = Contact::create(&t, "", "bob@example.org").await?;
chat::add_contact_to_chat(&t, group_id, bob).await?;
let subject = send_msg_get_subject(&t, group_id, None).await;
let subject = send_msg_get_subject(&t, group_id, None).await?;
assert_eq!(subject, "groupname");
let subject = send_msg_get_subject(&t, group_id, None).await;
let subject = send_msg_get_subject(&t, group_id, None).await?;
assert_eq!(subject, "Re: groupname");
dc_receive_imf(
@@ -1666,28 +1683,26 @@ mod tests {
5,
false,
)
.await
.unwrap();
.await?;
let message_from_bob = t.get_last_msg().await;
let subject = send_msg_get_subject(&t, group_id, None).await;
let subject = send_msg_get_subject(&t, group_id, None).await?;
assert_eq!(subject, "Re: groupname");
let subject = send_msg_get_subject(&t, group_id, Some(&message_from_bob)).await;
let subject = send_msg_get_subject(&t, group_id, Some(&message_from_bob)).await?;
let outgoing_quoting_msg = t.get_last_msg().await;
assert_eq!(subject, "Re: Different subject");
let subject = send_msg_get_subject(&t, group_id, None).await;
let subject = send_msg_get_subject(&t, group_id, None).await?;
assert_eq!(subject, "Re: groupname");
let subject = send_msg_get_subject(&t, group_id, Some(&outgoing_quoting_msg)).await;
let subject = send_msg_get_subject(&t, group_id, Some(&outgoing_quoting_msg)).await?;
assert_eq!(subject, "Re: Different subject");
chat::forward_msgs(&t, &[message_from_bob.id], group_id)
.await
.unwrap();
let subject = get_subject(&t, t.pop_sent_msg().await).await;
assert_eq!(subject, "Fwd: Different subject");
chat::forward_msgs(&t, &[message_from_bob.id], group_id).await?;
let subject = get_subject(&t, t.pop_sent_msg().await).await?;
assert_eq!(subject, "Re: groupname");
Ok(())
}
async fn first_subject_str(t: TestContext) -> String {

View File

@@ -136,6 +136,18 @@ const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
impl MimeMessage {
pub async fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
MimeMessage::from_bytes_with_partial(context, body, None).await
}
/// Parse a mime message.
///
/// If `partial` is set, it contains the full message size in bytes
/// and `body` contains the header only.
pub async fn from_bytes_with_partial(
context: &Context,
body: &[u8],
partial: Option<u32>,
) -> Result<Self> {
let mail = mailparse::parse_mail(body)?;
let message_time = mail
@@ -274,7 +286,18 @@ impl MimeMessage {
is_mime_modified: false,
decoded_data: Vec::new(),
};
parser.parse_mime_recursive(context, &mail, false).await?;
match partial {
Some(org_bytes) => {
parser
.create_stub_from_partial_download(context, org_bytes)
.await?;
}
None => {
parser.parse_mime_recursive(context, &mail, false).await?;
}
};
parser.maybe_remove_bad_parts();
parser.maybe_remove_inline_mailinglist_footer();
parser.heuristically_parse_ndn(context).await;
@@ -1141,11 +1164,11 @@ impl MimeMessage {
report: &mailparse::ParsedMail<'_>,
) -> Result<Option<FailureReport>> {
// parse as mailheaders
if let Some(original_msg) = report
.subparts
.iter()
.find(|p| p.ctype.mimetype.contains("rfc822") || p.ctype.mimetype == "message/global")
{
if let Some(original_msg) = report.subparts.iter().find(|p| {
p.ctype.mimetype.contains("rfc822")
|| p.ctype.mimetype == "message/global"
|| p.ctype.mimetype == "message/global-headers"
}) {
let report_body = original_msg.get_body_raw()?;
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
@@ -1443,9 +1466,9 @@ pub struct Part {
pub msg_raw: Option<String>,
pub bytes: usize,
pub param: Params,
org_filename: Option<String>,
pub(crate) org_filename: Option<String>,
pub error: Option<String>,
dehtml_failed: bool,
pub(crate) dehtml_failed: bool,
/// the part is a child or a descendant of multipart/related.
/// typically, these are images that are referenced from text/html part
@@ -1453,7 +1476,7 @@ pub struct Part {
///
/// note that multipart/related may contain further multipart nestings
/// and all of them needs to be marked with `is_related`.
is_related: bool,
pub(crate) is_related: bool,
}
/// return mimetype and viewtype for a parsed mail
@@ -2997,4 +3020,74 @@ Message.
Ok(())
}
#[async_std::test]
async fn test_ignore_read_receipt_to_self() -> Result<()> {
let alice = TestContext::new_alice().await;
// Alice receives BCC-self copy of a message sent to Bob.
dc_receive_imf(
&alice,
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: alice@example.com\n\
To: bob@example.net\n\
Subject: foo\n\
Message-ID: first@example.com\n\
Chat-Version: 1.0\n\
Chat-Disposition-Notification-To: alice@example.com\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n"
.as_bytes(),
"INBOX",
1,
false,
)
.await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.state, MessageState::OutDelivered);
// Due to a bug in the old version running on the other device, Alice receives a read
// receipt from self.
dc_receive_imf(
&alice,
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
From: alice@example.com\n\
To: alice@example.com\n\
Subject: message opened\n\
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
Chat-Version: 1.0\n\
Message-ID: second@example.com\n\
Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\
\n\
\n\
--SNIPP\n\
Content-Type: text/plain; charset=utf-8\n\
\n\
Read receipts do not guarantee sth. was read.\n\
\n\
\n\
--SNIPP\n\
Content-Type: message/disposition-notification\n\
\n\
Original-Recipient: rfc822;bob@example.com\n\
Final-Recipient: rfc822;bob@example.com\n\
Original-Message-ID: <first@example.com>\n\
Disposition: manual-action/MDN-sent-automatically; displayed\n\
\n\
\n\
--SNIPP--"
.as_bytes(),
"INBOX",
2,
false,
)
.await?;
// Check that the state has not changed to `MessageState::OutMdnRcvd`.
let msg = Message::load_from_db(&alice, msg.id).await?;
assert_eq!(msg.state, MessageState::OutDelivered);
Ok(())
}
}

View File

@@ -139,6 +139,27 @@ pub enum Param {
/// For MDN-sending job
MsgId = b'I',
/// For Contacts: timestamp of status (aka signature or footer) update.
StatusTimestamp = b'j',
/// For Contacts and Chats: timestamp of avatar update.
AvatarTimestamp = b'J',
/// For Chats: timestamp of status/signature/footer update.
EphemeralSettingsTimestamp = b'B',
/// For Chats: timestamp of subject update.
SubjectTimestamp = b'C',
/// For Chats: timestamp of group name update.
GroupNameTimestamp = b'g',
/// For Chats: timestamp of group name update.
MemberListTimestamp = b'k',
/// For Chats: timestamp of protection settings update.
ProtectionSettingsTimestamp = b'L',
}
/// An object for handling key=value parameter lists.
@@ -245,6 +266,11 @@ impl Params {
self.get(key).and_then(|s| s.parse().ok())
}
/// Get the given parameter and parse as `i64`.
pub fn get_i64(&self, key: Param) -> Option<i64> {
self.get(key).and_then(|s| s.parse().ok())
}
/// Get the given parameter and parse as `bool`.
pub fn get_bool(&self, key: Param) -> Option<bool> {
self.get_int(key).map(|v| v != 0)
@@ -346,6 +372,12 @@ impl Params {
self
}
/// Set the given paramter to the passed in `i64`.
pub fn set_i64(&mut self, key: Param, value: i64) -> &mut Self {
self.set(key, value.to_string());
self
}
/// Set the given parameter to the passed in `f64` .
pub fn set_float(&mut self, key: Param, value: f64) -> &mut Self {
self.set(key, format!("{}", value));

View File

@@ -277,8 +277,8 @@ impl Peerstate {
let msg = stock_str::contact_setup_changed(context, self.addr.clone()).await;
chat::add_info_msg(context, chat_id, msg, timestamp).await;
emit_event!(context, EventType::ChatModified(chat_id));
chat::add_info_msg(context, chat_id, msg, timestamp).await?;
context.emit_event(EventType::ChatModified(chat_id));
} else {
bail!("contact with peerstate.addr {:?} not found", &self.addr);
}
@@ -496,6 +496,30 @@ impl Peerstate {
}
}
/// Removes duplicate peerstates from `acpeerstates` database table.
///
/// Normally there should be no more than one peerstate per address.
/// However, the database does not enforce this condition.
///
/// Previously there were bugs that caused creation of additional
/// peerstates when existing peerstate could not be read due to a
/// temporary database error or a failure to parse stored data. This
/// procedure fixes the problem by removing duplicate records.
pub(crate) async fn deduplicate_peerstates(sql: &Sql) -> Result<()> {
sql.execute(
"DELETE FROM acpeerstates
WHERE id NOT IN (
SELECT MIN(id)
FROM acpeerstates
GROUP BY addr
)",
paramsv![],
)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -131,6 +131,35 @@ static P_AUTISTICI_ORG: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// blindzeln.org.md: delta.blinzeln.de, delta.blindzeln.org
static P_BLINDZELN_ORG: Lazy<Provider> = Lazy::new(|| Provider {
id: "blindzeln.org",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/blindzeln-org",
server: vec![
Server {
protocol: Imap,
socket: Ssl,
hostname: "webbox222.server-home.org",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "webbox222.server-home.org",
port: 465,
username_pattern: Email,
},
],
config_defaults: None,
strict_tls: true,
max_smtp_rcpt_to: None,
oauth2_authorizer: None,
});
// bluewin.ch.md: bluewin.ch
static P_BLUEWIN_CH: Lazy<Provider> = Lazy::new(|| Provider {
id: "bluewin.ch",
@@ -1363,7 +1392,7 @@ static P_ZIGGO_NL: Lazy<Provider> = Lazy::new(|| Provider {
oauth2_authorizer: None,
});
// zoho.md: zohomail.eu, zoho.com
// zoho.md: zohomail.eu, zohomail.com, zoho.com
static P_ZOHO: Lazy<Provider> = Lazy::new(|| Provider {
id: "zoho",
status: Status::Preparation,
@@ -1399,6 +1428,8 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("aol.com", &*P_AOL),
("arcor.de", &*P_ARCOR_DE),
("autistici.org", &*P_AUTISTICI_ORG),
("delta.blinzeln.de", &*P_BLINDZELN_ORG),
("delta.blindzeln.org", &*P_BLINDZELN_ORG),
("bluewin.ch", &*P_BLUEWIN_CH),
("buzon.uy", &*P_BUZON_UY),
("chello.at", &*P_CHELLO_AT),
@@ -1580,6 +1611,7 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("yggmail", &*P_YGGMAIL),
("ziggo.nl", &*P_ZIGGO_NL),
("zohomail.eu", &*P_ZOHO),
("zohomail.com", &*P_ZOHO),
("zoho.com", &*P_ZOHO),
]
.iter()
@@ -1594,6 +1626,7 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
("aol", &*P_AOL),
("arcor.de", &*P_ARCOR_DE),
("autistici.org", &*P_AUTISTICI_ORG),
("blindzeln.org", &*P_BLINDZELN_ORG),
("bluewin.ch", &*P_BLUEWIN_CH),
("buzon.uy", &*P_BUZON_UY),
("chello.at", &*P_CHELLO_AT),
@@ -1653,4 +1686,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
});
pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd(2021, 8, 17));
Lazy::new(|| chrono::NaiveDate::from_ymd(2021, 9, 29));

1019
src/qr.rs

File diff suppressed because it is too large Load Diff

View File

@@ -20,14 +20,17 @@ use crate::{job, stock_str, EventType};
/// quota icon is "yellow".
pub const QUOTA_WARN_THRESHOLD_PERCENTAGE: u64 = 80;
// warning is already issued at QUOTA_WARN_THRESHOLD_PERCENTAGE,
// this threshold only makes the quota icon "red".
pub const QUOTA_ERROR_THRESHOLD_PERCENTAGE: u64 = 99;
// warning again after this usage percentage is reached,
// quota icon is "red".
pub const QUOTA_ERROR_THRESHOLD_PERCENTAGE: u64 = 95;
/// if quota is below this value (again),
/// QuotaExceeding is cleared.
/// This value should be a bit below QUOTA_WARN_THRESHOLD_PERCENTAGE to
/// avoid jittering and lots of warnings when quota is exactly at the warning threshold.
///
/// We do not repeat warnings on a daily base or so as some provider
/// providers report bad values and we would then spam the user.
pub const QUOTA_ALLCLEAR_PERCENTAGE: u64 = 75;
// if recent quota is older,
@@ -96,15 +99,25 @@ fn get_highest_usage<'t>(
highest.ok_or_else(|| anyhow!("no quota_resource found, this is unexpected"))
}
/// Checks if a quota warning is needed.
pub fn needs_quota_warning(curr_percentage: u64, warned_at_percentage: u64) -> bool {
(curr_percentage >= QUOTA_WARN_THRESHOLD_PERCENTAGE
&& warned_at_percentage < QUOTA_WARN_THRESHOLD_PERCENTAGE)
|| (curr_percentage >= QUOTA_ERROR_THRESHOLD_PERCENTAGE
&& warned_at_percentage < QUOTA_ERROR_THRESHOLD_PERCENTAGE)
}
impl Context {
// Adds a job to update `quota.recent`
pub(crate) async fn schedule_quota_update(&self) {
job::kill_action(self, Action::UpdateRecentQuota).await;
job::add(
self,
job::Job::new(Action::UpdateRecentQuota, 0, Params::new(), 0),
)
.await;
pub(crate) async fn schedule_quota_update(&self) -> Result<()> {
if !job::action_exists(self, Action::UpdateRecentQuota).await? {
job::add(
self,
job::Job::new(Action::UpdateRecentQuota, 0, Params::new(), 0),
)
.await?;
}
Ok(())
}
/// Updates `quota.recent`, sets `quota.modified` to the current time
@@ -127,22 +140,21 @@ impl Context {
let folders = get_watched_folders(self).await;
get_unique_quota_roots_and_usage(folders, imap).await
} else {
Err(anyhow!("Quota not supported by your provider."))
Err(anyhow!(stock_str::not_supported_by_provider(self).await))
};
if let Ok(quota) = &quota {
match get_highest_usage(quota) {
Ok((highest, _, _)) => {
if highest >= QUOTA_WARN_THRESHOLD_PERCENTAGE {
if self.get_config_int(Config::QuotaExceeding).await? == 0 {
self.set_config(Config::QuotaExceeding, Some(&highest.to_string()))
.await?;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_str::quota_exceeding(self, highest).await);
add_device_msg_with_importance(self, None, Some(&mut msg), true)
.await?;
}
if needs_quota_warning(
highest,
self.get_config_int(Config::QuotaExceeding).await? as u64,
) {
self.set_config(Config::QuotaExceeding, Some(&highest.to_string()))
.await?;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_str::quota_exceeding(self, highest).await);
add_device_msg_with_importance(self, None, Some(&mut msg), true).await?;
} else if highest <= QUOTA_ALLCLEAR_PERCENTAGE {
self.set_config(Config::QuotaExceeding, None).await?;
}
@@ -163,11 +175,30 @@ impl Context {
#[cfg(test)]
mod tests {
use super::*;
use crate::quota::{
QUOTA_ALLCLEAR_PERCENTAGE, QUOTA_ERROR_THRESHOLD_PERCENTAGE,
QUOTA_WARN_THRESHOLD_PERCENTAGE,
};
#[async_std::test]
async fn test_needs_quota_warning() -> Result<()> {
assert!(!needs_quota_warning(0, 0));
assert!(!needs_quota_warning(10, 0));
assert!(!needs_quota_warning(70, 0));
assert!(!needs_quota_warning(75, 0));
assert!(!needs_quota_warning(79, 0));
assert!(needs_quota_warning(80, 0));
assert!(needs_quota_warning(81, 0));
assert!(!needs_quota_warning(85, 80));
assert!(!needs_quota_warning(85, 81));
assert!(needs_quota_warning(95, 82));
assert!(!needs_quota_warning(97, 95));
assert!(!needs_quota_warning(97, 96));
assert!(!needs_quota_warning(1000, 96));
Ok(())
}
#[allow(clippy::assertions_on_constants)]
#[async_std::test]
async fn test_quota_thresholds() -> anyhow::Result<()> {

View File

@@ -82,7 +82,11 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
let mut jobs_loaded = 0;
let mut info = InterruptInfo::default();
loop {
match job::load_next(&ctx, Thread::Imap, &info).await {
match job::load_next(&ctx, Thread::Imap, &info)
.await
.ok()
.flatten()
{
Some(job) if jobs_loaded <= 20 => {
jobs_loaded += 1;
job::perform_job(&ctx, job::Connection::Inbox(&mut connection), job).await;
@@ -289,7 +293,11 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
let mut interrupt_info = Default::default();
loop {
match job::load_next(&ctx, Thread::Smtp, &interrupt_info).await {
match job::load_next(&ctx, Thread::Smtp, &interrupt_info)
.await
.ok()
.flatten()
{
Some(job) => {
info!(ctx, "executing smtp job");
job::perform_job(&ctx, job::Connection::Smtp(&mut connection), job).await;

View File

@@ -8,7 +8,7 @@ use crate::events::EventType;
use crate::quota::{
QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_MAX_AGE_SECONDS, QUOTA_WARN_THRESHOLD_PERCENTAGE,
};
use crate::{config::Config, dc_tools, scheduler::Scheduler};
use crate::{config::Config, dc_tools, scheduler::Scheduler, stock_str};
use crate::{context::Context, log::LogExt};
use anyhow::{anyhow, Result};
use humansize::{file_size_opts, FileSize};
@@ -73,33 +73,33 @@ impl DetailedConnectivity {
}
}
fn to_string_imap(&self, _context: &Context) -> String {
async fn to_string_imap(&self, context: &Context) -> String {
match self {
DetailedConnectivity::Error(e) => format!("Error: {}", e),
DetailedConnectivity::Error(e) => stock_str::error(context, e).await,
DetailedConnectivity::Uninitialized => "Not started".to_string(),
DetailedConnectivity::Connecting => "Connecting".to_string(),
DetailedConnectivity::Working => "Getting new messages…".to_string(),
DetailedConnectivity::Connecting => stock_str::connecting(context).await,
DetailedConnectivity::Working => stock_str::updating(context).await,
DetailedConnectivity::InterruptingIdle | DetailedConnectivity::Connected => {
"Connected".to_string()
stock_str::connected(context).await
}
DetailedConnectivity::NotConfigured => "Not configured".to_string(),
}
}
fn to_string_smtp(&self, _context: &Context) -> String {
async fn to_string_smtp(&self, context: &Context) -> String {
match self {
DetailedConnectivity::Error(e) => format!("Error: {}", e),
DetailedConnectivity::Error(e) => stock_str::error(context, e).await,
DetailedConnectivity::Uninitialized => {
"(You did not try to send a message recently)".to_string()
"You did not try to send a message recently.".to_string()
}
DetailedConnectivity::Connecting => "Connecting".to_string(),
DetailedConnectivity::Working => "Sending".to_string(),
DetailedConnectivity::Connecting => stock_str::connecting(context).await,
DetailedConnectivity::Working => stock_str::sending(context).await,
// We don't know any more than that the last message was sent successfully;
// since sending the last message, connectivity could have changed, which we don't notice
// until another message is sent
DetailedConnectivity::InterruptingIdle | DetailedConnectivity::Connected => {
"Your last message was sent successfully".to_string()
stock_str::last_msg_sent_successfully(context).await
}
DetailedConnectivity::NotConfigured => "Not configured".to_string(),
}
@@ -251,7 +251,7 @@ impl Context {
/// One of:
/// - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
/// - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
/// - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
/// - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Updating…" or a spinning wheel
/// - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
///
/// We don't use exact values but ranges here so that we can split up
@@ -380,7 +380,7 @@ impl Context {
};
drop(lock);
ret += "<h3>Incoming messages</h3><ul>";
ret += &format!("<h3>{}</h3><ul>", stock_str::incoming_messages(self).await);
for (folder, watch, state) in &folders_states {
let w = self.get_config(*watch).await.ok_or_log(self);
@@ -395,7 +395,7 @@ impl Context {
ret += " <b>";
ret += &*escaper::encode_minimal(&foldername);
ret += ":</b> ";
ret += &*escaper::encode_minimal(&*detailed.to_string_imap(self));
ret += &*escaper::encode_minimal(&*detailed.to_string_imap(self).await);
ret += "</li>";
folder_added = true;
@@ -410,18 +410,21 @@ impl Context {
ret += "<li>";
ret += &*detailed.to_icon();
ret += " ";
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self));
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
ret += "</li>";
}
}
}
ret += "</ul>";
ret += "<h3>Outgoing messages</h3><ul><li>";
ret += &format!(
"<h3>{}</h3><ul><li>",
stock_str::outgoing_messages(self).await
);
let detailed = smtp.get_detailed().await;
ret += &*detailed.to_icon();
ret += " ";
ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self));
ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self).await);
ret += "</li></ul>";
let domain = dc_tools::EmailAddress::new(
@@ -431,7 +434,10 @@ impl Context {
.unwrap_or_default(),
)?
.domain;
ret += &format!("<h3>Storage on {}</h3><ul>", domain);
ret += &format!(
"<h3>{}</h3><ul>",
stock_str::storage_on_domain(self, domain).await
);
let quota = self.quota.read().await;
if let Some(quota) = &*quota {
match &quota.recent {
@@ -454,17 +460,26 @@ impl Context {
ret += &match &resource.name {
Atom(resource_name) => {
format!(
"<b>{}:</b> {} of {} used",
"<b>{}:</b> {}",
&*escaper::encode_minimal(resource_name),
resource.usage.to_string(),
resource.limit.to_string(),
stock_str::part_of_total_used(
self,
resource.usage.to_string(),
resource.limit.to_string()
)
.await,
)
}
Message => {
format!(
"<b>Messages:</b> {} of {} used",
resource.usage.to_string(),
resource.limit.to_string(),
"<b>{}:</b> {}",
stock_str::messages(self).await,
stock_str::part_of_total_used(
self,
resource.usage.to_string(),
resource.limit.to_string()
)
.await,
)
}
Storage => {
@@ -480,7 +495,7 @@ impl Context {
let limit = (resource.limit * 1024)
.file_size(file_size_opts::BINARY)
.unwrap_or_default();
format!("{} of {} used", usage, limit)
stock_str::part_of_total_used(self, usage, limit).await
}
};
@@ -504,11 +519,11 @@ impl Context {
}
if quota.modified + QUOTA_MAX_AGE_SECONDS < time() {
self.schedule_quota_update().await;
self.schedule_quota_update().await?;
}
} else {
ret += "<li>One moment...</li>";
self.schedule_quota_update().await;
ret += &format!("<li>{}</li>", stock_str::one_moment(self).await);
self.schedule_quota_update().await?;
}
ret += "</ul>";

View File

@@ -31,7 +31,7 @@ mod bobstate;
mod qrinvite;
use bobstate::{BobHandshakeStage, BobState, BobStateHandle};
use qrinvite::{QrError, QrInvite};
use qrinvite::QrInvite;
pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
@@ -105,7 +105,8 @@ impl Bob {
) -> Result<StartedProtocolVariant, JoinError> {
let mut guard = self.inner.lock().await;
if guard.is_some() {
return Err(JoinError::AlreadyRunning);
warn!(context, "The new securejoin will replace the ongoing one.");
*guard = None;
}
let variant = match invite {
QrInvite::Group { ref grpid, .. } => {
@@ -160,7 +161,7 @@ impl Bob {
///
/// With `group` set to `None` this generates a setup-contact QR code, with `group` set to a
/// [`ChatId`] generates a join-group QR code for the given chat.
pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Option<String> {
pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Result<String> {
/*=======================================================
==== Alice - the inviter side ====
==== Step 1 in "Setup verified contact" protocol ====
@@ -175,28 +176,25 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> O
let self_addr = match context.get_config(Config::ConfiguredAddr).await {
Ok(Some(addr)) => addr,
Ok(None) => {
error!(context, "Not configured, cannot generate QR code.");
return None;
bail!("Not configured, cannot generate QR code.");
}
Err(err) => {
error!(
context,
"Unable to retrieve configuration, cannot generate QR code: {:?}", err
bail!(
"Unable to retrieve configuration, cannot generate QR code: {:?}",
err
);
return None;
}
};
let self_name = context
.get_config(Config::Displayname)
.await
.ok()?
.await?
.unwrap_or_default();
let fingerprint: Fingerprint = match get_self_fingerprint(context).await {
Some(fp) => fp,
None => {
return None;
bail!("No fingerprint, cannot generate QR code.");
}
};
@@ -207,39 +205,34 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> O
let qr = if let Some(group) = group {
// parameters used: a=g=x=i=s=
if let Ok(chat) = Chat::load_from_db(context, group).await {
let group_name = chat.get_name();
let group_name_urlencoded =
utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
let chat = Chat::load_from_db(context, group).await?;
let group_name = chat.get_name();
let group_name_urlencoded = utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
Some(format!(
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
fingerprint.hex(),
self_addr_urlencoded,
&group_name_urlencoded,
&chat.grpid,
&invitenumber,
&auth,
))
} else {
error!(context, "Cannot get QR-code for chat-id {}", group,);
return None;
}
format!(
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
fingerprint.hex(),
self_addr_urlencoded,
&group_name_urlencoded,
&chat.grpid,
&invitenumber,
&auth,
)
} else {
// parameters used: a=n=i=s=
Some(format!(
format!(
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
fingerprint.hex(),
self_addr_urlencoded,
self_name_urlencoded,
&invitenumber,
&auth,
))
)
};
info!(context, "Generated QR code: {}", qr.as_ref().unwrap());
info!(context, "Generated QR code: {}", qr);
qr
Ok(qr)
}
async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
@@ -254,21 +247,21 @@ async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
#[derive(Debug, thiserror::Error)]
pub enum JoinError {
#[error("Unknown QR-code: {0}")]
QrCode(#[from] QrError),
#[error("A setup-contact/secure-join protocol is already running")]
AlreadyRunning,
#[error("An \"ongoing\" process is already running")]
OngoingRunning,
#[error("Failed to send handshake message: {0}")]
SendMessage(#[from] SendMsgError),
// Note that this can currently only occur if there is a bug in the QR/Lot code as this
// is supposed to create a contact for us.
#[error("Unknown contact (this is a bug): {0}")]
UnknownContact(#[source] anyhow::Error),
// Note that this can only occur if we failed to create the chat correctly.
#[error("Ongoing sender dropped (this is a bug)")]
OngoingSenderDropped,
#[error("Other")]
Other(#[from] anyhow::Error),
}
@@ -297,7 +290,7 @@ async fn securejoin(context: &Context, qr: &str) -> Result<ChatId, JoinError> {
========================================================*/
info!(context, "Requesting secure-join ...",);
let qr_scan = check_qr(context, qr).await;
let qr_scan = check_qr(context, qr).await?;
let invite = QrInvite::try_from(qr_scan)?;
@@ -635,10 +628,10 @@ pub(crate) async fn handle_securejoin_handshake(
.await?;
return Ok(HandshakeMessage::Ignore);
}
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await;
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await?;
info!(context, "Auth verified.",);
secure_connection_established(context, contact_chat_id).await?;
emit_event!(context, EventType::ContactsChanged(Some(contact_id)));
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
inviter_progress!(context, contact_id, 600);
if join_vg {
// the vg-member-added message is special:
@@ -720,7 +713,7 @@ pub(crate) async fn handle_securejoin_handshake(
==========================================================*/
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
if contact.is_verified(context).await == VerifiedStatus::Unverified {
if contact.is_verified(context).await? == VerifiedStatus::Unverified {
warn!(context, "{} invalid.", step);
return Ok(HandshakeMessage::Ignore);
}
@@ -860,8 +853,8 @@ async fn secure_connection_established(
"?"
};
let msg = stock_str::contact_verified(context, addr).await;
chat::add_info_msg(context, contact_chat_id, msg, time()).await;
emit_event!(context, EventType::ChatModified(contact_chat_id));
chat::add_info_msg(context, contact_chat_id, msg, time()).await?;
context.emit_event(EventType::ChatModified(contact_chat_id));
info!(context, "StockMessage::ContactVerified posted to 1:1 chat");
Ok(())
@@ -884,7 +877,7 @@ async fn could_not_establish_secure_connection(
)
.await;
chat::add_info_msg(context, contact_chat_id, &msg, time()).await;
chat::add_info_msg(context, contact_chat_id, &msg, time()).await?;
error!(
context,
"StockMessage::ContactNotVerified posted to 1:1 chat ({})", details
@@ -953,12 +946,13 @@ mod tests {
use crate::chat;
use crate::chat::ProtectionStatus;
use crate::constants::Chattype;
use crate::events::Event;
use crate::peerstate::Peerstate;
use crate::test_utils::TestContext;
#[async_std::test]
async fn test_setup_contact() {
async fn test_setup_contact() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
@@ -975,10 +969,10 @@ mod tests {
.await;
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
let qr = dc_get_securejoin_qr(&alice.ctx, None).await.unwrap();
let qr = dc_get_securejoin_qr(&alice.ctx, None).await?;
// Step 2: Bob scans QR-code, sends vc-request
dc_join_securejoin(&bob.ctx, &qr).await.unwrap();
dc_join_securejoin(&bob.ctx, &qr).await?;
let sent = bob.pop_sent_msg().await;
assert!(!bob.ctx.has_ongoing().await);
@@ -1055,14 +1049,14 @@ mod tests {
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&alice.ctx).await,
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::Unverified
);
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await,
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::BidirectVerified
);
@@ -1104,14 +1098,14 @@ mod tests {
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&bob.ctx).await,
contact_bob.is_verified(&bob.ctx).await?,
VerifiedStatus::Unverified
);
// Step 7: Bob receives vc-contact-confirm, sends vc-contact-confirm-received
bob.recv_msg(&sent).await;
assert_eq!(
contact_alice.is_verified(&bob.ctx).await,
contact_alice.is_verified(&bob.ctx).await?,
VerifiedStatus::BidirectVerified
);
@@ -1142,17 +1136,18 @@ mod tests {
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm-received"
);
Ok(())
}
#[async_std::test]
async fn test_setup_contact_bad_qr() {
let bob = TestContext::new_bob().await;
let ret = dc_join_securejoin(&bob.ctx, "not a qr code").await;
assert!(matches!(ret, Err(JoinError::QrCode(_))));
assert!(ret.is_err());
}
#[async_std::test]
async fn test_setup_contact_bob_knows_alice() {
async fn test_setup_contact_bob_knows_alice() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
@@ -1169,7 +1164,7 @@ mod tests {
.await;
// Ensure Bob knows Alice_FP
let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await.unwrap();
let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await?;
let peerstate = Peerstate {
addr: "alice@example.com".into(),
last_seen: 10,
@@ -1185,10 +1180,10 @@ mod tests {
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
peerstate.save_to_db(&bob.ctx.sql, true).await.unwrap();
peerstate.save_to_db(&bob.ctx.sql, true).await?;
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
let qr = dc_get_securejoin_qr(&alice.ctx, None).await.unwrap();
let qr = dc_get_securejoin_qr(&alice.ctx, None).await?;
// Step 2+4: Bob scans QR-code, sends vc-request-with-auth, skipping vc-request
dc_join_securejoin(&bob.ctx, &qr).await.unwrap();
@@ -1228,10 +1223,7 @@ mod tests {
"vc-request-with-auth"
);
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
.await
.unwrap()
.fingerprint();
let bob_fp = SignedPublicKey::load_self(&bob.ctx).await?.fingerprint();
assert_eq!(
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
@@ -1244,20 +1236,17 @@ mod tests {
"bob@example.net",
Origin::ManuallyCreated,
)
.await
.unwrap();
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id)
.await
.unwrap();
.await?;
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id).await?;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await,
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::Unverified
);
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await,
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::BidirectVerified
);
@@ -1275,18 +1264,16 @@ mod tests {
.await
.expect("Error looking up contact")
.expect("Contact not found");
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id)
.await
.unwrap();
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id).await?;
assert_eq!(
contact_bob.is_verified(&bob.ctx).await,
contact_bob.is_verified(&bob.ctx).await?,
VerifiedStatus::Unverified
);
// Step 7: Bob receives vc-contact-confirm, sends vc-contact-confirm-received
bob.recv_msg(&sent).await;
assert_eq!(
contact_alice.is_verified(&bob.ctx).await,
contact_alice.is_verified(&bob.ctx).await?,
VerifiedStatus::BidirectVerified
);
@@ -1297,10 +1284,41 @@ mod tests {
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm-received"
);
Ok(())
}
#[async_std::test]
async fn test_secure_join() {
async fn test_setup_contact_concurrent_calls() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
// do a scan that is not working as claire is never responding
let qr_stale = "OPENPGP4FPR:1234567890123456789012345678901234567890#a=claire%40foo.de&n=&i=12345678901&s=23456789012";
let claire_id = dc_join_securejoin(&bob, qr_stale).await?;
let chat = Chat::load_from_db(&bob, claire_id).await?;
assert!(!claire_id.is_special());
assert_eq!(chat.typ, Chattype::Single);
assert!(bob.pop_sent_msg().await.payload().contains("claire@foo.de"));
// subsequent scans shall abort existing ones or run concurrently -
// but they must not fail as otherwise the whole qr scanning becomes unusable until restart.
let qr = dc_get_securejoin_qr(&alice, None).await?;
let alice_id = dc_join_securejoin(&bob, &qr).await?;
let chat = Chat::load_from_db(&bob, alice_id).await?;
assert!(!alice_id.is_special());
assert_eq!(chat.typ, Chattype::Single);
assert_ne!(claire_id, alice_id);
assert!(bob
.pop_sent_msg()
.await
.payload()
.contains("alice@example.com"));
Ok(())
}
#[async_std::test]
async fn test_secure_join() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
@@ -1316,9 +1334,8 @@ mod tests {
})
.await;
let chatid = chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat")
.await
.unwrap();
let chatid =
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
// Step 1: Generate QR-code, secure-join implied by chatid
let qr = dc_get_securejoin_qr(&alice.ctx, Some(chatid))
@@ -1388,10 +1405,7 @@ mod tests {
"vg-request-with-auth"
);
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
.await
.unwrap()
.fingerprint();
let bob_fp = SignedPublicKey::load_self(&bob.ctx).await?.fingerprint();
assert_eq!(
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
@@ -1400,21 +1414,18 @@ mod tests {
// Alice should not yet have Bob verified
let contact_bob_id =
Contact::lookup_id_by_addr(&alice.ctx, "bob@example.net", Origin::Unknown)
.await
.expect("Error looking up contact")
.await?
.expect("Contact not found");
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id)
.await
.unwrap();
let contact_bob = Contact::load_from_db(&alice.ctx, contact_bob_id).await?;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await,
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::Unverified
);
// Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await,
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::BidirectVerified
);
@@ -1432,18 +1443,16 @@ mod tests {
.await
.expect("Error looking up contact")
.expect("Contact not found");
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id)
.await
.unwrap();
let contact_alice = Contact::load_from_db(&bob.ctx, contact_alice_id).await?;
assert_eq!(
contact_bob.is_verified(&bob.ctx).await,
contact_bob.is_verified(&bob.ctx).await?,
VerifiedStatus::Unverified
);
// Step 7: Bob receives vg-member-added, sends vg-member-added-received
bob.recv_msg(&sent).await;
assert_eq!(
contact_alice.is_verified(&bob.ctx).await,
contact_alice.is_verified(&bob.ctx).await?,
VerifiedStatus::BidirectVerified
);
@@ -1456,8 +1465,9 @@ mod tests {
);
let bob_chatid = joiner.await;
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await.unwrap();
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
assert!(bob_chat.is_protected());
assert!(!bob.ctx.has_ongoing().await)
assert!(!bob.ctx.has_ongoing().await);
Ok(())
}
}

View File

@@ -357,8 +357,8 @@ impl BobState {
}
mark_peer_as_verified(context, self.invite.fingerprint()).await?;
Contact::scaleup_origin_by_id(context, self.invite.contact_id(), Origin::SecurejoinJoined)
.await;
emit_event!(context, EventType::ContactsChanged(None));
.await?;
context.emit_event(EventType::ContactsChanged(None));
if let QrInvite::Group { .. } = self.invite {
let member_added = mime_message

View File

@@ -1,16 +1,15 @@
//! Supporting code for the QR-code invite.
//!
//! QR-codes are decoded into a more general-purpose [`Lot`] struct normally, this struct is
//! so general it is not even specific to QR-codes. This makes working with it rather hard,
//! so here we have a wrapper type that specifically deals with Secure-Join QR-codes so
//! that the Secure-Join code can have many more guarantees when dealing with this.
//! QR-codes are decoded into a more general-purpose [`Qr`] struct normally. This makes working
//! with it rather hard, so here we have a wrapper type that specifically deals with Secure-Join
//! QR-codes so that the Secure-Join code can have more guarantees when dealing with this.
use std::convert::TryFrom;
use anyhow::Result;
use anyhow::{bail, Error, Result};
use crate::key::Fingerprint;
use crate::lot::{Lot, LotState};
use crate::qr::Qr;
/// Represents the data from a QR-code scan.
///
@@ -66,53 +65,38 @@ impl QrInvite {
}
}
impl TryFrom<Lot> for QrInvite {
type Error = QrError;
impl TryFrom<Qr> for QrInvite {
type Error = Error;
fn try_from(lot: Lot) -> Result<Self, Self::Error> {
if lot.state != LotState::QrAskVerifyContact && lot.state != LotState::QrAskVerifyGroup {
return Err(QrError::UnsupportedProtocol);
}
if lot.id == 0 {
return Err(QrError::MissingContactId);
}
let fingerprint = lot.fingerprint.ok_or(QrError::MissingFingerprint)?;
let invitenumber = lot.invitenumber.ok_or(QrError::MissingInviteNumber)?;
let authcode = lot.auth.ok_or(QrError::MissingAuthCode)?;
match lot.state {
LotState::QrAskVerifyContact => Ok(QrInvite::Contact {
contact_id: lot.id,
fn try_from(qr: Qr) -> Result<Self> {
match qr {
Qr::AskVerifyContact {
contact_id,
fingerprint,
invitenumber,
authcode,
} => Ok(QrInvite::Contact {
contact_id,
fingerprint,
invitenumber,
authcode,
}),
LotState::QrAskVerifyGroup => Ok(QrInvite::Group {
contact_id: lot.id,
Qr::AskVerifyGroup {
grpname,
grpid,
contact_id,
fingerprint,
name: lot.text1.ok_or(QrError::MissingGroupName)?,
grpid: lot.text2.ok_or(QrError::MissingGroupId)?,
invitenumber,
authcode,
} => Ok(QrInvite::Group {
contact_id,
fingerprint,
name: grpname,
grpid,
invitenumber,
authcode,
}),
_ => Err(QrError::UnsupportedProtocol),
_ => bail!("Unsupported QR type {:?}", qr),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum QrError {
#[error("Unsupported protocol in QR-code")]
UnsupportedProtocol,
#[error("Missing fingerprint")]
MissingFingerprint,
#[error("Missing invitenumber")]
MissingInviteNumber,
#[error("Missing auth code")]
MissingAuthCode,
#[error("Missing group name")]
MissingGroupName,
#[error("Missing group id")]
MissingGroupId,
#[error("Missing contact id")]
MissingContactId,
}

View File

@@ -109,7 +109,8 @@ impl Smtp {
&lp.socks5_config,
&lp.addr,
lp.server_flags & DC_LP_AUTH_OAUTH2 != 0,
lp.provider.map_or(false, |provider| provider.strict_tls),
lp.provider
.map_or(lp.socks5_config.is_some(), |provider| provider.strict_tls),
)
.await
}

View File

@@ -20,7 +20,7 @@ use crate::dc_tools::{dc_delete_file, time};
use crate::ephemeral::start_ephemeral_timers;
use crate::message::Message;
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::peerstate::{deduplicate_peerstates, Peerstate};
use crate::stock_str;
#[macro_export]
@@ -598,7 +598,11 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
);
}
context.schedule_quota_update().await;
if let Err(err) = deduplicate_peerstates(&context.sql).await {
warn!(context, "Failed to deduplicate peerstates: {}", err)
}
context.schedule_quota_update().await?;
if let Err(e) = context
.set_config(Config::LastHousekeeping, Some(&time().to_string()))

View File

@@ -477,6 +477,16 @@ paramsv![]
sql.execute_migration("UPDATE chats SET archived=1 WHERE blocked=2;", 78)
.await?;
}
if dbversion < 79 {
info!(context, "[migration] v79");
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN download_state INTEGER DEFAULT 0;
"#,
79,
)
.await?;
}
Ok((
recalc_fingerprints,

View File

@@ -13,8 +13,10 @@ use crate::config::Config;
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF};
use crate::contact::{Contact, Origin};
use crate::context::Context;
use crate::dc_tools::dc_timestamp_to_str;
use crate::message::Message;
use crate::param::Param;
use humansize::{file_size_opts, FileSize};
/// Stock strings
///
@@ -267,6 +269,54 @@ pub enum StockMessage {
You can check your current storage usage anytime at \"Settings / Connectivity\"."
))]
QuotaExceedingMsgBody = 98,
#[strum(props(fallback = "%1$s message"))]
PartialDownloadMsgBody = 99,
#[strum(props(fallback = "Download maximum available until %1$s"))]
DownloadAvailability = 100,
#[strum(props(fallback = "Incoming Messages"))]
IncomingMessages = 103,
#[strum(props(fallback = "Outgoing Messages"))]
OutgoingMessages = 104,
#[strum(props(fallback = "Storage on %1$s"))]
StorageOnDomain = 105,
#[strum(props(fallback = "One moment…"))]
OneMoment = 106,
#[strum(props(fallback = "Connected"))]
Connected = 107,
#[strum(props(fallback = "Connecting…"))]
Connecting = 108,
#[strum(props(fallback = "Updating…"))]
Updating = 109,
#[strum(props(fallback = "Sending…"))]
Sending = 110,
#[strum(props(fallback = "Your last message was sent successfully."))]
LastMsgSentSuccessfully = 111,
#[strum(props(fallback = "Error: %1$s"))]
Error = 112,
#[strum(props(fallback = "Not supported by your provider."))]
NotSupportedByProvider = 113,
#[strum(props(fallback = "Messages"))]
Messages = 114,
#[strum(props(fallback = "Broadcast List"))]
BroadcastList = 115,
#[strum(props(fallback = "%1$s of %2$s used"))]
PartOfTotallUsed = 116,
}
impl StockMessage {
@@ -857,6 +907,108 @@ pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> St
.replace("%%", "%")
}
/// Stock string: `%1$s message` with placeholder replaced by human-readable size.
pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32) -> String {
let size = org_bytes
.file_size(file_size_opts::BINARY)
.unwrap_or_default();
translated(context, StockMessage::PartialDownloadMsgBody)
.await
.replace1(size)
}
/// Stock string: `Download maximum available until %1$s`.
pub(crate) async fn download_availability(context: &Context, timestamp: i64) -> String {
translated(context, StockMessage::DownloadAvailability)
.await
.replace1(dc_timestamp_to_str(timestamp))
}
/// Stock string: `Incoming Messages`.
pub(crate) async fn incoming_messages(context: &Context) -> String {
translated(context, StockMessage::IncomingMessages).await
}
/// Stock string: `Outgoing Messages`.
pub(crate) async fn outgoing_messages(context: &Context) -> String {
translated(context, StockMessage::OutgoingMessages).await
}
/// Stock string: `Storage on %1$s`.
/// `%1$s` will be replaced by the domain of the configured email-address.
pub(crate) async fn storage_on_domain(context: &Context, domain: impl AsRef<str>) -> String {
translated(context, StockMessage::StorageOnDomain)
.await
.replace1(domain)
}
/// Stock string: `One moment…`.
pub(crate) async fn one_moment(context: &Context) -> String {
translated(context, StockMessage::OneMoment).await
}
/// Stock string: `Connected`.
pub(crate) async fn connected(context: &Context) -> String {
translated(context, StockMessage::Connected).await
}
/// Stock string: `Connecting…`.
pub(crate) async fn connecting(context: &Context) -> String {
translated(context, StockMessage::Connecting).await
}
/// Stock string: `Updating…`.
pub(crate) async fn updating(context: &Context) -> String {
translated(context, StockMessage::Updating).await
}
/// Stock string: `Sending…`.
pub(crate) async fn sending(context: &Context) -> String {
translated(context, StockMessage::Sending).await
}
/// Stock string: `Your last message was sent successfully.`.
pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
translated(context, StockMessage::LastMsgSentSuccessfully).await
}
/// Stock string: `Error: %1$s…`.
/// `%1$s` will be replaced by a possibly more detailed, typically english, error description.
pub(crate) async fn error(context: &Context, error: impl AsRef<str>) -> String {
translated(context, StockMessage::Error)
.await
.replace1(error)
}
/// Stock string: `Not supported by your provider.`.
pub(crate) async fn not_supported_by_provider(context: &Context) -> String {
translated(context, StockMessage::NotSupportedByProvider).await
}
/// Stock string: `Messages`.
/// Used as a subtitle in quota context; can be plural always.
pub(crate) async fn messages(context: &Context) -> String {
translated(context, StockMessage::Messages).await
}
/// Stock string: `%1$s of %2$s used`.
pub(crate) async fn part_of_total_used(
context: &Context,
part: impl AsRef<str>,
total: impl AsRef<str>,
) -> String {
translated(context, StockMessage::PartOfTotallUsed)
.await
.replace1(part)
.replace2(total)
}
/// Stock string: `Broadcast List`.
/// Used as the default name for broadcast lists; a number may be added.
pub(crate) async fn broadcast_list(context: &Context) -> String {
translated(context, StockMessage::BroadcastList).await
}
impl Context {
/// Set the stock string for the [StockMessage].
///
@@ -1050,6 +1202,14 @@ mod tests {
Ok(())
}
#[async_std::test]
async fn test_partial_download_msg_body() -> anyhow::Result<()> {
let t = TestContext::new().await;
let str = partial_download_msg_body(&t, 1024 * 1024).await;
assert_eq!(str, "1 MiB message");
Ok(())
}
#[async_std::test]
async fn test_update_device_chats() {
let t = TestContext::new().await;

317
src/summary.rs Normal file
View File

@@ -0,0 +1,317 @@
//! # Message summary for chatlist.
use crate::chat::Chat;
use crate::constants::{Chattype, Viewtype, DC_CONTACT_ID_SELF};
use crate::contact::Contact;
use crate::context::Context;
use crate::dc_tools::dc_truncate;
use crate::message::{Message, MessageState};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::stock_str;
use itertools::Itertools;
use std::borrow::Cow;
use std::fmt;
/// Prefix displayed before message and separated by ":" in the chatlist.
#[derive(Debug)]
pub enum SummaryPrefix {
/// Username.
Username(String),
/// Stock string saying "Draft".
Draft(String),
/// Stock string saying "Me".
Me(String),
}
impl fmt::Display for SummaryPrefix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SummaryPrefix::Username(username) => write!(f, "{}", username),
SummaryPrefix::Draft(text) => write!(f, "{}", text),
SummaryPrefix::Me(text) => write!(f, "{}", text),
}
}
}
/// Message summary.
#[derive(Debug, Default)]
pub struct Summary {
/// Part displayed before ":", such as an username or a string "Draft".
pub prefix: Option<SummaryPrefix>,
/// Summary text, always present.
pub text: String,
/// Message timestamp.
pub timestamp: i64,
/// Message state.
pub state: MessageState,
}
impl Summary {
pub async fn new(
context: &Context,
msg: &Message,
chat: &Chat,
contact: Option<&Contact>,
) -> Self {
let prefix = if msg.state == MessageState::OutDraft {
Some(SummaryPrefix::Draft(stock_str::draft(context).await))
} else if msg.from_id == DC_CONTACT_ID_SELF {
if msg.is_info() || chat.is_self_talk() {
None
} else {
Some(SummaryPrefix::Me(stock_str::self_msg(context).await))
}
} else {
match chat.typ {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
if msg.is_info() || contact.is_none() {
None
} else {
msg.get_override_sender_name()
.or_else(|| contact.map(|contact| msg.get_sender_name(contact)))
.map(SummaryPrefix::Username)
}
}
Chattype::Single | Chattype::Undefined => None,
}
};
let mut text = msg.get_summary_text(context).await;
if text.is_empty() && msg.quoted_text().is_some() {
text = stock_str::reply_noun(context).await
}
Self {
prefix,
text,
timestamp: msg.get_timestamp(),
state: msg.state,
}
}
/// Returns the [`Summary::text`] attribute truncated to an approximate length.
pub fn truncated_text(&self, approx_chars: usize) -> Cow<str> {
dc_truncate(&self.text, approx_chars)
}
}
impl Message {
/// Returns a summary text.
async fn get_summary_text(&self, context: &Context) -> String {
let mut append_text = true;
let prefix = match self.viewtype {
Viewtype::Image => stock_str::image(context).await,
Viewtype::Gif => stock_str::gif(context).await,
Viewtype::Sticker => stock_str::sticker(context).await,
Viewtype::Video => stock_str::video(context).await,
Viewtype::Voice => stock_str::voice_message(context).await,
Viewtype::Audio | Viewtype::File => {
if self.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
append_text = false;
stock_str::ac_setup_msg_subject(context).await
} else {
let file_name: String = self
.param
.get_path(Param::File, context)
.unwrap_or(None)
.and_then(|path| {
path.file_name()
.map(|fname| fname.to_string_lossy().into_owned())
})
.unwrap_or_else(|| String::from("ErrFileName"));
let label = if self.viewtype == Viewtype::Audio {
stock_str::audio(context).await
} else {
stock_str::file(context).await
};
format!("{} {}", label, file_name)
}
}
Viewtype::VideochatInvitation => {
append_text = false;
stock_str::videochat_invitation(context).await
}
_ => {
if self.param.get_cmd() != SystemMessage::LocationOnly {
"".to_string()
} else {
append_text = false;
stock_str::location(context).await
}
}
};
if !append_text {
return prefix;
}
let summary_content = if let Some(text) = &self.text {
if text.is_empty() {
prefix
} else if prefix.is_empty() {
text.to_string()
} else {
format!("{} {}", prefix, text)
}
} else {
prefix
};
let summary = if self.is_forwarded() {
format!(
"{}: {}",
stock_str::forwarded(context).await,
summary_content
)
} else {
summary_content
};
summary.split_whitespace().join(" ")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils as test;
#[async_std::test]
async fn test_get_summary_text() {
let d = test::TestContext::new().await;
let ctx = &d.ctx;
let some_text = Some(" bla \t\n\tbla\n\t".to_string());
let empty_text = Some("".to_string());
let no_text: Option<String> = None;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(some_text.clone());
assert_eq!(
msg.get_summary_text(ctx).await,
"bla bla" // for simple text, the type is not added to the summary
);
let mut msg = Message::new(Viewtype::Image);
msg.set_text(no_text.clone());
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Image" // file names are not added for images
);
let mut msg = Message::new(Viewtype::Video);
msg.set_text(no_text.clone());
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Video" // file names are not added for videos
);
let mut msg = Message::new(Viewtype::Gif);
msg.set_text(no_text.clone());
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"GIF" // file names are not added for GIFs
);
let mut msg = Message::new(Viewtype::Sticker);
msg.set_text(no_text.clone());
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Sticker" // file names are not added for stickers
);
let mut msg = Message::new(Viewtype::Voice);
msg.set_text(empty_text.clone());
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Voice message" // file names are not added for voice messages, empty text is skipped
);
let mut msg = Message::new(Viewtype::Voice);
msg.set_text(no_text.clone());
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Voice message" // file names are not added for voice messages
);
let mut msg = Message::new(Viewtype::Voice);
msg.set_text(some_text.clone());
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Voice message \u{2013} bla bla" // `\u{2013}` explicitly checks for "EN DASH"
);
let mut msg = Message::new(Viewtype::Audio);
msg.set_text(no_text.clone());
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Audio \u{2013} foo.bar" // file name is added for audio
);
let mut msg = Message::new(Viewtype::Audio);
msg.set_text(empty_text.clone());
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Audio \u{2013} foo.bar" // file name is added for audio, empty text is not added
);
let mut msg = Message::new(Viewtype::Audio);
msg.set_text(some_text.clone());
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"Audio \u{2013} foo.bar \u{2013} bla bla" // file name and text added for audio
);
let mut msg = Message::new(Viewtype::File);
msg.set_text(some_text.clone());
msg.set_file("foo.bar", None);
assert_eq!(
msg.get_summary_text(ctx).await,
"File \u{2013} foo.bar \u{2013} bla bla" // file name is added for files
);
// Forwarded
let mut msg = Message::new(Viewtype::Text);
msg.set_text(some_text.clone());
msg.param.set_int(Param::Forwarded, 1);
assert_eq!(
msg.get_summary_text(ctx).await,
"Forwarded: bla bla" // for simple text, the type is not added to the summary
);
let mut msg = Message::new(Viewtype::File);
msg.set_text(some_text.clone());
msg.set_file("foo.bar", None);
msg.param.set_int(Param::Forwarded, 1);
assert_eq!(
msg.get_summary_text(ctx).await,
"Forwarded: File \u{2013} foo.bar \u{2013} bla bla"
);
let mut msg = Message::new(Viewtype::File);
msg.set_text(no_text.clone());
msg.param.set(Param::File, "foo.bar");
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
assert_eq!(
msg.get_summary_text(ctx).await,
"Autocrypt Setup Message" // file name is not added for autocrypt setup messages
);
}
}

197
src/update_helper.rs Normal file
View File

@@ -0,0 +1,197 @@
//! # Functions to update timestamps.
use crate::chat::{Chat, ChatId};
use crate::contact::Contact;
use crate::context::Context;
use crate::param::{Param, Params};
use anyhow::Result;
impl Context {
/// Updates a contact's timestamp, if reasonable.
/// Returns true if the caller shall update the settings belonging to the scope.
/// (if we have a ContactId type at some point, the function should go there)
pub(crate) async fn update_contacts_timestamp(
&self,
contact_id: u32,
scope: Param,
new_timestamp: i64,
) -> Result<bool> {
let mut contact = Contact::load_from_db(self, contact_id).await?;
if contact.param.update_timestamp(scope, new_timestamp)? {
contact.update_param(self).await?;
return Ok(true);
}
Ok(false)
}
}
impl ChatId {
/// Updates a chat id's timestamp on disk, if reasonable.
/// Returns true if the caller shall update the settings belonging to the scope.
pub(crate) async fn update_timestamp(
&self,
context: &Context,
scope: Param,
new_timestamp: i64,
) -> Result<bool> {
let mut chat = Chat::load_from_db(context, *self).await?;
if chat.param.update_timestamp(scope, new_timestamp)? {
chat.update_param(context).await?;
return Ok(true);
}
Ok(false)
}
}
impl Params {
/// Updates a param's timestamp in memory, if reasonable.
/// Returns true if the caller shall update the settings belonging to the scope.
pub(crate) fn update_timestamp(&mut self, scope: Param, new_timestamp: i64) -> Result<bool> {
let old_timestamp = self.get_i64(scope).unwrap_or_default();
if new_timestamp >= old_timestamp {
self.set_i64(scope, new_timestamp);
return Ok(true);
}
Ok(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dc_receive_imf::dc_receive_imf;
use crate::dc_tools::time;
use crate::test_utils::TestContext;
#[async_std::test]
async fn test_params_update_timestamp() -> Result<()> {
let mut params = Params::new();
let ts = time();
assert!(params.update_timestamp(Param::LastSubject, ts)?);
assert!(params.update_timestamp(Param::LastSubject, ts)?); // same timestamp -> update
assert!(params.update_timestamp(Param::LastSubject, ts + 10)?);
assert!(!params.update_timestamp(Param::LastSubject, ts)?); // `ts` is now too old
assert!(!params.update_timestamp(Param::LastSubject, 0)?);
assert_eq!(params.get_i64(Param::LastSubject).unwrap(), ts + 10);
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)?); // stay unset -> update ...
assert!(params.update_timestamp(Param::GroupNameTimestamp, 0)?); // ... also on multiple calls
assert_eq!(params.get_i64(Param::GroupNameTimestamp).unwrap(), 0);
assert!(!params.update_timestamp(Param::AvatarTimestamp, -1)?);
assert_eq!(params.get_i64(Param::AvatarTimestamp), None);
Ok(())
}
#[async_std::test]
async fn test_out_of_order_subject() -> Result<()> {
let t = TestContext::new_alice().await;
dc_receive_imf(
&t,
b"From: Bob Authname <bob@example.org>\n\
To: alice@example.com\n\
Subject: updated subject\n\
Message-ID: <msg2@example.org>\n\
Chat-Version: 1.0\n\
Date: Sun, 22 Mar 2021 23:37:57 +0000\n\
\n\
second message\n",
"INBOX",
1,
false,
)
.await?;
dc_receive_imf(
&t,
b"From: Bob Authname <bob@example.org>\n\
To: alice@example.com\n\
Subject: original subject\n\
Message-ID: <msg1@example.org>\n\
Chat-Version: 1.0\n\
Date: Sun, 22 Mar 2021 22:37:57 +0000\n\
\n\
first message\n",
"INBOX",
2,
false,
)
.await?;
let msg = t.get_last_msg().await;
let chat = Chat::load_from_db(&t, msg.chat_id).await?;
assert_eq!(
chat.param.get(Param::LastSubject).unwrap(),
"updated subject"
);
Ok(())
}
#[async_std::test]
async fn test_out_of_order_group_name() -> Result<()> {
let t = TestContext::new_alice().await;
dc_receive_imf(
&t,
b"From: Bob Authname <bob@example.org>\n\
To: alice@example.com\n\
Message-ID: <msg1@example.org>\n\
Chat-Version: 1.0\n\
Chat-Group-ID: abcde\n\
Chat-Group-Name: initial name\n\
Date: Sun, 22 Mar 2021 01:00:00 +0000\n\
\n\
first message\n",
"INBOX",
1,
false,
)
.await?;
let msg = t.get_last_msg().await;
let chat = Chat::load_from_db(&t, msg.chat_id).await?;
assert_eq!(chat.name, "initial name");
dc_receive_imf(
&t,
b"From: Bob Authname <bob@example.org>\n\
To: alice@example.com\n\
Message-ID: <msg3@example.org>\n\
Chat-Version: 1.0\n\
Chat-Group-ID: abcde\n\
Chat-Group-Name: another name update\n\
Chat-Group-Name-Changed: a name update\n\
Date: Sun, 22 Mar 2021 03:00:00 +0000\n\
\n\
third message\n",
"INBOX",
2,
false,
)
.await?;
dc_receive_imf(
&t,
b"From: Bob Authname <bob@example.org>\n\
To: alice@example.com\n\
Message-ID: <msg2@example.org>\n\
Chat-Version: 1.0\n\
Chat-Group-ID: abcde\n\
Chat-Group-Name: a name update\n\
Chat-Group-Name-Changed: initial name\n\
Date: Sun, 22 Mar 2021 02:00:00 +0000\n\
\n\
second message\n",
"INBOX",
3,
false,
)
.await?;
let msg = t.get_last_msg().await;
let chat = Chat::load_from_db(&t, msg.chat_id).await?;
assert_eq!(chat.name, "another name update");
Ok(())
}
}

View File

@@ -0,0 +1,60 @@
Return-Path: <return@t.ttline.com>
X-Original-To: pdetersen@b123.com
Delivered-To: m123123f@d123123.kasserver.com
X-policyd-weight: NOT_IN_SPAMCOP=-1.5 NOT_IN_IX_MANITU=-1.5 CL_IP_EQ_HELO_IP=-2 (check from: .ttline. - helo: .mail18-212.srv2. - helo-domain: .srv2.) FROM/MX_MATCHES_HELO(DOMAIN)=-2; rate: -7
Authentication-Results: d123123.kasserver.com;
dkim=pass (1024-bit key; unprotected) header.d=ttline.com header.i=newsletter@ttline.com header.b="SEFAAx0a";
dkim=pass (1024-bit key; unprotected) header.d=srv2.de header.i=@srv2.de header.b="UqUBlHLF";
dkim-atps=neutral
Received: from mail18-212.srv2.de (mail18-212.srv2.de [193.169.181.212])
by d123123.kasserver.com (Postfix) with ESMTPS id 4216753C0228
for <pdetersen@b123.com>; Mon, 12 Jul 2021 18:00:55 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; s=mailing; d=ttline.com;
h=Date:From:Reply-To:To:Message-ID:Subject:MIME-Version:Content-Type:X-ulpe:
List-Id:X-CSA-Complaints:List-Unsubscribe:List-Unsubscribe-Post:Feedback-ID;
i=newsletter@ttline.com;
bh=s/Rjfns4gt9JjcDSSsqHZctvWTOtocDJRpEVs80pElM=;
b=SEFAAx0aG2fD5fytZ1z0WI2elUpWh5J+ekno+UQE/PDqc4bwz5xEUGRXuBszhV9vh3UJVq9HL0Lz
40Bcjzcoob+Iza9KKnl0spLKMPgQNpoCerBCdE/v/DmiWus/gs2MOE+xE5dTM6A8kK0K4ukDoDjr
mnkjezkK8iuh5wwjPqA=
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; s=mailing; d=srv2.de;
h=Date:From:Reply-To:To:Message-ID:Subject:MIME-Version:Content-Type:X-ulpe:
List-Id:X-CSA-Complaints:List-Unsubscribe:List-Unsubscribe-Post:Feedback-ID;
bh=s/Rjfns4gt9JjcDSSsqHZctvWTOtocDJRpEVs80pElM=;
b=UqUBlHLFoluhBzwmQDgHdd9OdiyI9Cy8Y5zqJqfyhmdV34Owpvu1Vx7HnlljqxlUTSVSPtL6Ldoe
bjWHA8yBc0lFKnF7Kt8a2Wd2ac0aHsLgQvwVmoM0T9Av8Hgx4qyRhaTQIho2IbcKcP0IEwEUKVou
KwU4tfT8MLuZHX4rkWc=
Date: Mon, 12 Jul 2021 18:00:54 +0200 (CEST)
From: =?UTF-8?Q?TT-Line_-_Die_Schwedenf=C3=A4hren?= <newsletter@ttline.com>
Reply-To:
=?UTF-8?Q?TT-Line_-_Die_Schwedenf=C3=A4hren?= <newsletter@ttline.com>
To: pdetersen@b123.com
Message-ID: <re-p123123123123123123123123-41231231-4J123123-1123123@t.ttline.com>
Subject: =?UTF-8?Q?Unsere_Sommerangebote_an_Bord_=E2=9A=93?=
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="----=_Part_15756440_1404911700.1626105654088"
X-ulpe: re-p123123123123123123123123-41231231-4J123123-1123123@t.ttline.com
List-Id: <39123123-1BBQXPY.t.ttline.com>
X-Report-Spam: complaints@episerver.com
X-CSA-Complaints: csa-complaints@eco.de
X-sender: =?UTF-8?Q?TT-Line_-_Die_Schwedenf=C3=A4hren?= <newsletter@ttline.com>
List-Unsubscribe: <mailto:listoff-41231231-4J123123-5M123@t.ttline.com?subject=unsubscribe>,<https://t.ttline.com/go/0/41231231-4J123123-39123123-A9E123-UL.html>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
Feedback-ID: 39123123:4J123123:episerver
X-KasLoop: m123123f
------=_Part_15756440_1404911700.1626105654088
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
Content-Disposition: inline
plain...
------=_Part_15756440_1404911700.1626105654088
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
Content-Disposition: inline
html...
------=_Part_15756440_1404911700.1626105654088--

View File

@@ -0,0 +1,75 @@
Return-Path: <mailrobot@mail.xing.com>
X-Original-To: pbetersen@b123.com
Delivered-To: m123123f@dd12312.kasserver.com
X-policyd-weight: using cached result; rate: -7
Authentication-Results: dd12312.kasserver.com;
dkim=pass (1024-bit key; unprotected) header.d=mail.xing.com header.i=@mail.xing.com header.b="o123123j";
dkim-atps=neutral
Received: from mailout1-107.xing.com (mailout1-107.xing.com [109.233.158.107])
by dd12312.kasserver.com (Postfix) with ESMTPS id DCB9D53C055C
for <pbetersen@b123.com>; Tue, 14 Sep 2021 12:11:17 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; d=mail.xing.com; s=main; c=relaxed/simple;
q=dns/txt; i=@mail.xing.com; t=1631614277;
h=From:Subject:Date:To:Mime-Version:Content-Type:X-CSA-Complaints:List-Help;
bh=bc0sqvXhKYO4cLpVn9ZYqLQQYPgPysrjt9/f6mx2BQI=;
b=o123123jtfoTCEiqaRZ3ax7h17rGfXP1ZQ3sjbdaBguPy5q1k+EpuXYdCwFq7S7z
yRjbK+VvTSKDn4Dxqk/wA9hFyGrO6XuYdJt3NEZ3Yye7W222dNR58ww3XCavjSpY
pzhJdAEo9Zw1sG3fhtm2eI60Oe1hLOr6G657sAo2ubQ=;
X-MSFBL: SwzhdcKRUcljkfTYaEiSU28Q3a0pxO+Z9dY1NMgWyq4=|eyJnIjoibWFpbG91dDE
iLCJiIjoibWFpbG91dDEtMTA3IiwidSI6ImNvbnRhY3RzL215bWs7RzhMWjI0RlR
SSUItdVkxaXRaa1EiLCJyIjoiYnBldGVyc2VuQGI0NHQuY29tIn0=
Received: from [10.12.225.241] ([10.12.225.241:27696])
by mta-6.mail.ams1.xing.com (envelope-from <mailrobot@mail.xing.com>)
(ecelerity 4.2.1.51128 r(Core:4.2.1.5)) with REST
id 82/69-03094-54570416; Tue, 14 Sep 2021 12:11:17 +0200
Date: Tue, 14 Sep 2021 12:11:17 +0200
From: =?UTF-8?B?WElORyBLb250YWt0dm9yc2NobMOkZ2U=?= <mailrobot@mail.xing.com>
Reply-To: no-reply@mail.xing.com
To: =?UTF-8?B?QmrDtnJuIFBldGVyc2Vu?= <pbetersen@b123.com>
Message-ID: <6123123123123_5123123123123@hermes-worker-5123123123-7lcmp.mail>
Subject: Kennst Du Dr. Mabuse?
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="--==_mimepart_614075452ac33_5ed3cc13386fc";
charset=UTF-8
Content-Transfer-Encoding: 7bit
List-Help: <https://www.xing.com/settings/notifications>,
<mailto:fbl@xing.com>
List-Id: <51231231231231231231231232869f58.xing.com>
X-CSA-Complaints: csa-complaints@eco.de
X-KasLoop: m123123f
----==_mimepart_614075452ac33_5ed3cc13386fc
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: quoted-printable
**********************************************************
* unfortunately:
*
* - xing mailinglists do not have a name in `List-Id:`
* so we cannot get the name from there
*
* - different senders may use the same `List-Id:`,
* at least i found that two times,
* maybe it is a bug on xing's side as most times `List-Id:` differs,
* however, so we cannot get the name from `From:`.
*
* to avoid chat names as `51231231231231231231231232869f58.xing.com`,
* we detect the hash prefix and strip that;
* as the sender, we have "xing.com" then, which is fine.
*
* that approach should also work for other mailinglist.
**********************************************************
----==_mimepart_614075452ac33_5ed3cc13386fc
Content-Type: text/html;
charset=UTF-8
Content-Transfer-Encoding: quoted-printable
html ...
----==_mimepart_614075452ac33_5ed3cc13386fc--

View File

@@ -0,0 +1,164 @@
Return-Path: <bounce-889884_HTML-1111111111-2222222-33333333-4444@bounce.microsoftstoreemail.com>
X-Original-To: me@foobar.com
Delivered-To: m111111f@dd22222.kasserver.com
X-policyd-weight: NOT_IN_SPAMCOP=-1.5 NOT_IN_IX_MANITU=-1.5 CL_IP_EQ_HELO_MX=-3.1 (check from: .microsoftstoreemail. - helo: .merlinux. - helo-domain: .merlinux.) FROM/MX_MATCHES_NOT_HELO(DOMAIN)=1; rate: -5.1
Authentication-Results: dd22222.kasserver.com;
dkim=pass (1024-bit key; unprotected) header.d=microsoftstoreemail.com header.i=microsoftstore@microsoftstoreemail.com header.b="XV8jLprF";
dkim-atps=neutral
Received: from merlin.eu (hq6.merlin.eu [95.217.159.152])
by dd22222.kasserver.com (Postfix) with ESMTPS id BAD2E53C0541
for <me@foobar.com>; Mon, 30 Aug 2021 20:29:57 +0200 (CEST)
Received: from [127.0.0.1] (localhost [127.0.0.1])
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
(No client certificate requested)
by merlin.eu (Postfix) with ESMTPS id 40DA541C7E
for <me@merlin.eu>; Mon, 30 Aug 2021 20:29:55 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; s=200608; d=microsoftstoreemail.com;
h=From:To:Subject:Date:List-Help:MIME-Version:List-ID:X-CSA-Complaints:
Message-ID:Content-Type; i=microsoftstore@microsoftstoreemail.com;
bh=dvrsbCk+3USZNtvsQRvPSo2qpqsG0det56Snu0/Vz7I=;
b=XV8jLprFcPv/OmruNBYNRrau26cDZYl4EchN88fJa3q49VpWwom5Pakcw2fkj1i63acBRpyVOBEr
M3rZ/p/S3c+n5wkqcQJO/ruWPR16GacnfwYq3zGFIEs5HVjFLbMF+26YuCD7u6GEJC559yD7kWje
1RCh9UZqYxBOsdYhkyk=
Received: by mta16.microsoftstoreemail.com id h1111111111v for <me@merlin.eu>; Mon, 30 Aug 2021 18:14:50 +0000 (envelope-from <bounce-889884_HTML-1111111111-2222222-33333333-4444@bounce.microsoftstoreemail.com>)
From: "Microsoft Store" <microsoftstore@microsoftstoreemail.com>
To: <me@merlin.eu>
Subject: Notice of Update to Microsoft Store Marketing Disclosure Document
Date: Mon, 30 Aug 2021 12:14:50 -0600
List-Help: <https://click.microsoftstoreemail.com/subscription_center.aspx?jwt=123.123.123>
x-CSA-Compliance-Source: SFMC
MIME-Version: 1.0
List-ID: <96540.xt.local>
X-CSA-Complaints: csa-complaints@eco.de
X-SFMC-Stack: 1
x-job: 10359607_7641603
Message-ID: <9b806dd3-1234-1234-1234-49603f588b2c@ind1s01mta720.xt.local>
Content-Type: multipart/alternative;
boundary="x28u2yBkExvV=_?:"
X-Spamd-Bar: ++++
X-Spam-Level: ****
X-Rspamd-Server: hq6
Authentication-Results: merlin.eu;
dkim=pass header.d=microsoftstoreemail.com;
dmarc=pass (policy=none) header.from=microsoftstoreemail.com;
spf=pass smtp.mailfrom=bounce-889884_HTML-1111111111-2222222-33333333-4444@bounce.microsoftstoreemail.com
X-Rspamd-Queue-Id: 40DA541C7E
X-Spamd-Result: default: False [4.17 / 15.00];
ARC_NA(0.00)[];
R_DKIM_ALLOW(-0.20)[microsoftstoreemail.com];
FROM_HAS_DN(0.00)[];
R_SPF_ALLOW(-0.20)[+ip4:64.132.88.0/23];
TO_MATCH_ENVRCPT_ALL(0.00)[];
R_BAD_CTE_7BIT(1.05)[7bit,utf8];
PREVIOUSLY_DELIVERED(0.00)[me@merlin.eu];
TO_DN_NONE(0.00)[];
MIME_GOOD(-0.10)[multipart/alternative,text/plain];
URI_COUNT_ODD(1.00)[63];
RCPT_COUNT_ONE(0.00)[1];
MANY_INVISIBLE_PARTS(0.10)[2];
IP_SCORE(-0.47)[asn: 22606(-2.23), country: US(-0.10)];
DKIM_TRACE(0.00)[microsoftstoreemail.com:+];
HTML_SHORT_LINK_IMG_2(1.00)[];
DMARC_POLICY_ALLOW(-0.50)[microsoftstoreemail.com,none];
MX_GOOD(-0.01)[cached: inbound.s1.exacttarget.com];
FORGED_SENDER(0.30)[microsoftstore@microsoftstoreemail.com,bounce-889884_HTML-1111111111-2222222-33333333-4444@bounce.microsoftstoreemail.com];
RWL_MAILSPIKE_POSSIBLE(0.00)[177.89.132.64.rep.mailspike.net : 127.0.0.17];
RCVD_TLS_LAST(0.00)[];
HFILTER_URL_ONLY(2.20)[1];
ASN(0.00)[asn:22606, ipnet:64.132.89.0/24, country:US];
FROM_NEQ_ENVFROM(0.00)[microsoftstore@microsoftstoreemail.com,bounce-889884_HTML-1111111111-2222222-33333333-4444@bounce.microsoftstoreemail.com];
GREYLIST(0.00)[pass,body];
RCVD_COUNT_TWO(0.00)[2]
X-KasLoop: m111111f
This is a multi-part message in MIME format.
--x28u2yBkExvV=_?:
Content-Type: text/plain;
charset="utf-8"
Content-Transfer-Encoding: 7bit
<!--
en_us
@templateVersion = "Templates_v01"
-->
<span style="display: none"></span>
<html lang="en">
<head>
<title>Notice of Update to Microsoft Store Marketing Disclosure Document</title>
<style media="all" type="text/css">
div.preheader {
display: none !important;
}
...
</style>
</head>
<body>
<!-- Wrapper -->
<div style="display: none; max-height: 0px; overflow: hidden;">Please read these important updates&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
... 100 or so more of these lines - and yes, this is really in a `text/plain` part ...
</body>
</html>
--x28u2yBkExvV=_?:
Content-Type: text/html;
charset="utf-8"
Content-Transfer-Encoding: 7bit
<!-- Template Top -->
<!--
en_us
@templateVersion = "Templates_v01"
-->
<span style="display: none"></span>
<html lang="en">
<head>
<title>Notice of Update to Microsoft Store Marketing Disclosure Document</title>
<style media="all" type="text/css">
div.preheader {
display: none !important;
}
...
</style>
</head>
<body>
<!-- Wrapper -->
<div style="display: none; max-height: 0px; overflow: hidden;">Please read these important updates&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
... 100 or so more of these lines ...
</body>
</html>
--x28u2yBkExvV=_?:--

View File

@@ -0,0 +1,70 @@
Return-Path: <bounce-155_HTML-111111111-222222-333333333-1234@bounce.angebote.spiegel.de>
X-Original-To: me@foobar.com
Delivered-To: m000002f@dd22222.kasserver.com
X-policyd-weight: NOT_IN_SPAMCOP=-1.5 NOT_IN_IX_MANITU=-1.5 CL_IP_EQ_HELO_IP=-2 (check from: .spiegel. - helo: .mta.angebote.spiegel. - helo-domain: .spiegel.) FROM/MX_MATCHES_HELO(DOMAIN)=-2; rate: -7
Authentication-Results: dd22222.kasserver.com;
dkim=pass (2048-bit key; unprotected) header.d=angebote.spiegel.de header.i=service@angebote.spiegel.de header.b="EO7/nr7w";
dkim-atps=neutral
Received: from mta.angebote.spiegel.de (mta.angebote.spiegel.de [13.111.60.76])
by dd22222.kasserver.com (Postfix) with ESMTPS id 5667B53C0576
for <me@foobar.com>; Mon, 6 Sep 2021 13:27:43 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; s=10dkim1; d=angebote.spiegel.de;
h=From:To:Subject:Date:List-Unsubscribe:List-Unsubscribe-Post:MIME-Version:
Reply-To:List-ID:X-CSA-Complaints:Message-ID:Content-Type;
i=service@angebote.spiegel.de;
bh=OgZoChKe0M5AaHNMVEyCAZ9jK0q01BajqQhtcqP22/Q=;
b=EO7/nr7w54xd68XV1+qUteycqAN63r7HH4QAw60wImD4rE76J4+vWAcf8TNYayM/vPefcM7zcfFk
ERwrlR/aT4BoRAWghyQHKgAZ0lwVKpqWGuYe9cF8wjLuYJOwPCoYIiry/GgOSXIVwazlXE2FRN9V
Y8m8OStmH3KbVZC55j1Ta6OXMMHEyvwZ+/OHbJ2CLW3jinw84NevP2aMDoI60TidBS5HYVclUT9W
IT4bs1o6a659LeVw/ViitQULL7c2P/UWPv0gm5w5IRci6jmdCOzfa+rvFmxSGIlfalTJ/VVG4V+V
PlbJl49RrPBuXl62ub6f1EPjFGtbJD8Gy3BliQ==
Received: by mta.angebote.spiegel.de id h6ntj42fmd4o for <me@foobar.com>; Mon, 6 Sep 2021 11:27:41 +0000 (envelope-from <bounce-155_HTML-111111111-222222-333333333-1234@bounce.angebote.spiegel.de>)
From: "DER SPIEGEL Kundenservice" <service@angebote.spiegel.de>
To: <me@foobar.com>
Subject: subject here
Date: Mon, 06 Sep 2021 05:27:41 -0600
List-Unsubscribe: <https://click.angebote.spiegel.de/subscription_center.aspx?jwt=123.123.123-123-123>, <mailto:leave-123-123-123-123-123@leave.angebote.spiegel.de>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
x-CSA-Compliance-Source: SFMC
MIME-Version: 1.0
Reply-To: "DER SPIEGEL Kundenservice" <reply-123-123-123-123-123@angebote.spiegel.de>
List-ID: <121231234.xt.local>
X-CSA-Complaints: csa-complaints@eco.de
X-SFMC-Stack: 10
x-job: 100006074_884883
Message-ID: <123-123-123-123-123@dfw123123a96.xt.local>
Content-Type: multipart/alternative;
boundary="NwnF3UJtimpn=_?:"
X-KasLoop: m000002f
This is a multi-part message in MIME format.
--NwnF3UJtimpn=_?:
Content-Type: text/plain;
charset="utf-8"
Content-Transfer-Encoding: 8bit
plain text here
--NwnF3UJtimpn=_?:
Content-Type: text/html;
charset="utf-8"
Content-Transfer-Encoding: 8bit
<p>html text here</p>
--NwnF3UJtimpn=_?:--

View File

@@ -0,0 +1,97 @@
Return-Path: <>
Delivered-To: alice@example.org
Received: from hq5.merlinux.eu
by hq5.merlinux.eu with LMTP
id IF/+LHrTFGEQBQAAPzvFDg
(envelope-from <>)
for <alice@example.org>; Thu, 12 Aug 2021 09:53:30 +0200
Received: by hq5.merlinux.eu (Postfix)
id 9C87727A0006; Thu, 12 Aug 2021 09:53:30 +0200 (CEST)
Date: Thu, 12 Aug 2021 09:53:30 +0200 (CEST)
From: MAILER-DAEMON@hq5.merlinux.eu (Mail Delivery System)
Subject: Undelivered Mail Returned to Sender
To: alice@example.org
Auto-Submitted: auto-replied
MIME-Version: 1.0
Content-Type: multipart/report; report-type=delivery-status;
boundary="A82D727A0003.1628754810/hq5.merlinux.eu"
Content-Transfer-Encoding: 8bit
Message-Id: <20210812075330.9C87727A0006@hq5.merlinux.eu>
This is a MIME-encapsulated message.
--A82D727A0003.1628754810/hq5.merlinux.eu
Content-Description: Notification
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
This is the mail system at host hq5.merlinux.eu.
I'm sorry to have to inform you that your message could not
be delivered to one or more recipients. It's attached below.
For further assistance, please send mail to postmaster.
If you do so, please include this problem report. You can
delete your own text from the attached returned message.
The mail system
<bob@example.org>: Host or domain name not found. Name service error for
name=echedelyr.tk type=AAAA: Host not found
--A82D727A0003.1628754810/hq5.merlinux.eu
Content-Description: Delivery report
Content-Type: message/global-delivery-status
Content-Transfer-Encoding: 8bit
Reporting-MTA: dns; hq5.merlinux.eu
X-Postfix-Queue-ID: A82D727A0003
X-Postfix-Sender: rfc822; alice@example.org
Arrival-Date: Thu, 12 Aug 2021 09:53:29 +0200 (CEST)
Final-Recipient: rfc822; bob@example.org
Original-Recipient: rfc822;bob@example.org
Action: failed
Status: 5.4.4
Diagnostic-Code: X-Postfix; Host or domain name not found. Name service error
for name=echedelyr.tk type=AAAA: Host not found
--A82D727A0003.1628754810/hq5.merlinux.eu
Content-Description: Undelivered Message Headers
Content-Type: message/global-headers
Content-Transfer-Encoding: 8bit
Return-Path: <alice@example.org>
Chat-Disposition-Notification-To: alice@example.org
DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=testrun.org;
s=testrun; t=1628754810;
bh=exDToKrRerWMZ62UQVK/RgNiwDDKe+GqF6zGdG64jl8=;
h=Subject:References:Date:To:From:From;
b=SNTTDdhkppXqimCSPP+cqDvdzmryYwzurtZdN2XkTVQEeqMMdnGvEA9TOgeZpHqi0
oHSjJ5oD+eUK1ECnfRuxDF9DmFnK7sbw1MaHxIcAVTLPgHrMxv+2Fjq1nmrerzmr1t
z9jOYY8e6gfEBw1uDAfHMmIl4OGuoDSll8haqNF3C2JqdwTtcdtE/w6ERJwSeHhCsR
bknan9Rh75Tr46Zh8WVi9YYRVDGFj7+OlL/67Va+Jxl3c4v4EJ5vF6ncxyJupP9eU2
qndV+g1BbxsARo663codYZRiGh217AI8DG2HUr0rVOPdvWm1kw/NTkp3BxoHkv5q2a
Uak5Jiieur6Hg==
Subject: Message from Hocuri
Chat-User-Avatar: 0
MIME-Version: 1.0
References: <Mr.5xqflwt0YFv.IXDFfHauvWx@testrun.org>
Date: Thu, 12 Aug 2021 07:53:28 +0000
Chat-Version: 1.0
Autocrypt: addr=alice@example.org;
keydata=xjMEX3tmZxYJKwYBBAHaRw8BAQdAl4LKVKPRqxG1ZXEO8e9s1DZWt6f38wSuJnY0mLSOuf
7NFTxob2N1cmkxQHRlc3RydW4ub3JnPsKLBBAWCAAzAhkBBQJfe2ZnAhsDBAsJCAcGFQgJCgsCAxYC
ARYhBBmltZLxgqC0SHJPEL7qvlxmQUTNAAoJEL7qvlxmQUTNIucBAJkRclHRG7cWpFbMYW+rspEFIQ
j1GTKwriiBpk5ffnroAQC3h/scScpG/EeIPL0y80GRS5BoR1Ium3zrlR92EaijDc44BF97ZmcSCisG
AQQBl1UBBQEBB0CmxhyX/NuXIlrl0/fdeEseAv6KCbZ4tV3tIvSvnH1KHgMBCAfCeAQYFggAIAUCX3
tmZwIbDBYhBBmltZLxgqC0SHJPEL7qvlxmQUTNAAoJEL7qvlxmQUTNnkYA/3qY+e6PrtR1WT7PiVeZ
RIQBkkJjWWSx+lBQ5fNb3e92AQCBEG3OnGy4RrxOqWW2ry7ETP33CJeiwAwCvv4LQlwCCw==
Message-ID: <Mr.5xqflwt0YFv.IXDFfHauvWx@testrun.org>
To: <bob@example.org>
From: Hocuri <alice@example.org>
Content-Type: multipart/mixed; boundary="qtFe0wPDNHWVlvqV0B8ymdWmE6ZmKD"
--A82D727A0003.1628754810/hq5.merlinux.eu--