mirror of
https://github.com/chatmail/core.git
synced 2026-10-18 02:43:02 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fba6e885ae | ||
|
|
eadbf41383 | ||
|
|
70ab41d7c2 | ||
|
|
fab39fdb97 | ||
|
|
63664396dd | ||
|
|
f10f65333b | ||
|
|
7cf42a43e7 |
-142
@@ -1,138 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [2.10.0] - 2025-08-04
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Also lookup key contacts in lookup_id_by_addr() ([#7073](https://github.com/chatmail/core/pull/7073)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump serde_json from 1.0.140 to 1.0.142.
|
||||
- cargo: Bump bolero from 0.13.3 to 0.13.4.
|
||||
- cargo: Bump async-channel from 2.3.1 to 2.5.0.
|
||||
- cargo: Bump hyper-util from 0.1.14 to 0.1.16.
|
||||
- cargo: Bump criterion from 0.6.0 to 0.7.0.
|
||||
- cargo: Bump strum from 0.27.1 to 0.27.2.
|
||||
- cargo: Bump strum_macros from 0.27.1 to 0.27.2.
|
||||
- Upgrade async-imap to 0.11.1.
|
||||
|
||||
## [2.9.0] - 2025-07-31
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- repl: Add import-vcard and make-vcard commands ([#7048](https://github.com/chatmail/core/pull/7048)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Display correct timer value for ephemeral timer changes.
|
||||
- Get_chat_msgs_ex(): Report local midnight in ChatItem::DayMarker.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Rename add_or_lookup_key_contacts_by_address_list() to add_or_lookup_key_contacts().
|
||||
- Don't call add_or_lookup_key_contacts() in advance.
|
||||
|
||||
## [2.8.0] - 2025-07-28
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Remove ProtectionBroken, make such chats Unprotected ([#7041](https://github.com/chatmail/core/pull/7041)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Lookup self by address if there is no fingerprint or gossip.
|
||||
|
||||
## [2.7.0] - 2025-07-26
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Mimefactory: Order message recipients by time of addition ([#6872](https://github.com/chatmail/core/pull/6872)).
|
||||
- Put the debug/release build version into the info ([#7034](https://github.com/chatmail/core/pull/7034)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Realtime late join ([#6869](https://github.com/chatmail/core/pull/6869)).
|
||||
- Do not fail to upgrade if the verifier of a contact doesn't exist anymore ([#7044](https://github.com/chatmail/core/pull/7044)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Add regression test for verification-gossiping crash ([#7033](https://github.com/chatmail/core/pull/7033)).
|
||||
|
||||
## [2.6.0] - 2025-07-23
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix crash when receiving a verification-gossiping message which a contact also sends to itself ([#7032](https://github.com/chatmail/core/pull/7032)).
|
||||
|
||||
## [2.5.0] - 2025-07-22
|
||||
|
||||
### Fixes
|
||||
|
||||
- Correctly migrate "verified by me".
|
||||
- Mark all email chats as unprotected in the migration ([#7026](https://github.com/chatmail/core/pull/7026)).
|
||||
- Do not ignore errors in add_flag_finalized_with_set.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Deprecate protection-broken and related stuff ([#7018](https://github.com/chatmail/core/pull/7018)).
|
||||
- Clarify the meaning of is_verified() vs verifier_id() ([#7027](https://github.com/chatmail/core/pull/7027)).
|
||||
- STYLE.md: Prefer `try_next()` over `next()`.
|
||||
|
||||
## [2.4.0] - 2025-07-21
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not ignore errors when draining FETCH responses. This avoids IMAP loop getting stuck in an infinite loop retrying reading from the connection.
|
||||
- Update `tokio-io-timeout` to 1.2.1. This release includes a fix to reset timeout after every error, so timeout error is returned at most once a minute if read is attempted after a timeout.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update async-imap to 0.11.0.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use `try_next()` when processing FETCH responses.
|
||||
|
||||
## [2.3.0] - 2025-07-19
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add "e2ee encrypted" info message to all e2ee chats ([#7008](https://github.com/chatmail/core/pull/7008)).
|
||||
- repl: Print errors and debug logs to stderr.
|
||||
- `{ensure_and,logged}_debug_assert`: Don't evaluate condition twice.
|
||||
- Log when background fetch of all accounts finishes successfully.
|
||||
- Log the number of read/written bytes on IMAP stream read error ([#6924](https://github.com/chatmail/core/pull/6924)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Ignore protected headers in outer message part ([#6357](https://github.com/chatmail/core/pull/6357)).
|
||||
- List e-mail contacts in repl listcontacts command.
|
||||
- Save peer address for LoggingStream early.
|
||||
|
||||
## [2.2.0] - 2025-07-14
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add chat::create_group_ex(), deprecate create_group_chat() ([#6927](https://github.com/chatmail/core/pull/6927)).
|
||||
- jsonrpc: Add CommandApi::create_group_chat_unencrypted() ([#6927](https://github.com/chatmail/core/pull/6927)).
|
||||
- [**breaking**] In ChatListItem, replace is_group and is_(out_)broadcast with chat_type property ([#7003](https://github.com/chatmail/core/pull/7003)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Log failed debug assertions in all configurations.
|
||||
- Donation request device message ([#6913](https://github.com/chatmail/core/pull/6913)).
|
||||
- Advance next UID even if connection fails while fetching.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Always prefer the last header.
|
||||
|
||||
### Tests
|
||||
|
||||
- Tune down DELTACHAT_SAVE_TMP_DB hint ([#6998](https://github.com/chatmail/core/pull/6998)).
|
||||
- Unencrypted group creation ([#6927](https://github.com/chatmail/core/pull/6927)).
|
||||
|
||||
## [2.1.0] - 2025-07-11
|
||||
|
||||
### Features / Changes
|
||||
@@ -6587,12 +6454,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.160.0]: https://github.com/chatmail/core/compare/v1.159.5..v1.160.0
|
||||
[2.0.0]: https://github.com/chatmail/core/compare/v1.160.0..v2.0.0
|
||||
[2.1.0]: https://github.com/chatmail/core/compare/v2.0.0..v2.1.0
|
||||
[2.2.0]: https://github.com/chatmail/core/compare/v2.1.0..v2.2.0
|
||||
[2.3.0]: https://github.com/chatmail/core/compare/v2.2.0..v2.3.0
|
||||
[2.4.0]: https://github.com/chatmail/core/compare/v2.3.0..v2.4.0
|
||||
[2.5.0]: https://github.com/chatmail/core/compare/v2.4.0..v2.5.0
|
||||
[2.6.0]: https://github.com/chatmail/core/compare/v2.5.0..v2.6.0
|
||||
[2.7.0]: https://github.com/chatmail/core/compare/v2.6.0..v2.7.0
|
||||
[2.8.0]: https://github.com/chatmail/core/compare/v2.7.0..v2.8.0
|
||||
[2.9.0]: https://github.com/chatmail/core/compare/v2.8.0..v2.9.0
|
||||
[2.10.0]: https://github.com/chatmail/core/compare/v2.9.0..v2.10.0
|
||||
|
||||
Generated
+65
-77
@@ -229,9 +229,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.5.0"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
@@ -268,11 +268,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.11.1"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8da885da5980f3934831e6370445c0e0e44ef251d7792308b39e908915a41d09"
|
||||
checksum = "ca726c61b73c471f531b65e83e161776ba62c2b6ba4ec73d51fad357009ed00a"
|
||||
dependencies = [
|
||||
"async-channel 2.5.0",
|
||||
"async-channel 2.3.1",
|
||||
"async-compression",
|
||||
"base64",
|
||||
"bytes",
|
||||
@@ -535,9 +535,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bolero"
|
||||
version = "0.13.4"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ff44d278fc0062c95327087ed96b3d256906d1d8f579e534a3de8d6b386913a"
|
||||
checksum = "4e913ed74716cd68dc5be41c702327b1cc4ffc8f0b55945ae46fb015777007eb"
|
||||
dependencies = [
|
||||
"bolero-afl",
|
||||
"bolero-engine",
|
||||
@@ -561,9 +561,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bolero-engine"
|
||||
version = "0.13.4"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dca199170a7c92c669c1019f9219a316b66bcdcfa4b36cac5a460a4c1a851aba"
|
||||
checksum = "05cae8c41807b046bb7005f52fa60c8f67787c1bf272242f0b84224853e04ceb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bolero-generator",
|
||||
@@ -575,9 +575,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bolero-generator"
|
||||
version = "0.13.5"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98a5782f2650f80d533f58ec339c6dce4cc5428f9c2755894f98156f52af81f2"
|
||||
checksum = "8e3ac7405f187921256faa515fa05ae02521103582a9d938410cefabe3a9a172"
|
||||
dependencies = [
|
||||
"bolero-generator-derive",
|
||||
"either",
|
||||
@@ -588,14 +588,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bolero-generator-derive"
|
||||
version = "0.13.4"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a21a3b022507b9edd2050caf370d945e398c1a7c8455531220fa3968c45d29e"
|
||||
checksum = "9c56c2f8c1c0707d678bebb36168cfd523c45927bb8d9cb7567d3578fa428cbd"
|
||||
dependencies = [
|
||||
"proc-macro-crate 2.0.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1023,16 +1023,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.7.0"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928"
|
||||
checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679"
|
||||
dependencies = [
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"itertools",
|
||||
"itertools 0.13.0",
|
||||
"num-traits",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
@@ -1047,12 +1047,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.6.0"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338"
|
||||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools",
|
||||
"itertools 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1285,11 +1285,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.10.0"
|
||||
version = "2.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
"async-channel 2.5.0",
|
||||
"async-channel 2.3.1",
|
||||
"async-imap",
|
||||
"async-native-tls",
|
||||
"async-smtp",
|
||||
@@ -1353,8 +1353,8 @@ dependencies = [
|
||||
"sha2",
|
||||
"shadowsocks",
|
||||
"smallvec",
|
||||
"strum 0.27.2",
|
||||
"strum_macros 0.27.2",
|
||||
"strum 0.27.1",
|
||||
"strum_macros 0.27.1",
|
||||
"tagger",
|
||||
"tempfile",
|
||||
"testdir",
|
||||
@@ -1395,10 +1395,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.10.0"
|
||||
version = "2.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
"async-channel 2.3.1",
|
||||
"base64",
|
||||
"deltachat",
|
||||
"deltachat-contact-tools",
|
||||
@@ -1417,7 +1417,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.10.0"
|
||||
version = "2.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1433,7 +1433,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.10.0"
|
||||
version = "2.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1462,7 +1462,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.10.0"
|
||||
version = "2.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1923,9 +1923,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.4"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||
checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2"
|
||||
dependencies = [
|
||||
"event-listener 5.4.0",
|
||||
"pin-project-lite",
|
||||
@@ -2557,9 +2557,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "human-panic"
|
||||
version = "2.0.3"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac63a746b187e95d51fe16850eb04d1cfef203f6af98e6c405a6f262ad3df00a"
|
||||
checksum = "80b84a66a325082740043a6c28bbea400c129eac0d3a27673a1de971e44bf1f7"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"os_info",
|
||||
@@ -2619,9 +2619,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.16"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
|
||||
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
@@ -3019,7 +3019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ca43045ceb44b913369f417d56323fb1628ebf482ab4c1e9360e81f1b58cbc2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
"async-channel 2.3.1",
|
||||
"bytes",
|
||||
"derive_more 1.0.0",
|
||||
"ed25519-dalek",
|
||||
@@ -3169,6 +3169,15 @@ dependencies = [
|
||||
"z32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
@@ -3817,7 +3826,7 @@ version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro-crate 2.0.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
@@ -5384,9 +5393,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.142"
|
||||
version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -5396,9 +5405,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.0.0"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -5717,9 +5726,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.2"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
@@ -5736,13 +5745,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.27.2"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
@@ -6063,9 +6073,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-io-timeout"
|
||||
version = "1.2.1"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bd86198d9ee903fedd2f9a2e72014287c0d9167e4ae43b5853007205dda1b76"
|
||||
checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
@@ -6176,17 +6186,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.4"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.7.0",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow 0.7.11",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.22.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6194,12 +6201,6 @@ name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -6211,7 +6212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_datetime",
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
|
||||
@@ -6222,32 +6223,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime 0.6.11",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"winnow 0.7.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
|
||||
dependencies = [
|
||||
"winnow 0.7.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.2"
|
||||
|
||||
+8
-8
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.10.0"
|
||||
version = "2.1.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.85"
|
||||
@@ -15,7 +15,7 @@ opt-level = 1
|
||||
# Make anyhow `backtrace` feature useful.
|
||||
# With `debug = 0` there are no line numbers in the backtrace
|
||||
# produced with RUST_BACKTRACE=1.
|
||||
debug = 1
|
||||
debug = 'full'
|
||||
opt-level = 0
|
||||
|
||||
[profile.fuzz]
|
||||
@@ -44,7 +44,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-imap = { version = "0.10.4", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
@@ -63,7 +63,7 @@ hickory-resolver = "0.25.2"
|
||||
http-body-util = "0.1.3"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.16"
|
||||
hyper-util = "0.1.14"
|
||||
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.35", default-features = false }
|
||||
@@ -101,13 +101,13 @@ strum_macros = "0.27"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.2"
|
||||
thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.1"
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
tokio-util = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.9"
|
||||
toml = "0.8"
|
||||
tracing = "0.1.41"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
@@ -116,7 +116,7 @@ blake3 = "1.8.2"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
||||
criterion = { version = "0.6.0", features = ["async_tokio"] }
|
||||
futures-lite = { workspace = true }
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
@@ -175,7 +175,7 @@ harness = false
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
async-channel = "2.5.0"
|
||||
async-channel = "2.3.1"
|
||||
base64 = "0.22"
|
||||
chrono = { version = "0.4.41", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
|
||||
@@ -80,41 +80,30 @@ Connect to your mail server (if already configured):
|
||||
> connect
|
||||
```
|
||||
|
||||
Export your public key to a vCard file:
|
||||
|
||||
```
|
||||
> make-vcard my.vcard 1
|
||||
```
|
||||
|
||||
Create contacts by address or vCard file:
|
||||
Create a contact:
|
||||
|
||||
```
|
||||
> addcontact yourfriends@email.org
|
||||
> import-vcard key-contact.vcard
|
||||
Command executed successfully.
|
||||
```
|
||||
|
||||
List contacts:
|
||||
|
||||
```
|
||||
> listcontacts
|
||||
Contact#Contact#11: key-contact@email.org <key-contact@email.org>
|
||||
Contact#Contact#Self: Me √ <your@email.org>
|
||||
2 key contacts.
|
||||
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
|
||||
1 address contacts.
|
||||
Contact#10: <name unset> <yourfriends@email.org>
|
||||
Contact#1: Me √√ <your@email.org>
|
||||
```
|
||||
|
||||
Create a chat with your friend and send a message:
|
||||
|
||||
```
|
||||
> createchat 10
|
||||
Single#Chat#12 created successfully.
|
||||
> chat 12
|
||||
Selecting chat Chat#12
|
||||
Single#Chat#12: yourfriends@email.org [yourfriends@email.org] Icon: profile-db-blobs/4138c52e5bc1c576cda7dd44d088c07.png
|
||||
0 messages.
|
||||
81.252µs to create this list, 123.625µs to mark all messages as noticed.
|
||||
Single#10 created successfully.
|
||||
> chat 10
|
||||
Single#10: yourfriends@email.org [yourfriends@email.org]
|
||||
> send hi
|
||||
Message sent.
|
||||
```
|
||||
|
||||
List messages when inside a chat:
|
||||
|
||||
@@ -78,27 +78,6 @@ All errors should be handled in one of these ways:
|
||||
- With `.log_err().ok()`.
|
||||
- Bubbled up with `?`.
|
||||
|
||||
When working with [async streams](https://docs.rs/futures/0.3.31/futures/stream/index.html),
|
||||
prefer [`try_next`](https://docs.rs/futures/0.3.31/futures/stream/trait.TryStreamExt.html#method.try_next) over `next()`, e.g. do
|
||||
```
|
||||
while let Some(event) = stream.try_next().await? {
|
||||
todo!();
|
||||
}
|
||||
```
|
||||
instead of
|
||||
```
|
||||
while let Some(event_res) = stream.next().await {
|
||||
todo!();
|
||||
}
|
||||
```
|
||||
as it allows bubbling up the error early with `?`
|
||||
with no way to accidentally skip error processing
|
||||
with early `continue` or `break`.
|
||||
Some streams reading from a connection
|
||||
return infinite number of `Some(Err(_))`
|
||||
items when connection breaks and not processing
|
||||
errors may result in infinite loop.
|
||||
|
||||
`backtrace` feature is enabled for `anyhow` crate
|
||||
and `debug = 1` option is set in the test profile.
|
||||
This allows to run `RUST_BACKTRACE=1 cargo test`
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.10.0"
|
||||
version = "2.1.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
+45
-64
@@ -503,6 +503,13 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
|
||||
* seconds. 2 days by default.
|
||||
* This is not supposed to be changed by UIs and only used for testing.
|
||||
* - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it
|
||||
* to 1 if it supports verified 1:1 chats.
|
||||
* Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified,
|
||||
* and when the key changes, an info message is posted into the chat.
|
||||
* 0=Nothing else happens when the key changes.
|
||||
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
|
||||
* until `dc_accept_chat()` is called.
|
||||
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
|
||||
* - `is_muted` = Whether a context is muted by the user.
|
||||
* Muted contexts should not sound, vibrate or show notifications.
|
||||
@@ -1332,14 +1339,12 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
|
||||
* Optionally, some special markers added to the ID array may help to
|
||||
* implement virtual lists.
|
||||
*
|
||||
* To get the concrete time of the message, use dc_array_get_timestamp().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param chat_id The chat ID of which the messages IDs should be queried.
|
||||
* @param flags If set to DC_GCM_ADDDAYMARKER, the marker DC_MSG_ID_DAYMARKER will
|
||||
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
|
||||
* The day marker timestamp is the midnight one for the corresponding (following) day in the local timezone.
|
||||
* To get the concrete time of the marker, use dc_array_get_timestamp().
|
||||
* If set to DC_GCM_INFO_ONLY, only system messages will be returned, can be combined with DC_GCM_ADDDAYMARKER.
|
||||
* @param marker1before Deprecated, set this to 0.
|
||||
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
|
||||
@@ -2089,19 +2094,9 @@ int dc_may_be_valid_addr (const char* addr);
|
||||
|
||||
|
||||
/**
|
||||
* Looks up a known and unblocked contact with a given e-mail address.
|
||||
* Check if an e-mail address belongs to a known and unblocked contact.
|
||||
* To get a list of all known and unblocked contacts, use dc_get_contacts().
|
||||
*
|
||||
* **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
|
||||
* (e.g. an address-contact and a key-contact),
|
||||
* this looks up the most recently seen contact,
|
||||
* i.e. which contact is returned depends on which contact last sent a message.
|
||||
* If the user just clicked on a mailto: link, then this is the best thing you can do.
|
||||
* But **DO NOT** internally represent contacts by their email address
|
||||
* and do not use this function to look them up;
|
||||
* otherwise this function will sometimes look up the wrong contact.
|
||||
* Instead, you should internally represent contacts by their ids.
|
||||
*
|
||||
* To validate an e-mail address independently of the contact database
|
||||
* use dc_may_be_valid_addr().
|
||||
*
|
||||
@@ -2123,13 +2118,6 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char*
|
||||
* To add a number of contacts, see dc_add_address_book() which is much faster for adding
|
||||
* a bunch of addresses.
|
||||
*
|
||||
* This will always create or look up an address-contact,
|
||||
* i.e. a contact identified by an email address,
|
||||
* with all messages sent to and from this contact being unencrypted.
|
||||
* If the user just clicked on an email address,
|
||||
* you should first check `lookup_contact_id_by_addr`,
|
||||
* and only if there is no contact yet, call this function here.
|
||||
*
|
||||
* May result in a #DC_EVENT_CONTACTS_CHANGED event.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -3830,12 +3818,21 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
||||
/**
|
||||
* Check if a chat is protected.
|
||||
*
|
||||
* Only verified contacts
|
||||
* End-to-end encryption is guaranteed in protected chats
|
||||
* and only verified contacts
|
||||
* as determined by dc_contact_is_verified()
|
||||
* can be added to protected chats.
|
||||
*
|
||||
* Protected chats are created using dc_create_group_chat()
|
||||
* by setting the 'protect' parameter to 1.
|
||||
* 1:1 chats become protected or unprotected automatically
|
||||
* if `verified_one_on_one_chats` setting is enabled.
|
||||
*
|
||||
* UI should display a green checkmark
|
||||
* in the chat title,
|
||||
* in the chatlist item
|
||||
* and in the chat profile
|
||||
* if chat protection is enabled.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
@@ -3872,8 +3869,6 @@ int dc_chat_is_encrypted (const dc_chat_t *chat);
|
||||
*
|
||||
* The UI should let the user confirm that this is OK with a message like
|
||||
* `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
|
||||
*
|
||||
* @deprecated 2025-07 chats protection cannot break any longer
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return 1=chat protection broken, 0=otherwise.
|
||||
@@ -4540,12 +4535,12 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
||||
* - DC_INFO_MEMBER_ADDED_TO_GROUP (4) - "Member CONTACT added by OTHER_CONTACT"
|
||||
* - DC_INFO_MEMBER_REMOVED_FROM_GROUP (5) - "Member CONTACT removed by OTHER_CONTACT"
|
||||
* - DC_INFO_EPHEMERAL_TIMER_CHANGED (10) - "Disappearing messages CHANGED_TO by CONTACT"
|
||||
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is protected"
|
||||
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
|
||||
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
|
||||
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
|
||||
* the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL
|
||||
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
|
||||
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
|
||||
* - DC_INFO_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted"
|
||||
*
|
||||
* For the messages that refer to a CONTACT,
|
||||
* dc_msg_get_info_contact_id() returns the contact ID.
|
||||
@@ -4598,10 +4593,9 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
|
||||
#define DC_INFO_LOCATION_ONLY 9
|
||||
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
|
||||
#define DC_INFO_PROTECTION_ENABLED 11
|
||||
#define DC_INFO_PROTECTION_DISABLED 12 // deprecated 2025-07
|
||||
#define DC_INFO_PROTECTION_DISABLED 12
|
||||
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
|
||||
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
||||
#define DC_INFO_CHAT_E2EE 50
|
||||
|
||||
|
||||
/**
|
||||
@@ -5272,14 +5266,20 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Check if the contact
|
||||
* can be added to protected chats.
|
||||
* can be added to verified chats,
|
||||
* i.e. has a verified key
|
||||
* and Autocrypt key matches the verified key.
|
||||
*
|
||||
* See dc_contact_get_verifier_id() for a guidance how to display these information.
|
||||
* If contact is verified
|
||||
* UI should display green checkmark after the contact name
|
||||
* in contact list items,
|
||||
* in chat member list items
|
||||
* and in profiles if no chat with the contact exist (otherwise, use dc_chat_is_protected()).
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return 0: contact is not verified.
|
||||
* 2: SELF and contact have verified their fingerprints in both directions.
|
||||
* 2: SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown.
|
||||
*/
|
||||
int dc_contact_is_verified (dc_contact_t* contact);
|
||||
|
||||
@@ -5310,22 +5310,16 @@ int dc_contact_is_key_contact (dc_contact_t* contact);
|
||||
/**
|
||||
* Return the contact ID that verified a contact.
|
||||
*
|
||||
* As verifier may be unknown,
|
||||
* use dc_contact_is_verified() to check if a contact can be added to a protected chat.
|
||||
* If the function returns non-zero result,
|
||||
* display green checkmark in the profile and "Introduced by ..." line
|
||||
* with the name and address of the contact
|
||||
* formatted by dc_contact_get_name_n_addr.
|
||||
*
|
||||
* UI should display the information in the contact's profile as follows:
|
||||
*
|
||||
* - If dc_contact_get_verifier_id() != 0,
|
||||
* display text "Introduced by ..."
|
||||
* with the name and address of the contact
|
||||
* formatted by dc_contact_get_name_n_addr().
|
||||
* Prefix the text by a green checkmark.
|
||||
*
|
||||
* - If dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() != 0,
|
||||
* display "Introduced" prefixed by a green checkmark.
|
||||
*
|
||||
* - if dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() == 0,
|
||||
* display nothing
|
||||
* If this function returns a verifier,
|
||||
* this does not necessarily mean
|
||||
* you can add the contact to verified chats.
|
||||
* Use dc_contact_is_verified() to check
|
||||
* if a contact can be added to a verified chat instead.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
@@ -6391,6 +6385,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/**
|
||||
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
* Or the verify state of a chat has changed.
|
||||
* See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
|
||||
* and dc_remove_contact_from_chat().
|
||||
*
|
||||
@@ -6903,7 +6898,9 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in summaries.
|
||||
#define DC_STR_GIF 23
|
||||
|
||||
/// @deprecated 2025-07, this string is no longer needed.
|
||||
/// "Encrypted message"
|
||||
///
|
||||
/// Used in subjects of outgoing messages.
|
||||
#define DC_STR_ENCRYPTEDMSG 24
|
||||
|
||||
/// "End-to-end encryption available."
|
||||
@@ -7598,18 +7595,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// `%2$s` will be replaced by name and address of the contact.
|
||||
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
|
||||
|
||||
/// "You set message deletion timer to 1 year."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_YEAR_BY_YOU 158
|
||||
|
||||
/// "Message deletion timer is set to 1 year by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_YEAR_BY_OTHER 159
|
||||
|
||||
/// "Scan to set up second device for %1$s"
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the account.
|
||||
@@ -7620,7 +7605,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as a device message after a successful backup transfer.
|
||||
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
|
||||
|
||||
/// "Messages are end-to-end encrypted."
|
||||
/// "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
///
|
||||
/// Used in info messages.
|
||||
#define DC_STR_CHAT_PROTECTION_ENABLED 170
|
||||
@@ -7628,7 +7613,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "%1$s sent a message from another device."
|
||||
///
|
||||
/// Used in info messages.
|
||||
/// @deprecated 2025-07
|
||||
#define DC_STR_CHAT_PROTECTION_DISABLED 171
|
||||
|
||||
/// "Others will only see this group after you sent a first message."
|
||||
@@ -7683,9 +7667,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// @deprecated 2025-06-05
|
||||
#define DC_STR_SECUREJOIN_TAKES_LONGER 192
|
||||
|
||||
/// "❤️ Seems you're enjoying Delta Chat!"… (donation request device message)
|
||||
#define DC_STR_DONATION_REQUEST 193
|
||||
|
||||
/// "Contact". Deprecated, currently unused.
|
||||
#define DC_STR_CONTACT 200
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.10.0"
|
||||
version = "2.1.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -953,7 +953,7 @@ impl CommandApi {
|
||||
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>())
|
||||
}
|
||||
|
||||
/// Create a new encrypted group chat (with key-contacts).
|
||||
/// Create a new group chat.
|
||||
///
|
||||
/// After creation,
|
||||
/// the group has one member with the ID DC_CONTACT_ID_SELF
|
||||
@@ -971,24 +971,14 @@ impl CommandApi {
|
||||
///
|
||||
/// @param protect If set to 1 the function creates group with protection initially enabled.
|
||||
/// Only verified members are allowed in these groups
|
||||
/// and end-to-end-encryption is always enabled.
|
||||
async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let protect = match protect {
|
||||
true => ProtectionStatus::Protected,
|
||||
false => ProtectionStatus::Unprotected,
|
||||
};
|
||||
chat::create_group_ex(&ctx, Some(protect), &name)
|
||||
.await
|
||||
.map(|id| id.to_u32())
|
||||
}
|
||||
|
||||
/// Create a new unencrypted group chat.
|
||||
///
|
||||
/// Same as [`Self::create_group_chat`], but the chat is unencrypted and can only have
|
||||
/// address-contacts.
|
||||
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::create_group_ex(&ctx, None, &name)
|
||||
chat::create_group_chat(&ctx, protect, &name)
|
||||
.await
|
||||
.map(|id| id.to_u32())
|
||||
}
|
||||
@@ -1227,10 +1217,8 @@ impl CommandApi {
|
||||
}
|
||||
|
||||
/// Returns all messages of a particular chat.
|
||||
///
|
||||
/// * `add_daymarker` - If `true`, add day markers as `DC_MSG_ID_DAYMARKER` to the result,
|
||||
/// e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
|
||||
/// corresponding (following) day in the local timezone.
|
||||
/// If `add_daymarker` is `true`, it will return them as
|
||||
/// `DC_MSG_ID_DAYMARKER`, e.g. [1234, 1237, 9, 1239].
|
||||
async fn get_message_ids(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1473,14 +1461,7 @@ impl CommandApi {
|
||||
|
||||
/// Add a single contact as a result of an explicit user action.
|
||||
///
|
||||
/// This will always create or look up an address-contact,
|
||||
/// i.e. a contact identified by an email address,
|
||||
/// with all messages sent to and from this contact being unencrypted.
|
||||
/// If the user just clicked on an email address,
|
||||
/// you should first check [`Self::lookup_contact_id_by_addr`]/`lookupContactIdByAddr.`,
|
||||
/// and only if there is no contact yet, call this function here.
|
||||
///
|
||||
/// Returns contact id of the created or existing contact.
|
||||
/// Returns contact id of the created or existing contact
|
||||
async fn create_contact(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1630,19 +1611,9 @@ impl CommandApi {
|
||||
Contact::get_encrinfo(&ctx, ContactId::new(contact_id)).await
|
||||
}
|
||||
|
||||
/// Looks up a known and unblocked contact with a given e-mail address.
|
||||
/// Check if an e-mail address belongs to a known and unblocked contact.
|
||||
/// To get a list of all known and unblocked contacts, use contacts_get_contacts().
|
||||
///
|
||||
/// **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
|
||||
/// (e.g. an address-contact and a key-contact),
|
||||
/// this looks up the most recently seen contact,
|
||||
/// i.e. which contact is returned depends on which contact last sent a message.
|
||||
/// If the user just clicked on a mailto: link, then this is the best thing you can do.
|
||||
/// But **DO NOT** internally represent contacts by their email address
|
||||
/// and do not use this function to look them up;
|
||||
/// otherwise this function will sometimes look up the wrong contact.
|
||||
/// Instead, you should internally represent contacts by their ids.
|
||||
///
|
||||
/// To validate an e-mail address independently of the contact database
|
||||
/// use check_email_validity().
|
||||
async fn lookup_contact_id_by_addr(
|
||||
|
||||
@@ -21,16 +21,15 @@ pub struct FullChat {
|
||||
|
||||
/// True if the chat is protected.
|
||||
///
|
||||
/// Only verified contacts
|
||||
/// as determined by [`ContactObject::is_verified`] / `Contact.isVerified`
|
||||
/// can be added to protected chats.
|
||||
///
|
||||
/// Protected chats are created using [`create_group_chat`] / `createGroupChat()`
|
||||
/// by setting the 'protect' parameter to true.
|
||||
///
|
||||
/// [`create_group_chat`]: crate::api::CommandApi::create_group_chat
|
||||
/// UI should display a green checkmark
|
||||
/// in the chat title,
|
||||
/// in the chat profile title and
|
||||
/// in the chatlist item
|
||||
/// if chat protection is enabled.
|
||||
/// UI should also display a green checkmark
|
||||
/// in the contact profile
|
||||
/// if 1:1 chat with this contact exists and is protected.
|
||||
is_protected: bool,
|
||||
|
||||
/// True if the chat is encrypted.
|
||||
/// This means that all messages in the chat are encrypted,
|
||||
/// and all contacts in the chat are "key-contacts",
|
||||
@@ -71,9 +70,7 @@ pub struct FullChat {
|
||||
fresh_message_counter: usize,
|
||||
// is_group - please check over chat.type in frontend instead
|
||||
is_contact_request: bool,
|
||||
/// Deprecated 2025-07. Chats protection cannot break any longer.
|
||||
is_protection_broken: bool,
|
||||
|
||||
is_device_chat: bool,
|
||||
self_in_group: bool,
|
||||
is_muted: bool,
|
||||
@@ -218,9 +215,7 @@ pub struct BasicChat {
|
||||
is_self_talk: bool,
|
||||
color: String,
|
||||
is_contact_request: bool,
|
||||
/// Deprecated 2025-07. Chats protection cannot break any longer.
|
||||
is_protection_broken: bool,
|
||||
|
||||
is_device_chat: bool,
|
||||
is_muted: bool,
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ pub enum ChatListItemFetchResult {
|
||||
name: String,
|
||||
avatar_path: Option<String>,
|
||||
color: String,
|
||||
chat_type: u32,
|
||||
last_updated: Option<i64>,
|
||||
summary_text1: String,
|
||||
summary_text2: String,
|
||||
@@ -55,7 +54,6 @@ pub enum ChatListItemFetchResult {
|
||||
///
|
||||
/// See also `is_key_contact` on `Contact`.
|
||||
is_encrypted: bool,
|
||||
/// deprecated 2025-07, use chat_type instead
|
||||
is_group: bool,
|
||||
fresh_message_counter: usize,
|
||||
is_self_talk: bool,
|
||||
@@ -66,6 +64,10 @@ pub enum ChatListItemFetchResult {
|
||||
is_pinned: bool,
|
||||
is_muted: bool,
|
||||
is_contact_request: bool,
|
||||
/// Deprecated 2025-07, alias for is_out_broadcast
|
||||
is_broadcast: bool,
|
||||
/// true if the chat type is OutBroadcast
|
||||
is_out_broadcast: bool,
|
||||
/// contact id if this is a dm chat (for view profile entry in context menu)
|
||||
dm_chat_contact: Option<u32>,
|
||||
was_seen_recently: bool,
|
||||
@@ -155,7 +157,6 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
name: chat.get_name().to_owned(),
|
||||
avatar_path,
|
||||
color,
|
||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||
last_updated,
|
||||
summary_text1,
|
||||
summary_text2,
|
||||
@@ -173,6 +174,8 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
is_pinned: visibility == ChatVisibility::Pinned,
|
||||
is_muted: chat.is_muted(),
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
is_broadcast: chat.get_type() == Chattype::OutBroadcast,
|
||||
is_out_broadcast: chat.get_type() == Chattype::OutBroadcast,
|
||||
dm_chat_contact,
|
||||
was_seen_recently,
|
||||
last_message_type: message_type,
|
||||
|
||||
@@ -31,11 +31,13 @@ pub struct ContactObject {
|
||||
/// e.g. if we just scanned the fingerprint from a QR code.
|
||||
e2ee_avail: bool,
|
||||
|
||||
/// True if the contact
|
||||
/// can be added to protected chats
|
||||
/// because SELF and contact have verified their fingerprints in both directions.
|
||||
/// True if the contact can be added to verified groups.
|
||||
///
|
||||
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
|
||||
/// If this is true
|
||||
/// UI should display green checkmark after the contact name
|
||||
/// in contact list items,
|
||||
/// in chat member list items
|
||||
/// and in profiles if no chat with the contact exist.
|
||||
is_verified: bool,
|
||||
|
||||
/// True if the contact profile title should have a green checkmark.
|
||||
@@ -44,29 +46,12 @@ pub struct ContactObject {
|
||||
/// or will have a green checkmark if created.
|
||||
is_profile_verified: bool,
|
||||
|
||||
/// The contact ID that verified a contact.
|
||||
/// The ID of the contact that verified this contact.
|
||||
///
|
||||
/// As verifier may be unknown,
|
||||
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
|
||||
///
|
||||
/// UI should display the information in the contact's profile as follows:
|
||||
///
|
||||
/// - If `verifierId` != 0,
|
||||
/// display text "Introduced by ..."
|
||||
/// with the name and address of the contact
|
||||
/// formatted by `name_and_addr`/`nameAndAddr`.
|
||||
/// Prefix the text by a green checkmark.
|
||||
///
|
||||
/// - If `verifierId` == 0 and `isVerified` != 0,
|
||||
/// display "Introduced" prefixed by a green checkmark.
|
||||
///
|
||||
/// - if `verifierId` == 0 and `isVerified` == 0,
|
||||
/// display nothing
|
||||
///
|
||||
/// This contains the contact ID of the verifier.
|
||||
/// If it is `DC_CONTACT_ID_SELF`, we verified the contact ourself.
|
||||
/// If it is None/Null, we don't have verifier information or
|
||||
/// the contact is not verified.
|
||||
/// If this is present,
|
||||
/// display a green checkmark and "Introduced by ..."
|
||||
/// string followed by the verifier contact name and address
|
||||
/// in the contact profile.
|
||||
verifier_id: Option<u32>,
|
||||
|
||||
/// the contact's last seen timestamp
|
||||
|
||||
@@ -224,6 +224,7 @@ pub enum EventType {
|
||||
},
|
||||
|
||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
/// Or the verify state of a chat has changed.
|
||||
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
||||
/// and removeContactFromChat().
|
||||
///
|
||||
|
||||
@@ -416,9 +416,6 @@ pub enum SystemMessageType {
|
||||
/// Chat ephemeral message timer is changed.
|
||||
EphemeralTimerChanged,
|
||||
|
||||
// Chat is e2ee
|
||||
ChatE2ee,
|
||||
|
||||
// Chat protection state changed
|
||||
ChatProtectionEnabled,
|
||||
ChatProtectionDisabled,
|
||||
@@ -453,7 +450,6 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
|
||||
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
|
||||
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
|
||||
SystemMessage::ChatE2ee => SystemMessageType::ChatE2ee,
|
||||
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
|
||||
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
|
||||
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.10.0"
|
||||
"version": "2.1.0"
|
||||
}
|
||||
|
||||
@@ -95,10 +95,8 @@ describe("online tests", function () {
|
||||
false,
|
||||
);
|
||||
|
||||
// There are 2 messages in the chat:
|
||||
// 'Messages are end-to-end encrypted' (info message) and 'Hello'
|
||||
expect(messageList).have.length(2);
|
||||
const message = await dc.rpc.getMessage(accountId2, messageList[1]);
|
||||
expect(messageList).have.length(1);
|
||||
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
|
||||
expect(message.text).equal("Hello");
|
||||
expect(message.showPadlock).equal(true);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.10.0"
|
||||
version = "2.1.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -87,7 +87,7 @@ async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
||||
let data = read_file(context, filename).await?;
|
||||
|
||||
if let Err(err) = receive_imf(context, &data, false).await {
|
||||
eprintln!("receive_imf errored: {err:?}");
|
||||
println!("receive_imf errored: {err:?}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -403,8 +403,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
block <contact-id>\n\
|
||||
unblock <contact-id>\n\
|
||||
listblocked\n\
|
||||
import-vcard <file>\n\
|
||||
make-vcard <file> <contact-id> [contact-id ...]\n\
|
||||
======================================Misc.==\n\
|
||||
getqr [<chat-id>]\n\
|
||||
getqrsvg [<chat-id>]\n\
|
||||
@@ -623,7 +621,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!("Location streaming enabled.");
|
||||
}
|
||||
println!("{cnt} chats");
|
||||
eprintln!("{time_needed:?} to create this list");
|
||||
println!("{time_needed:?} to create this list");
|
||||
}
|
||||
"start-realtime" => {
|
||||
if arg1.is_empty() {
|
||||
@@ -733,7 +731,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
|
||||
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
|
||||
|
||||
eprintln!(
|
||||
println!(
|
||||
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
|
||||
);
|
||||
}
|
||||
@@ -987,7 +985,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
},
|
||||
query,
|
||||
);
|
||||
eprintln!("{time_needed:?} to create this list");
|
||||
println!("{time_needed:?} to create this list");
|
||||
}
|
||||
"draft" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
@@ -1153,10 +1151,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"listcontacts" | "contacts" => {
|
||||
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
println!("{} key contacts.", contacts.len());
|
||||
let addrcontacts = Contact::get_all(&context, DC_GCL_ADDRESS, Some(arg1)).await?;
|
||||
log_contactlist(&context, &addrcontacts).await?;
|
||||
println!("{} address contacts.", addrcontacts.len());
|
||||
println!("{} contacts.", contacts.len());
|
||||
}
|
||||
"addcontact" => {
|
||||
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
|
||||
@@ -1220,24 +1215,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
println!("{} blocked contacts.", contacts.len());
|
||||
}
|
||||
"import-vcard" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||
let vcard_content = fs::read_to_string(&arg1.to_string()).await?;
|
||||
let contacts = import_vcard(&context, &vcard_content).await?;
|
||||
println!("vCard contacts imported:");
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
}
|
||||
"make-vcard" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||
ensure!(!arg2.is_empty(), "Argument <contact-id> missing.");
|
||||
let mut contact_ids = vec![];
|
||||
for x in arg2.split_whitespace() {
|
||||
contact_ids.push(ContactId::new(x.parse()?))
|
||||
}
|
||||
let vcard_content = make_vcard(&context, &contact_ids).await?;
|
||||
fs::write(&arg1.to_string(), vcard_content).await?;
|
||||
println!("vCard written to: {arg1}");
|
||||
}
|
||||
"checkqr" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
let qr = check_qr(&context, arg1).await?;
|
||||
@@ -1247,7 +1224,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
match set_config_from_qr(&context, arg1).await {
|
||||
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
|
||||
Err(err) => eprintln!("Cannot set config from QR code: {err:?}"),
|
||||
Err(err) => println!("Cannot set config from QR code: {err:?}"),
|
||||
}
|
||||
}
|
||||
"createqrsvg" => {
|
||||
|
||||
@@ -232,7 +232,7 @@ const MESSAGE_COMMANDS: [&str; 10] = [
|
||||
"delmsg",
|
||||
"react",
|
||||
];
|
||||
const CONTACT_COMMANDS: [&str; 9] = [
|
||||
const CONTACT_COMMANDS: [&str; 7] = [
|
||||
"listcontacts",
|
||||
"addcontact",
|
||||
"contactinfo",
|
||||
@@ -240,8 +240,6 @@ const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"block",
|
||||
"unblock",
|
||||
"listblocked",
|
||||
"import-vcard",
|
||||
"make-vcard",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 14] = [
|
||||
"getqr",
|
||||
@@ -313,7 +311,7 @@ impl Validator for DcHelper {}
|
||||
|
||||
async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
if args.len() < 2 {
|
||||
eprintln!("Error: Bad arguments, expected [db-name].");
|
||||
println!("Error: Bad arguments, expected [db-name].");
|
||||
bail!("No db-name specified");
|
||||
}
|
||||
let context = ContextBuilder::new(args[1].clone().into())
|
||||
@@ -368,7 +366,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
false
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Error: {err:#}");
|
||||
println!("Error: {err:#}");
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -383,7 +381,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Error: {err:#}");
|
||||
println!("Error: {err:#}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.10.0"
|
||||
version = "2.1.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -185,21 +185,7 @@ class Account:
|
||||
return Contact(self, contact_id)
|
||||
|
||||
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
||||
"""Looks up a known and unblocked contact with a given e-mail address.
|
||||
To get a list of all known and unblocked contacts, use contacts_get_contacts().
|
||||
|
||||
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
|
||||
(e.g. an address-contact and a key-contact),
|
||||
this looks up the most recently seen contact,
|
||||
i.e. which contact is returned depends on which contact last sent a message.
|
||||
If the user just clicked on a mailto: link, then this is the best thing you can do.
|
||||
But **DO NOT** internally represent contacts by their email address
|
||||
and do not use this function to look them up;
|
||||
otherwise this function will sometimes look up the wrong contact.
|
||||
Instead, you should internally represent contacts by their ids.
|
||||
|
||||
To validate an e-mail address independently of the contact database
|
||||
use check_email_validity()."""
|
||||
"""Check if an e-mail address belongs to a known and unblocked contact."""
|
||||
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||
return contact_id and Contact(self, contact_id)
|
||||
|
||||
|
||||
@@ -13,12 +13,6 @@ from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Messag
|
||||
from ._utils import futuremethod
|
||||
from .rpc import Rpc
|
||||
|
||||
E2EE_INFO_MSGS = 1
|
||||
"""
|
||||
The number of info messages added to new e2ee chats.
|
||||
Currently this is "End-to-end encryption available".
|
||||
"""
|
||||
|
||||
|
||||
class ACFactory:
|
||||
"""Test account factory."""
|
||||
|
||||
@@ -36,9 +36,6 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
|
||||
# Second client receives only second message, but not the first.
|
||||
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import pytest
|
||||
|
||||
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||
from deltachat_rpc_client.const import ChatType, DownloadState, MessageState
|
||||
from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@@ -171,10 +170,7 @@ def test_account(acfactory) -> None:
|
||||
assert alice.get_size()
|
||||
assert alice.is_configured()
|
||||
assert not alice.get_avatar()
|
||||
# get_contact_by_addr() can lookup a key contact by address:
|
||||
bob_contact = alice.get_contact_by_addr(bob_addr).get_snapshot()
|
||||
assert bob_contact.display_name == "Bob"
|
||||
assert bob_contact.is_key_contact
|
||||
assert alice.get_contact_by_addr(bob_addr) is None # There is no address-contact, only key-contact
|
||||
assert alice.get_contacts()
|
||||
assert alice.get_contacts(snapshot=True)
|
||||
assert alice.self_contact
|
||||
@@ -461,12 +457,8 @@ def test_wait_next_messages(acfactory) -> None:
|
||||
alice_chat_bot.send_text("Hello!")
|
||||
|
||||
next_messages = next_messages_task.result()
|
||||
|
||||
if len(next_messages) == E2EE_INFO_MSGS:
|
||||
next_messages += bot.wait_next_messages()
|
||||
|
||||
assert len(next_messages) == 1 + E2EE_INFO_MSGS
|
||||
snapshot = next_messages[0 + E2EE_INFO_MSGS].get_snapshot()
|
||||
assert len(next_messages) == 1
|
||||
snapshot = next_messages[0].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.10.0"
|
||||
version = "2.1.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.10.0"
|
||||
"version": "2.1.0"
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ skip = [
|
||||
{ name = "lru", version = "0.12.3" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
{ name = "nom", version = "7.1.3" },
|
||||
{ name = "proc-macro-crate", version = "2.0.0" },
|
||||
{ name = "rand_chacha", version = "0.3.1" },
|
||||
{ name = "rand_core", version = "0.6.4" },
|
||||
{ name = "rand", version = "0.8.5" },
|
||||
@@ -48,7 +49,7 @@ skip = [
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "thiserror-impl", version = "1.0.69" },
|
||||
{ name = "thiserror", version = "1.0.69" },
|
||||
{ name = "toml_datetime", version = "0.6.11" },
|
||||
{ name = "toml_edit", version = "0.20.7" },
|
||||
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
|
||||
{ name = "windows" },
|
||||
{ name = "windows_aarch64_gnullvm" },
|
||||
@@ -66,6 +67,7 @@ skip = [
|
||||
{ name = "windows_x86_64_gnu" },
|
||||
{ name = "windows_x86_64_gnullvm" },
|
||||
{ name = "windows_x86_64_msvc" },
|
||||
{ name = "winnow", version = "0.5.40" },
|
||||
{ name = "zerocopy", version = "0.7.32" },
|
||||
]
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
bolero = "0.13.4"
|
||||
bolero = "0.13.3"
|
||||
|
||||
[dependencies]
|
||||
mailparse = { workspace = true }
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.10.0"
|
||||
version = "2.1.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
@@ -330,21 +330,7 @@ class Account:
|
||||
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
||||
|
||||
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
|
||||
"""Looks up a known and unblocked contact with a given e-mail address.
|
||||
To get a list of all known and unblocked contacts, use contacts_get_contacts().
|
||||
|
||||
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
|
||||
(e.g. an address-contact and a key-contact),
|
||||
this looks up the most recently seen contact,
|
||||
i.e. which contact is returned depends on which contact last sent a message.
|
||||
If the user just clicked on a mailto: link, then this is the best thing you can do.
|
||||
But **DO NOT** internally represent contacts by their email address
|
||||
and do not use this function to look them up;
|
||||
otherwise this function will sometimes look up the wrong contact.
|
||||
Instead, you should internally represent contacts by their ids.
|
||||
|
||||
To validate an e-mail address independently of the contact database
|
||||
use check_email_validity()."""
|
||||
"""get a contact for the email address or None if it's blocked or doesn't exist."""
|
||||
_, addr = parseaddr(email)
|
||||
addr = as_dc_charpointer(addr)
|
||||
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
|
||||
|
||||
@@ -20,12 +20,6 @@ import deltachat
|
||||
from . import Account, account_hookimpl, const, get_core_info
|
||||
from .events import FFIEventLogger, FFIEventTracker
|
||||
|
||||
E2EE_INFO_MSGS = 1
|
||||
"""
|
||||
The number of info messages added to new e2ee chats.
|
||||
Currently this is "End-to-end encryption available".
|
||||
"""
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("deltachat testplugin options")
|
||||
@@ -612,7 +606,7 @@ class ACFactory:
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg is not None
|
||||
assert msg.text == "Messages are end-to-end encrypted."
|
||||
assert msg.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg is not None
|
||||
assert "Member Me " in msg.text and " added by " in msg.text
|
||||
|
||||
@@ -133,7 +133,8 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
assert "added" in msg.text.lower()
|
||||
|
||||
assert any(
|
||||
m.is_system_message() and m.text == "Messages are end-to-end encrypted." for m in msg.chat.get_messages()
|
||||
m.is_system_message() and m.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
for m in msg.chat.get_messages()
|
||||
)
|
||||
lp.sec("ac1: send message")
|
||||
msg_out = chat1.send_text("hello")
|
||||
@@ -337,7 +338,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
assert contact.addr == ac1.get_config("addr")
|
||||
chat2 = msg_in.chat
|
||||
assert chat2.is_protected()
|
||||
assert chat2.get_messages()[0].text == "Messages are end-to-end encrypted."
|
||||
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
|
||||
|
||||
lp.sec("ac2_offl: sending message")
|
||||
@@ -411,7 +412,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2_offl.get_message_by_id(ev.data2)
|
||||
assert msg_in.is_system_message()
|
||||
assert msg_in.text == "Messages are end-to-end encrypted."
|
||||
assert msg_in.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
|
||||
# We need to consume one event that has data2=0
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
|
||||
@@ -10,7 +10,6 @@ from imap_tools import AND, U
|
||||
import deltachat as dc
|
||||
from deltachat import account_hookimpl, Message
|
||||
from deltachat.tracker import ImexTracker
|
||||
from deltachat.testplugin import E2EE_INFO_MSGS
|
||||
|
||||
|
||||
def test_basic_imap_api(acfactory, tmp_path):
|
||||
@@ -409,10 +408,6 @@ def test_forward_messages(acfactory, lp):
|
||||
msg_out = chat.send_text("message2")
|
||||
|
||||
lp.sec("ac2: wait for receive")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2.get_message_by_id(ev.data2)
|
||||
assert msg_in.text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
assert ev.data2 == msg_out.id
|
||||
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||
@@ -627,11 +622,6 @@ def test_moved_markseen(acfactory):
|
||||
|
||||
with ac2.direct_imap.idle() as idle2:
|
||||
ac2.start_io()
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg.text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
|
||||
@@ -748,7 +738,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
lp.sec("sending text message from ac1 to ac2")
|
||||
msg_out = chat.send_text("message1")
|
||||
|
||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
assert len(chat.get_messages()) == 1
|
||||
|
||||
lp.sec("disable ac1 MDNs")
|
||||
ac1.set_config("mdns_enabled", "0")
|
||||
@@ -756,7 +746,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
assert len(msg.chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
assert len(msg.chat.get_messages()) == 1
|
||||
|
||||
lp.sec("ac2: mark incoming message as seen")
|
||||
ac2.mark_seen_messages([msg])
|
||||
@@ -765,7 +755,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
# MDN should be moved even though MDNs are already disabled
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
assert len(chat.get_messages()) == 1
|
||||
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
|
||||
@@ -1133,11 +1123,6 @@ def test_send_and_receive_image(acfactory, lp, data):
|
||||
assert m == msg_out
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG")
|
||||
msg_in = ac2.get_message_by_id(ev.data2)
|
||||
assert msg_in.text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 == msg_out.id
|
||||
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||
@@ -1173,10 +1158,10 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
assert contact2.addr == some1_addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 3 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert messages[1 + E2EE_INFO_MSGS].filemime == "image/png"
|
||||
assert os.stat(messages[1 + E2EE_INFO_MSGS].filename).st_size == os.stat(original_image_path).st_size
|
||||
assert len(messages) == 3
|
||||
assert messages[0].text == "msg1"
|
||||
assert messages[1].filemime == "image/png"
|
||||
assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size
|
||||
ac.set_config("displayname", "new displayname")
|
||||
assert ac.get_config("displayname") == "new displayname"
|
||||
|
||||
@@ -1429,8 +1414,8 @@ def test_connectivity(acfactory, lp):
|
||||
ac1.maybe_network()
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
msgs = ac1.create_chat(ac2).get_messages()
|
||||
assert len(msgs) == 1 + E2EE_INFO_MSGS
|
||||
assert msgs[0 + E2EE_INFO_MSGS].text == "Hi"
|
||||
assert len(msgs) == 1
|
||||
assert msgs[0].text == "Hi"
|
||||
|
||||
lp.sec("Test that the connectivity changes to WORKING while new messages are fetched")
|
||||
|
||||
@@ -1440,8 +1425,8 @@ def test_connectivity(acfactory, lp):
|
||||
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
msgs = ac1.create_chat(ac2).get_messages()
|
||||
assert len(msgs) == 2 + E2EE_INFO_MSGS
|
||||
assert msgs[1 + E2EE_INFO_MSGS].text == "Hi 2"
|
||||
assert len(msgs) == 2
|
||||
assert msgs[1].text == "Hi 2"
|
||||
|
||||
|
||||
def test_fetch_deleted_msg(acfactory, lp):
|
||||
|
||||
@@ -6,7 +6,6 @@ import pytest
|
||||
import deltachat as dc
|
||||
from deltachat.tracker import ImexFailed
|
||||
from deltachat import Account, Message
|
||||
from deltachat.testplugin import E2EE_INFO_MSGS
|
||||
|
||||
|
||||
class TestOfflineAccountBasic:
|
||||
@@ -462,9 +461,9 @@ class TestOfflineChat:
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_import_export_on_encrypted_acct(self, acfactory, tmp_path):
|
||||
passphrase1 = "passphrase1"
|
||||
@@ -501,9 +500,9 @@ class TestOfflineChat:
|
||||
contact2_addr = contact2.addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
ac2.shutdown()
|
||||
|
||||
@@ -518,9 +517,9 @@ class TestOfflineChat:
|
||||
assert contact2.addr == contact2_addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_import_export_with_passphrase(self, acfactory, tmp_path):
|
||||
passphrase = "test_passphrase"
|
||||
@@ -558,9 +557,9 @@ class TestOfflineChat:
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
|
||||
"""
|
||||
@@ -604,9 +603,9 @@ class TestOfflineChat:
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
ac2.shutdown()
|
||||
|
||||
@@ -621,9 +620,9 @@ class TestOfflineChat:
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_set_get_draft(self, chat1):
|
||||
msg1 = Message.new_empty(chat1.account, "text")
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2025-08-04
|
||||
2025-07-11
|
||||
+2
-8
@@ -353,11 +353,11 @@ impl Accounts {
|
||||
/// This is an auxiliary function and not part of public API.
|
||||
/// Use [Accounts::background_fetch] instead.
|
||||
async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
|
||||
let n_accounts = accounts.len();
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::Info(format!(
|
||||
"Starting background fetch for {n_accounts} accounts."
|
||||
"Starting background fetch for {} accounts.",
|
||||
accounts.len()
|
||||
)),
|
||||
});
|
||||
let mut set = JoinSet::new();
|
||||
@@ -369,12 +369,6 @@ impl Accounts {
|
||||
});
|
||||
}
|
||||
set.join_all().await;
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::Info(format!(
|
||||
"Finished background fetch for {n_accounts} accounts."
|
||||
)),
|
||||
});
|
||||
}
|
||||
|
||||
/// Auxiliary function for [Accounts::background_fetch].
|
||||
|
||||
+109
-147
@@ -33,7 +33,6 @@ use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
|
||||
use crate::events::EventType;
|
||||
use crate::location;
|
||||
use crate::log::{LogExt, error, info, warn};
|
||||
use crate::logged_debug_assert;
|
||||
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
@@ -43,9 +42,9 @@ use crate::smtp::send_msg_to_smtp;
|
||||
use crate::stock_str;
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{
|
||||
IsNoneOrEmpty, SystemTime, buf_compress, create_id, create_outgoing_rfc724_mid,
|
||||
create_smeared_timestamp, create_smeared_timestamps, get_abs_path, gm2local_offset,
|
||||
smeared_time, time, truncate_msg_text,
|
||||
IsNoneOrEmpty, SystemTime, buf_compress, create_broadcast_shared_secret, create_id,
|
||||
create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps, get_abs_path,
|
||||
gm2local_offset, smeared_time, time, truncate_msg_text,
|
||||
};
|
||||
use crate::webxdc::StatusUpdateSerial;
|
||||
use crate::{chatlist_events, imap};
|
||||
@@ -94,12 +93,14 @@ pub enum ProtectionStatus {
|
||||
///
|
||||
/// All members of the chat must be verified.
|
||||
Protected = 1,
|
||||
// `2` was never used as a value.
|
||||
|
||||
// Chats don't break in Core v2 anymore. Chats with broken protection existing before the
|
||||
// key-contacts migration are treated as `Unprotected`.
|
||||
//
|
||||
// ProtectionBroken = 3,
|
||||
/// The chat was protected, but now a new message came in
|
||||
/// which was not encrypted / signed correctly.
|
||||
/// The user has to confirm that this is OK.
|
||||
///
|
||||
/// We only do this in 1:1 chats; in group chats, the chat just
|
||||
/// stays protected.
|
||||
ProtectionBroken = 3, // `2` was never used as a value.
|
||||
}
|
||||
|
||||
/// The reason why messages cannot be sent to the chat.
|
||||
@@ -116,6 +117,10 @@ pub(crate) enum CantSendReason {
|
||||
/// The chat is a contact request, it needs to be accepted before sending a message.
|
||||
ContactRequest,
|
||||
|
||||
/// The chat was protected, but now a new message came in
|
||||
/// which was not encrypted / signed correctly.
|
||||
ProtectionBroken,
|
||||
|
||||
/// Mailing list without known List-Post header.
|
||||
ReadOnlyMailingList,
|
||||
|
||||
@@ -138,6 +143,10 @@ impl fmt::Display for CantSendReason {
|
||||
f,
|
||||
"contact request chat should be accepted before sending messages"
|
||||
),
|
||||
Self::ProtectionBroken => write!(
|
||||
f,
|
||||
"accept that the encryption isn't verified anymore before sending messages"
|
||||
),
|
||||
Self::ReadOnlyMailingList => {
|
||||
write!(f, "mailing list does not have a know post address")
|
||||
}
|
||||
@@ -339,8 +348,6 @@ impl ChatId {
|
||||
chat_id
|
||||
.add_protection_msg(context, ProtectionStatus::Protected, None, timestamp)
|
||||
.await?;
|
||||
} else {
|
||||
chat_id.maybe_add_encrypted_msg(context, timestamp).await?;
|
||||
}
|
||||
|
||||
info!(
|
||||
@@ -469,6 +476,16 @@ impl ChatId {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
|
||||
match chat.typ {
|
||||
Chattype::Single
|
||||
if chat.blocked == Blocked::Not
|
||||
&& chat.protected == ProtectionStatus::ProtectionBroken =>
|
||||
{
|
||||
// The protection was broken, then the user clicked 'Accept'/'OK',
|
||||
// so, now we want to set the status to Unprotected again:
|
||||
chat.id
|
||||
.inner_set_protection(context, ProtectionStatus::Unprotected)
|
||||
.await?;
|
||||
}
|
||||
Chattype::Single | Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast => {
|
||||
// User has "created a chat" with all these contacts.
|
||||
//
|
||||
@@ -525,7 +542,7 @@ impl ChatId {
|
||||
| Chattype::InBroadcast => {}
|
||||
Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
|
||||
},
|
||||
ProtectionStatus::Unprotected => {}
|
||||
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
|
||||
};
|
||||
|
||||
context
|
||||
@@ -568,6 +585,7 @@ impl ChatId {
|
||||
let cmd = match protect {
|
||||
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
|
||||
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
|
||||
ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
|
||||
};
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
@@ -585,42 +603,6 @@ impl ChatId {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds message "Messages are end-to-end encrypted" if appropriate.
|
||||
///
|
||||
/// This function is rather slow because it does a lot of database queries,
|
||||
/// but this is fine because it is only called on chat creation.
|
||||
async fn maybe_add_encrypted_msg(self, context: &Context, timestamp_sort: i64) -> Result<()> {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
|
||||
// as secure-join adds its own message on success (after some other messasges),
|
||||
// we do not want to add "Messages are end-to-end encrypted" on chat creation.
|
||||
// we detect secure join by `can_send` (for Bob, scanner side) and by `blocked` (for Alice, inviter side) below.
|
||||
if !chat.is_encrypted(context).await?
|
||||
|| self <= DC_CHAT_ID_LAST_SPECIAL
|
||||
|| chat.is_device_talk()
|
||||
|| chat.is_self_talk()
|
||||
|| (!chat.can_send(context).await? && !chat.is_contact_request())
|
||||
|| chat.blocked == Blocked::Yes
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let text = stock_str::messages_e2e_encrypted(context).await;
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
self,
|
||||
&text,
|
||||
SystemMessage::ChatE2ee,
|
||||
timestamp_sort,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets protection and adds a message.
|
||||
///
|
||||
/// `timestamp_sort` is used as the timestamp of the added message
|
||||
@@ -1357,18 +1339,14 @@ impl ChatId {
|
||||
|
||||
let mut ret = stock_str::e2e_available(context).await + "\n";
|
||||
|
||||
for &contact_id in get_chat_contacts(context, self)
|
||||
for contact_id in get_chat_contacts(context, self)
|
||||
.await?
|
||||
.iter()
|
||||
.filter(|&contact_id| !contact_id.is_special())
|
||||
{
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
let addr = contact.get_addr();
|
||||
logged_debug_assert!(
|
||||
context,
|
||||
contact.is_key_contact(),
|
||||
"get_encryption_info: contact {contact_id} is not a key-contact."
|
||||
);
|
||||
debug_assert!(contact.is_key_contact());
|
||||
let fingerprint = contact
|
||||
.fingerprint()
|
||||
.context("Contact does not have a fingerprint in encrypted chat")?;
|
||||
@@ -1679,6 +1657,12 @@ impl Chat {
|
||||
return Ok(Some(reason));
|
||||
}
|
||||
}
|
||||
if self.is_protection_broken() {
|
||||
let reason = ProtectionBroken;
|
||||
if !skip_fn(&reason) {
|
||||
return Ok(Some(reason));
|
||||
}
|
||||
}
|
||||
if self.is_mailing_list() && self.get_mailinglist_addr().is_none_or_empty() {
|
||||
let reason = ReadOnlyMailingList;
|
||||
if !skip_fn(&reason) {
|
||||
@@ -1771,12 +1755,6 @@ impl Chat {
|
||||
return Ok(Some(get_device_icon(context).await?));
|
||||
} else if self.is_self_talk() {
|
||||
return Ok(Some(get_saved_messages_icon(context).await?));
|
||||
} else if !self.is_encrypted(context).await? {
|
||||
// This is an unencrypted chat, show a special avatar that marks it as such.
|
||||
return Ok(Some(get_abs_path(
|
||||
context,
|
||||
Path::new(&get_unencrypted_icon(context).await?),
|
||||
)));
|
||||
} else if self.typ == Chattype::Single {
|
||||
// For 1:1 chats, we always use the same avatar as for the contact
|
||||
// This is before the `self.is_encrypted()` check, because that function
|
||||
@@ -1786,6 +1764,12 @@ impl Chat {
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
return contact.get_profile_image(context).await;
|
||||
}
|
||||
} else if !self.is_encrypted(context).await? {
|
||||
// This is an address-contact chat, show a special avatar that marks it as such
|
||||
return Ok(Some(get_abs_path(
|
||||
context,
|
||||
Path::new(&get_address_contact_icon(context).await?),
|
||||
)));
|
||||
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
// Load the group avatar, or the device-chat / saved-messages icon
|
||||
if !image_rel.is_empty() {
|
||||
@@ -1908,9 +1892,25 @@ impl Chat {
|
||||
Ok(is_encrypted)
|
||||
}
|
||||
|
||||
/// Deprecated 2025-07. Returns false.
|
||||
/// Returns true if the chat was protected, and then an incoming message broke this protection.
|
||||
///
|
||||
/// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
|
||||
/// otherwise it will return false for all chats.
|
||||
///
|
||||
/// 1:1 chats are automatically set as protected when a contact is verified.
|
||||
/// When a message comes in that is not encrypted / signed correctly,
|
||||
/// the chat is automatically set as unprotected again.
|
||||
/// `is_protection_broken()` will return true until `chat_id.accept()` is called.
|
||||
///
|
||||
/// The UI should let the user confirm that this is OK with a message like
|
||||
/// `Bob sent a message from another device. Tap to learn more`
|
||||
/// and then call `chat_id.accept()`.
|
||||
pub fn is_protection_broken(&self) -> bool {
|
||||
false
|
||||
match self.protected {
|
||||
ProtectionStatus::Protected => false,
|
||||
ProtectionStatus::Unprotected => false,
|
||||
ProtectionStatus::ProtectionBroken => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if location streaming is enabled in the chat.
|
||||
@@ -2490,13 +2490,11 @@ pub(crate) async fn get_archive_icon(context: &Context) -> Result<PathBuf> {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns path to the icon
|
||||
/// indicating unencrypted chats and address-contacts.
|
||||
pub(crate) async fn get_unencrypted_icon(context: &Context) -> Result<PathBuf> {
|
||||
pub(crate) async fn get_address_contact_icon(context: &Context) -> Result<PathBuf> {
|
||||
get_asset_icon(
|
||||
context,
|
||||
"icon-unencrypted",
|
||||
include_bytes!("../assets/icon-unencrypted.png"),
|
||||
"icon-address-contact",
|
||||
include_bytes!("../assets/icon-address-contact.png"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -2670,10 +2668,6 @@ impl ChatIdBlocked {
|
||||
smeared_time,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
chat_id
|
||||
.maybe_add_encrypted_msg(context, smeared_time)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
@@ -2906,7 +2900,7 @@ async fn prepare_send_msg(
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
|
||||
let skip_fn = |reason: &CantSendReason| match reason {
|
||||
CantSendReason::ContactRequest => {
|
||||
CantSendReason::ProtectionBroken | CantSendReason::ContactRequest => {
|
||||
// Allow securejoin messages, they are supposed to repair the verification.
|
||||
// If the chat is a contact request, let the user accept it later.
|
||||
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
@@ -2968,9 +2962,6 @@ async fn prepare_send_msg(
|
||||
let row_ids = create_send_msg_jobs(context, msg)
|
||||
.await
|
||||
.context("Failed to create send jobs")?;
|
||||
if !row_ids.is_empty() {
|
||||
donation_request_maybe(context).await.log_err(context).ok();
|
||||
}
|
||||
Ok(row_ids)
|
||||
}
|
||||
|
||||
@@ -3215,31 +3206,6 @@ pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Re
|
||||
send_msg(context, chat_id, &mut msg).await
|
||||
}
|
||||
|
||||
async fn donation_request_maybe(context: &Context) -> Result<()> {
|
||||
let secs_between_checks = 30 * 24 * 60 * 60;
|
||||
let now = time();
|
||||
let ts = context
|
||||
.get_config_i64(Config::DonationRequestNextCheck)
|
||||
.await?;
|
||||
if ts > now {
|
||||
return Ok(());
|
||||
}
|
||||
let msg_cnt = context.sql.count(
|
||||
"SELECT COUNT(*) FROM msgs WHERE state>=? AND hidden=0",
|
||||
(MessageState::OutDelivered,),
|
||||
);
|
||||
let ts = if ts == 0 || msg_cnt.await? < 100 {
|
||||
now.saturating_add(secs_between_checks)
|
||||
} else {
|
||||
let mut msg = Message::new_text(stock_str::donation_request(context).await);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
i64::MAX
|
||||
};
|
||||
context
|
||||
.set_config_internal(Config::DonationRequestNextCheck, Some(&ts.to_string()))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Chat message list request options.
|
||||
#[derive(Debug)]
|
||||
pub struct MessageListOptions {
|
||||
@@ -3325,11 +3291,10 @@ pub async fn get_chat_msgs_ex(
|
||||
for (ts, curr_id) in sorted_rows {
|
||||
if add_daymarker {
|
||||
let curr_local_timestamp = ts + cnv_to_local;
|
||||
let secs_in_day = 86400;
|
||||
let curr_day = curr_local_timestamp / secs_in_day;
|
||||
let curr_day = curr_local_timestamp / 86400;
|
||||
if curr_day != last_day {
|
||||
ret.push(ChatItem::DayMarker {
|
||||
timestamp: curr_day * secs_in_day - cnv_to_local,
|
||||
timestamp: curr_day * 86400, // Convert day back to Unix timestamp
|
||||
});
|
||||
last_day = curr_day;
|
||||
}
|
||||
@@ -3659,31 +3624,15 @@ pub async fn get_past_chat_contacts(context: &Context, chat_id: ChatId) -> Resul
|
||||
}
|
||||
|
||||
/// Creates a group chat with a given `name`.
|
||||
/// Deprecated on 2025-06-21, use `create_group_ex()`.
|
||||
pub async fn create_group_chat(
|
||||
context: &Context,
|
||||
protect: ProtectionStatus,
|
||||
name: &str,
|
||||
chat_name: &str,
|
||||
) -> Result<ChatId> {
|
||||
create_group_ex(context, Some(protect), name).await
|
||||
}
|
||||
|
||||
/// Creates a group chat.
|
||||
///
|
||||
/// * `encryption` - If `Some`, the chat is encrypted (with key-contacts) and can be protected.
|
||||
/// * `name` - Chat name.
|
||||
pub async fn create_group_ex(
|
||||
context: &Context,
|
||||
encryption: Option<ProtectionStatus>,
|
||||
name: &str,
|
||||
) -> Result<ChatId> {
|
||||
let chat_name = sanitize_single_line(name);
|
||||
let chat_name = sanitize_single_line(chat_name);
|
||||
ensure!(!chat_name.is_empty(), "Invalid chat name");
|
||||
|
||||
let grpid = match encryption {
|
||||
Some(_) => create_id(),
|
||||
None => String::new(),
|
||||
};
|
||||
let grpid = create_id();
|
||||
|
||||
let timestamp = create_smeared_timestamp(context);
|
||||
let row_id = context
|
||||
@@ -3703,8 +3652,7 @@ pub async fn create_group_ex(
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
|
||||
if encryption == Some(ProtectionStatus::Protected) {
|
||||
let protect = ProtectionStatus::Protected;
|
||||
if protect == ProtectionStatus::Protected {
|
||||
chat_id
|
||||
.set_protection_for_timestamp_sort(context, protect, timestamp, None)
|
||||
.await?;
|
||||
@@ -3737,7 +3685,8 @@ pub async fn create_group_ex(
|
||||
/// Returns the created chat's id.
|
||||
pub async fn create_broadcast(context: &Context, chat_name: String) -> Result<ChatId> {
|
||||
let grpid = create_id();
|
||||
create_broadcast_ex(context, Sync, grpid, chat_name).await
|
||||
let secret = create_broadcast_shared_secret();
|
||||
create_broadcast_ex(context, Sync, grpid, chat_name, secret).await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_broadcast_ex(
|
||||
@@ -3745,6 +3694,7 @@ pub(crate) async fn create_broadcast_ex(
|
||||
sync: sync::Sync,
|
||||
grpid: String,
|
||||
chat_name: String,
|
||||
secret: String,
|
||||
) -> Result<ChatId> {
|
||||
let row_id = {
|
||||
let chat_name = &chat_name;
|
||||
@@ -3762,17 +3712,28 @@ pub(crate) async fn create_broadcast_ex(
|
||||
},
|
||||
)?);
|
||||
}
|
||||
let mut param = Params::new();
|
||||
// param.set(Param::Unpromoted, 1); // TODO broadcasts will just never be unpromoted for now
|
||||
param.set(Param::SymmetricKey, &secret);
|
||||
t.execute(
|
||||
"INSERT INTO chats \
|
||||
(type, name, grpid, param, created_timestamp) \
|
||||
VALUES(?, ?, ?, \'U=1\', ?);",
|
||||
VALUES(?, ?, ?, ?, ?);",
|
||||
(
|
||||
Chattype::OutBroadcast,
|
||||
&chat_name,
|
||||
&grpid,
|
||||
param.to_string(),
|
||||
create_smeared_timestamp(context),
|
||||
),
|
||||
)?;
|
||||
let chat_id = t.last_insert_rowid();
|
||||
// TODO code duplication of `INSERT INTO broadcasts_shared_secrets`
|
||||
t.execute(
|
||||
"INSERT INTO broadcasts_shared_secrets (chat_id, secret) VALUES (?, ?)
|
||||
ON CONFLICT(chat_id) DO UPDATE SET secret=excluded.chat_id",
|
||||
(chat_id, &secret),
|
||||
)?;
|
||||
Ok(t.last_insert_rowid().try_into()?)
|
||||
};
|
||||
context.sql.transaction(trans_fn).await?
|
||||
@@ -3784,7 +3745,7 @@ pub(crate) async fn create_broadcast_ex(
|
||||
|
||||
if sync.into() {
|
||||
let id = SyncId::Grpid(grpid);
|
||||
let action = SyncAction::CreateBroadcast(chat_name);
|
||||
let action = SyncAction::CreateBroadcast { chat_name, secret };
|
||||
self::sync(context, id, action).await.log_err(context).ok();
|
||||
}
|
||||
|
||||
@@ -3970,7 +3931,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
}
|
||||
add_to_chat_contacts_table(context, time(), chat_id, &[contact_id]).await?;
|
||||
}
|
||||
if chat.typ == Chattype::Group && chat.is_promoted() {
|
||||
if chat.is_promoted() {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
|
||||
let contact_addr = contact.get_addr().to_lowercase();
|
||||
@@ -4483,24 +4444,15 @@ pub(crate) async fn save_copy_in_self_talk(
|
||||
bail!("message already saved.");
|
||||
}
|
||||
|
||||
let copy_fields = "from_id, to_id, timestamp_rcvd, type, txt,
|
||||
mime_modified, mime_headers, mime_compressed, mime_in_reply_to, subject, msgrmsg";
|
||||
let copy_fields = "from_id, to_id, timestamp_sent, timestamp_rcvd, type, txt, \
|
||||
mime_modified, mime_headers, mime_compressed, mime_in_reply_to, subject, msgrmsg";
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
&format!(
|
||||
"INSERT INTO msgs ({copy_fields},
|
||||
timestamp_sent,
|
||||
chat_id, rfc724_mid, state, timestamp, param, starred)
|
||||
SELECT {copy_fields},
|
||||
-- Outgoing messages on originating device
|
||||
-- have timestamp_sent == 0.
|
||||
-- We copy sort timestamp instead
|
||||
-- so UIs display the same timestamp
|
||||
-- for saved and original message.
|
||||
IIF(timestamp_sent == 0, timestamp, timestamp_sent),
|
||||
?, ?, ?, ?, ?, ?
|
||||
FROM msgs WHERE id=?;"
|
||||
"INSERT INTO msgs ({copy_fields}, chat_id, rfc724_mid, state, timestamp, param, starred) \
|
||||
SELECT {copy_fields}, ?, ?, ?, ?, ?, ? \
|
||||
FROM msgs WHERE id=?;"
|
||||
),
|
||||
(
|
||||
dest_chat_id,
|
||||
@@ -5009,7 +4961,10 @@ pub(crate) enum SyncAction {
|
||||
SetVisibility(ChatVisibility),
|
||||
SetMuted(MuteDuration),
|
||||
/// Create broadcast channel with the given name.
|
||||
CreateBroadcast(String),
|
||||
CreateBroadcast {
|
||||
chat_name: String,
|
||||
secret: String,
|
||||
},
|
||||
Rename(String),
|
||||
/// Set chat contacts by their addresses.
|
||||
SetContacts(Vec<String>),
|
||||
@@ -5072,8 +5027,15 @@ impl Context {
|
||||
.id
|
||||
}
|
||||
SyncId::Grpid(grpid) => {
|
||||
if let SyncAction::CreateBroadcast(name) = action {
|
||||
create_broadcast_ex(self, Nosync, grpid.clone(), name.clone()).await?;
|
||||
if let SyncAction::CreateBroadcast { chat_name, secret } = action {
|
||||
create_broadcast_ex(
|
||||
self,
|
||||
Nosync,
|
||||
grpid.clone(),
|
||||
chat_name.clone(),
|
||||
secret.to_string(),
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
get_chat_id_by_grpid(self, grpid)
|
||||
@@ -5096,7 +5058,7 @@ impl Context {
|
||||
SyncAction::Accept => chat_id.accept_ex(self, Nosync).await,
|
||||
SyncAction::SetVisibility(v) => chat_id.set_visibility_ex(self, Nosync, *v).await,
|
||||
SyncAction::SetMuted(duration) => set_muted_ex(self, Nosync, chat_id, *duration).await,
|
||||
SyncAction::CreateBroadcast(_) => {
|
||||
SyncAction::CreateBroadcast { .. } => {
|
||||
Err(anyhow!("sync_alter_chat({id:?}, {action:?}): Bad request."))
|
||||
}
|
||||
SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,
|
||||
|
||||
+74
-71
@@ -8,7 +8,7 @@ use crate::message::{MessengerMessage, delete_msgs};
|
||||
use crate::mimeparser::{self, MimeMessage};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, E2EE_INFO_MSGS, TestContext, TestContextManager,
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
|
||||
TimeShiftFalsePositiveNote, sync,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -2104,7 +2104,7 @@ async fn test_forward_basic() -> Result<()> {
|
||||
forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?;
|
||||
|
||||
let forwarded_msg = bob.pop_sent_msg().await;
|
||||
assert_eq!(bob_chat.id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 2);
|
||||
assert_eq!(bob_chat.id.get_msg_cnt(&bob).await?, 2);
|
||||
assert_ne!(
|
||||
forwarded_msg.load_from_db().await.rfc724_mid,
|
||||
msg.rfc724_mid,
|
||||
@@ -2132,7 +2132,7 @@ async fn test_forward_info_msg() -> Result<()> {
|
||||
assert!(msg1.get_text().contains("bob@example.net"));
|
||||
|
||||
let chat_id2 = ChatId::create_for_contact(alice, bob_id).await?;
|
||||
assert_eq!(get_chat_msgs(alice, chat_id2).await?.len(), E2EE_INFO_MSGS);
|
||||
assert_eq!(get_chat_msgs(alice, chat_id2).await?.len(), 0);
|
||||
forward_msgs(alice, &[msg1.id], chat_id2).await?;
|
||||
let msg2 = alice.get_last_msg_in(chat_id2).await;
|
||||
assert!(!msg2.is_info()); // forwarded info-messages lose their info-state
|
||||
@@ -2280,19 +2280,14 @@ async fn test_only_minimal_data_are_forwarded() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_msgs() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
|
||||
let sent = alice.send_text(alice_chat.get_id(), "hi, bob").await;
|
||||
let sent_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?;
|
||||
assert!(sent_msg.get_saved_msg_id(&alice).await?.is_none());
|
||||
assert!(sent_msg.get_original_msg_id(&alice).await?.is_none());
|
||||
let sent_timestamp = sent_msg.get_timestamp();
|
||||
assert!(sent_timestamp > 0);
|
||||
|
||||
SystemTime::shift(Duration::from_secs(60));
|
||||
|
||||
let self_chat = alice.get_self_chat().await;
|
||||
save_msgs(&alice, &[sent.sender_msg_id]).await?;
|
||||
@@ -2310,8 +2305,6 @@ async fn test_save_msgs() -> Result<()> {
|
||||
assert_eq!(saved_msg.get_from_id(), ContactId::SELF);
|
||||
assert_eq!(saved_msg.get_state(), MessageState::OutDelivered);
|
||||
assert_ne!(saved_msg.rfc724_mid(), sent_msg.rfc724_mid());
|
||||
let saved_timestamp = saved_msg.get_timestamp();
|
||||
assert_eq!(saved_timestamp, sent_timestamp);
|
||||
|
||||
let sent_msg = Message::load_from_db(&alice, sent.sender_msg_id).await?;
|
||||
assert_eq!(
|
||||
@@ -2525,34 +2518,22 @@ async fn test_resend_own_message() -> Result<()> {
|
||||
let sent1_ts_sent = msg.timestamp_sent;
|
||||
assert_eq!(msg.get_text(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 1
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 1);
|
||||
bob.recv_msg(&sent2).await;
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2);
|
||||
let received = bob.recv_msg_opt(&sent3).await;
|
||||
// No message should actually be added since we already know this message:
|
||||
assert!(received.is_none());
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2);
|
||||
|
||||
// Fiona does not receive the first message, however, due to resending, she has a similar view as Alice and Bob
|
||||
fiona.recv_msg(&sent2).await;
|
||||
let msg = fiona.recv_msg(&sent3).await;
|
||||
assert_eq!(msg.get_text(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&fiona, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&fiona, msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&fiona, msg.chat_id).await?.len(), 2);
|
||||
let msg_from = Contact::get_by_id(&fiona, msg.get_from_id()).await?;
|
||||
assert_eq!(msg_from.get_addr(), "alice@example.org");
|
||||
assert!(sent1_ts_sent < msg.timestamp_sent);
|
||||
@@ -3055,6 +3036,45 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_encrypt_decrypt_broadcast_integration() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_without_secret = &tcm.bob().await;
|
||||
|
||||
let secret = "secret";
|
||||
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
|
||||
tcm.section("Create a broadcast channel with Bob, and send a message");
|
||||
let alice_chat_id = create_broadcast(alice, "My Channel".to_string()).await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
|
||||
let mut alice_chat = Chat::load_from_db(alice, alice_chat_id).await?;
|
||||
alice_chat.param.set(Param::SymmetricKey, secret);
|
||||
alice_chat.update_param(alice).await?;
|
||||
|
||||
// TODO the chat_id 10 is magical here:
|
||||
bob.sql
|
||||
.execute(
|
||||
"INSERT INTO broadcasts_shared_secrets (chat_id, secret) VALUES (10, ?)",
|
||||
(secret,),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let sent = alice
|
||||
.send_text(alice_chat_id, "Symmetrically encrypted message")
|
||||
.await;
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
assert_eq!(rcvd.text, "Symmetrically encrypted message");
|
||||
|
||||
tcm.section("If Bob doesn't know the secret, he can't decrypt the message");
|
||||
bob_without_secret.recv_msg_trash(&sent).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_for_contact_with_blocked() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
@@ -3765,7 +3785,9 @@ async fn test_sync_broadcast() -> Result<()> {
|
||||
assert_eq!(a1_broadcast_chat.get_name(), a0_broadcast_chat.get_name());
|
||||
assert!(get_chat_contacts(alice1, a1_broadcast_id).await?.is_empty());
|
||||
add_contact_to_chat(alice0, a0_broadcast_id, a0b_contact_id).await?;
|
||||
sync(alice0, alice1).await;
|
||||
let sent = alice0.pop_sent_msg().await;
|
||||
let rcvd = alice1.recv_msg(&sent).await;
|
||||
dbg!(rcvd); // TODO
|
||||
|
||||
// This also imports Bob's key from the vCard.
|
||||
// Otherwise it is possible that second device
|
||||
@@ -3807,12 +3829,25 @@ async fn test_sync_name() -> Result<()> {
|
||||
let a0_broadcast_id = create_broadcast(alice0, "Channel".to_string()).await?;
|
||||
sync(alice0, alice1).await;
|
||||
let a0_broadcast_chat = Chat::load_from_db(alice0, a0_broadcast_id).await?;
|
||||
|
||||
set_chat_name(alice0, a0_broadcast_id, "Broadcast channel 42").await?;
|
||||
sync(alice0, alice1).await;
|
||||
//sync(alice0, alice1).await; // crash
|
||||
|
||||
let sent = alice0.pop_sent_msg().await;
|
||||
let rcvd = alice1.recv_msg(&sent).await;
|
||||
assert_eq!(rcvd.from_id, ContactId::SELF);
|
||||
assert_eq!(rcvd.to_id, ContactId::SELF);
|
||||
assert_eq!(
|
||||
rcvd.text,
|
||||
"You changed group name from \"Channel\" to \"Broadcast channel 42\"."
|
||||
);
|
||||
assert_eq!(rcvd.param.get_cmd(), SystemMessage::GroupNameChanged);
|
||||
let a1_broadcast_id = get_chat_id_by_grpid(alice1, &a0_broadcast_chat.grpid)
|
||||
.await?
|
||||
.unwrap()
|
||||
.0;
|
||||
assert_eq!(rcvd.chat_id, a1_broadcast_id);
|
||||
|
||||
let a1_broadcast_chat = Chat::load_from_db(alice1, a1_broadcast_id).await?;
|
||||
assert_eq!(a1_broadcast_chat.get_type(), Chattype::OutBroadcast);
|
||||
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast channel 42");
|
||||
@@ -4473,13 +4508,13 @@ async fn test_receive_edit_request_after_removal() -> Result<()> {
|
||||
let bob_msg = bob.recv_msg(&sent1).await;
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
assert_eq!(bob_msg.text, "zext me in delra.cat");
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 1);
|
||||
|
||||
delete_msgs(bob, &[bob_msg.id]).await?;
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 0);
|
||||
|
||||
bob.recv_msg_trash(&sent2).await;
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4568,34 +4603,28 @@ async fn test_send_delete_request() -> Result<()> {
|
||||
// Alice sends a message, then sends a deletion request
|
||||
let sent1 = alice.send_text(alice_chat.id, "wtf").await;
|
||||
let alice_msg = sent1.load_from_db().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 2);
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 2);
|
||||
|
||||
message::delete_msgs_ex(alice, &[alice_msg.id], true).await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 1);
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1);
|
||||
|
||||
// Bob receives both messages and has nothing the end
|
||||
let bob_msg = bob.recv_msg(&sent1).await;
|
||||
assert_eq!(bob_msg.text, "wtf");
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 2);
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 2);
|
||||
|
||||
bob.recv_msg_opt(&sent2).await;
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1);
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 1);
|
||||
|
||||
// Alice has another device, and there is also nothing at the end
|
||||
let alice2 = &tcm.alice().await;
|
||||
alice2.recv_msg(&sent0).await;
|
||||
let alice2_msg = alice2.recv_msg(&sent1).await;
|
||||
assert_eq!(
|
||||
alice2_msg.chat_id.get_msg_cnt(alice2).await?,
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 2);
|
||||
|
||||
alice2.recv_msg_opt(&sent2).await;
|
||||
assert_eq!(
|
||||
alice2_msg.chat_id.get_msg_cnt(alice2).await?,
|
||||
E2EE_INFO_MSGS + 1
|
||||
);
|
||||
assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4723,32 +4752,6 @@ async fn test_no_key_contacts_in_adhoc_chats() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that key-contacts cannot be added to an unencrypted (ad hoc) group and the group and
|
||||
/// messages report that they are unencrypted.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_unencrypted_group_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let chat_id = create_group_ex(alice, None, "Group chat").await?;
|
||||
let bob_key_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let charlie_address_contact_id = alice.add_or_lookup_address_contact_id(charlie).await;
|
||||
|
||||
let res = add_contact_to_chat(alice, chat_id, bob_key_contact_id).await;
|
||||
assert!(res.is_err());
|
||||
|
||||
add_contact_to_chat(alice, chat_id, charlie_address_contact_id).await?;
|
||||
|
||||
let chat = Chat::load_from_db(alice, chat_id).await?;
|
||||
assert!(!chat.is_encrypted(alice).await?);
|
||||
let sent_msg = alice.send_text(chat_id, "Hello").await;
|
||||
let msg = Message::load_from_db(alice, sent_msg.sender_msg_id).await?;
|
||||
assert!(!msg.get_showpadlock());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that avatar cannot be set in ad hoc groups.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_avatar_in_adhoc_chats() -> Result<()> {
|
||||
|
||||
@@ -245,6 +245,9 @@ impl Chatlist {
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
};
|
||||
// Return ProtectionBroken chats also, as that may happen to a verified chat at any
|
||||
// time. It may be confusing if a chat that is normally in the list disappears
|
||||
// suddenly. The UI need to deal with that case anyway.
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, c.type, c.param, m.id
|
||||
FROM chats c
|
||||
|
||||
+1
-4
@@ -369,9 +369,6 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))]
|
||||
DisableIdle,
|
||||
|
||||
/// Timestamp of the next check for donation request need.
|
||||
DonationRequestNextCheck,
|
||||
|
||||
/// Defines the max. size (in bytes) of messages downloaded automatically.
|
||||
/// 0 = no limit.
|
||||
#[strum(props(default = "0"))]
|
||||
@@ -417,7 +414,7 @@ pub enum Config {
|
||||
#[strum(props(default = "172800"))]
|
||||
GossipPeriod,
|
||||
|
||||
/// Deprecated 2025-07. Feature flag for verified 1:1 chats; the UI should set it
|
||||
/// Feature flag for verified 1:1 chats; the UI should set it
|
||||
/// to 1 if it supports verified 1:1 chats.
|
||||
/// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
|
||||
/// and when the key changes, an info message is posted into the chat.
|
||||
|
||||
+5
-31
@@ -755,19 +755,7 @@ impl Contact {
|
||||
self.is_bot
|
||||
}
|
||||
|
||||
/// Looks up a known and unblocked contact with a given e-mail address.
|
||||
/// To get a list of all known and unblocked contacts, use contacts_get_contacts().
|
||||
///
|
||||
///
|
||||
/// **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
|
||||
/// (e.g. an address-contact and a key-contact),
|
||||
/// this looks up the most recently seen contact,
|
||||
/// i.e. which contact is returned depends on which contact last sent a message.
|
||||
/// If the user just clicked on a mailto: link, then this is the best thing you can do.
|
||||
/// But **DO NOT** internally represent contacts by their email address
|
||||
/// and do not use this function to look them up;
|
||||
/// otherwise this function will sometimes look up the wrong contact.
|
||||
/// Instead, you should internally represent contacts by their ids.
|
||||
/// Check if an e-mail address belongs to a known and unblocked contact.
|
||||
///
|
||||
/// Known and unblocked contacts will be returned by `get_contacts()`.
|
||||
///
|
||||
@@ -807,28 +795,14 @@ impl Contact {
|
||||
.query_get_value(
|
||||
"SELECT id FROM contacts
|
||||
WHERE addr=?1 COLLATE NOCASE
|
||||
AND id>?2 AND origin>=?3 AND (? OR blocked=?)
|
||||
ORDER BY
|
||||
(
|
||||
SELECT COUNT(*) FROM chats c
|
||||
INNER JOIN chats_contacts cc
|
||||
ON c.id=cc.chat_id
|
||||
WHERE c.type=?
|
||||
AND c.id>?
|
||||
AND c.blocked=?
|
||||
AND cc.contact_id=contacts.id
|
||||
) DESC,
|
||||
last_seen DESC, fingerprint DESC
|
||||
LIMIT 1",
|
||||
AND fingerprint='' -- Do not lookup key-contacts
|
||||
AND id>?2 AND origin>=?3 AND (? OR blocked=?)",
|
||||
(
|
||||
&addr_normalized,
|
||||
ContactId::LAST_SPECIAL,
|
||||
min_origin as u32,
|
||||
blocked.is_none(),
|
||||
blocked.unwrap_or(Blocked::Not),
|
||||
Chattype::Single,
|
||||
constants::DC_CHAT_ID_LAST_SPECIAL,
|
||||
blocked.unwrap_or(Blocked::Not),
|
||||
blocked.unwrap_or_default(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
@@ -1564,7 +1538,7 @@ impl Contact {
|
||||
return Ok(Some(chat::get_device_icon(context).await?));
|
||||
}
|
||||
if show_fallback_icon && !self.id.is_special() && !self.is_key_contact() {
|
||||
return Ok(Some(chat::get_unencrypted_icon(context).await?));
|
||||
return Ok(Some(chat::get_address_contact_icon(context).await?));
|
||||
}
|
||||
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
if !image_rel.is_empty() {
|
||||
|
||||
@@ -1035,50 +1035,6 @@ async fn test_was_seen_recently_event() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_lookup_id_by_addr_recent_ex(accept_unencrypted_chat: bool) -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml");
|
||||
assert!(std::str::from_utf8(raw)?.contains("Date: Thu, 24 Nov 2022 20:05:57 +0100"));
|
||||
let received_msg = receive_imf(bob, raw, false).await?.unwrap();
|
||||
received_msg.chat_id.accept(bob).await?;
|
||||
|
||||
let raw = r#"From: Alice <alice@example.org>
|
||||
To: bob@example.net
|
||||
Message-ID: message$TIME@example.org
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
Date: Thu, 24 Nov 2022 $TIME +0100
|
||||
|
||||
Hi"#
|
||||
.to_string();
|
||||
for (time, is_key_contact) in [("20:05:57", true), ("20:05:58", !accept_unencrypted_chat)] {
|
||||
let raw = raw.replace("$TIME", time);
|
||||
let received_msg = receive_imf(bob, raw.as_bytes(), false).await?.unwrap();
|
||||
if accept_unencrypted_chat {
|
||||
received_msg.chat_id.accept(bob).await?;
|
||||
}
|
||||
let contact_id = Contact::lookup_id_by_addr(bob, "alice@example.org", Origin::Unknown)
|
||||
.await?
|
||||
.unwrap();
|
||||
let contact = Contact::get_by_id(bob, contact_id).await?;
|
||||
assert_eq!(contact.is_key_contact(), is_key_contact);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_lookup_id_by_addr_recent() -> Result<()> {
|
||||
let accept_unencrypted_chat = true;
|
||||
test_lookup_id_by_addr_recent_ex(accept_unencrypted_chat).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_lookup_id_by_addr_recent_accepted() -> Result<()> {
|
||||
let accept_unencrypted_chat = false;
|
||||
test_lookup_id_by_addr_recent_ex(accept_unencrypted_chat).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_verified_by_none() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -1116,7 +1072,6 @@ async fn test_sync_create() -> Result<()> {
|
||||
.unwrap();
|
||||
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
|
||||
assert_eq!(a1b_contact.name, "Bob");
|
||||
assert_eq!(a1b_contact.is_key_contact(), false);
|
||||
|
||||
Contact::create(alice0, "Bob Renamed", "bob@example.net").await?;
|
||||
test_utils::sync(alice0, alice1).await;
|
||||
@@ -1126,7 +1081,6 @@ async fn test_sync_create() -> Result<()> {
|
||||
assert_eq!(id, a1b_contact_id);
|
||||
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
|
||||
assert_eq!(a1b_contact.name, "Bob Renamed");
|
||||
assert_eq!(a1b_contact.is_key_contact(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+9
-32
@@ -27,7 +27,6 @@ use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
|
||||
use crate::key::{load_self_secret_key, self_fingerprint};
|
||||
use crate::log::{info, warn};
|
||||
use crate::logged_debug_assert;
|
||||
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
|
||||
use crate::message::{self, Message, MessageState, MsgId};
|
||||
use crate::param::{Param, Params};
|
||||
@@ -333,15 +332,6 @@ impl Default for RunningState {
|
||||
/// about the context on top of the information here.
|
||||
pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
let mut res = BTreeMap::new();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
res.insert(
|
||||
"debug_assertions",
|
||||
"On - DO NOT RELEASE THIS BUILD".to_string(),
|
||||
);
|
||||
#[cfg(not(debug_assertions))]
|
||||
res.insert("debug_assertions", "Off".to_string());
|
||||
|
||||
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
|
||||
res.insert("sqlite_version", rusqlite::version().to_string());
|
||||
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
|
||||
@@ -670,16 +660,8 @@ impl Context {
|
||||
/// or [`Self::emit_msgs_changed_without_msg_id`] should be used
|
||||
/// instead of this function.
|
||||
pub fn emit_msgs_changed(&self, chat_id: ChatId, msg_id: MsgId) {
|
||||
logged_debug_assert!(
|
||||
self,
|
||||
!chat_id.is_unset(),
|
||||
"emit_msgs_changed: chat_id is unset."
|
||||
);
|
||||
logged_debug_assert!(
|
||||
self,
|
||||
!msg_id.is_unset(),
|
||||
"emit_msgs_changed: msg_id is unset."
|
||||
);
|
||||
debug_assert!(!chat_id.is_unset());
|
||||
debug_assert!(!msg_id.is_unset());
|
||||
|
||||
self.emit_event(EventType::MsgsChanged { chat_id, msg_id });
|
||||
chatlist_events::emit_chatlist_changed(self);
|
||||
@@ -688,11 +670,7 @@ impl Context {
|
||||
|
||||
/// Emits a MsgsChanged event with specified chat and without message id.
|
||||
pub fn emit_msgs_changed_without_msg_id(&self, chat_id: ChatId) {
|
||||
logged_debug_assert!(
|
||||
self,
|
||||
!chat_id.is_unset(),
|
||||
"emit_msgs_changed_without_msg_id: chat_id is unset."
|
||||
);
|
||||
debug_assert!(!chat_id.is_unset());
|
||||
|
||||
self.emit_event(EventType::MsgsChanged {
|
||||
chat_id,
|
||||
@@ -1052,7 +1030,7 @@ impl Context {
|
||||
self.get_config_int(Config::GossipPeriod).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"verified_one_on_one_chats", // deprecated 2025-07
|
||||
"verified_one_on_one_chats",
|
||||
self.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
.await?
|
||||
.to_string(),
|
||||
@@ -1063,12 +1041,6 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"donation_request_next_check",
|
||||
self.get_config_i64(Config::DonationRequestNextCheck)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"first_key_contacts_msg_id",
|
||||
self.sql
|
||||
@@ -1087,6 +1059,7 @@ impl Context {
|
||||
#[derive(Default)]
|
||||
struct ChatNumbers {
|
||||
protected: u32,
|
||||
protection_broken: u32,
|
||||
opportunistic_dc: u32,
|
||||
opportunistic_mua: u32,
|
||||
unencrypted_dc: u32,
|
||||
@@ -1122,6 +1095,7 @@ impl Context {
|
||||
|
||||
// how many of the chats active in the last months are:
|
||||
// - protected
|
||||
// - protection-broken
|
||||
// - opportunistic-encrypted and the contact uses Delta Chat
|
||||
// - opportunistic-encrypted and the contact uses a classical MUA
|
||||
// - unencrypted and the contact uses Delta Chat
|
||||
@@ -1164,6 +1138,8 @@ impl Context {
|
||||
|
||||
if protected == ProtectionStatus::Protected {
|
||||
chats.protected += 1;
|
||||
} else if protected == ProtectionStatus::ProtectionBroken {
|
||||
chats.protection_broken += 1;
|
||||
} else if encrypted {
|
||||
if is_dc_message {
|
||||
chats.opportunistic_dc += 1;
|
||||
@@ -1181,6 +1157,7 @@ impl Context {
|
||||
)
|
||||
.await?;
|
||||
res += &format!("chats_protected {}\n", chats.protected);
|
||||
res += &format!("chats_protection_broken {}\n", chats.protection_broken);
|
||||
res += &format!("chats_opportunistic_dc {}\n", chats.opportunistic_dc);
|
||||
res += &format!("chats_opportunistic_mua {}\n", chats.opportunistic_mua);
|
||||
res += &format!("chats_unencrypted_dc {}\n", chats.unencrypted_dc);
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, get_chat_msg};
|
||||
use crate::test_utils::{TestContext, get_chat_msg};
|
||||
use crate::tools::{SystemTime, create_outgoing_rfc724_mid};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -571,7 +571,7 @@ async fn test_get_next_msgs() -> Result<()> {
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
|
||||
assert_eq!(alice.get_next_msgs().await?.len(), E2EE_INFO_MSGS);
|
||||
assert!(alice.get_next_msgs().await?.is_empty());
|
||||
assert!(bob.get_next_msgs().await?.is_empty());
|
||||
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
|
||||
+3
-3
@@ -14,13 +14,14 @@ use crate::pgp;
|
||||
pub fn try_decrypt<'a>(
|
||||
mail: &'a ParsedMail<'a>,
|
||||
private_keyring: &'a [SignedSecretKey],
|
||||
symmetric_secrets: &[String],
|
||||
) -> Result<Option<::pgp::composed::Message<'static>>> {
|
||||
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let data = encrypted_data_part.get_body_raw()?;
|
||||
let msg = pgp::pk_decrypt(data, private_keyring)?;
|
||||
let msg = pgp::decrypt(data, private_keyring, symmetric_secrets)?;
|
||||
|
||||
Ok(Some(msg))
|
||||
}
|
||||
@@ -178,8 +179,7 @@ mod tests {
|
||||
let bob = TestContext::new_bob().await;
|
||||
receive_imf(&bob, attachment_mime, false).await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
// Subject should be prepended because the attachment doesn't have "Chat-Version".
|
||||
assert_eq!(msg.text, "Hello, Bob! – Hello from Thunderbird!");
|
||||
assert_eq!(msg.text, "Hello from Thunderbird!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+14
-18
@@ -213,18 +213,17 @@ impl Session {
|
||||
|
||||
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
|
||||
uid_message_ids.insert(uid, rfc724_mid);
|
||||
let (sender, receiver) = async_channel::unbounded();
|
||||
self.fetch_many_msgs(
|
||||
context,
|
||||
folder,
|
||||
uidvalidity,
|
||||
vec![uid],
|
||||
&uid_message_ids,
|
||||
false,
|
||||
sender,
|
||||
)
|
||||
.await?;
|
||||
if receiver.recv().await.is_err() {
|
||||
let (last_uid, _received) = self
|
||||
.fetch_many_msgs(
|
||||
context,
|
||||
folder,
|
||||
uidvalidity,
|
||||
vec![uid],
|
||||
&uid_message_ids,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
if last_uid.is_none() {
|
||||
bail!("Failed to fetch UID {uid}");
|
||||
}
|
||||
Ok(())
|
||||
@@ -277,7 +276,7 @@ mod tests {
|
||||
use crate::chat::{get_chat_msgs, send_msg};
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::receive_imf::receive_imf_from_inbox;
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext};
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[test]
|
||||
fn test_downloadstate_values() {
|
||||
@@ -459,10 +458,7 @@ mod tests {
|
||||
.await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
let chat_id = msg.chat_id;
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 1
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 1);
|
||||
assert_eq!(msg.download_state(), DownloadState::Available);
|
||||
|
||||
// downloading the status update afterwards expands to nothing and moves the placeholder to trash-chat
|
||||
@@ -475,7 +471,7 @@ mod tests {
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), E2EE_INFO_MSGS);
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
|
||||
assert!(
|
||||
Message::load_from_db_optional(&bob, msg.id)
|
||||
.await?
|
||||
|
||||
+19
@@ -59,6 +59,25 @@ impl EncryptHelper {
|
||||
Ok(ctext)
|
||||
}
|
||||
|
||||
/// TODO documentation
|
||||
pub async fn encrypt_for_broadcast(
|
||||
self,
|
||||
context: &Context,
|
||||
passphrase: &str,
|
||||
mail_to_encrypt: MimePart<'static>,
|
||||
compress: bool,
|
||||
) -> Result<String> {
|
||||
let sign_key = load_self_secret_key(context).await?;
|
||||
|
||||
let mut raw_message = Vec::new();
|
||||
let cursor = Cursor::new(&mut raw_message);
|
||||
mail_to_encrypt.clone().write_part(cursor).ok();
|
||||
|
||||
let ctext = pgp::encrypt_for_broadcast(raw_message, passphrase, sign_key, compress).await?;
|
||||
|
||||
Ok(ctext)
|
||||
}
|
||||
|
||||
/// Signs the passed-in `mail` using the private key from `context`.
|
||||
/// Returns the payload and the signature.
|
||||
pub async fn sign(self, context: &Context, mail: &MimePart<'static>) -> Result<String> {
|
||||
|
||||
@@ -277,7 +277,6 @@ pub(crate) async fn stock_ephemeral_timer_changed(
|
||||
.await
|
||||
}
|
||||
604_800 => stock_str::msg_ephemeral_timer_week(context, from_id).await,
|
||||
31_536_000..=31_708_800 => stock_str::msg_ephemeral_timer_year(context, from_id).await,
|
||||
_ => {
|
||||
stock_str::msg_ephemeral_timer_weeks(
|
||||
context,
|
||||
|
||||
@@ -128,33 +128,31 @@ async fn test_stock_ephemeral_messages() {
|
||||
/// Test enabling and disabling ephemeral timer remotely.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ephemeral_enable_disable() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
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;
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
chat_alice
|
||||
.set_ephemeral_timer(alice, Timer::Enabled { duration: 60 })
|
||||
.set_ephemeral_timer(&alice.ctx, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let bob_received_message = bob.recv_msg(&sent).await;
|
||||
bob.recv_msg(&sent).await;
|
||||
assert_eq!(
|
||||
bob_received_message.text,
|
||||
"Message deletion timer is set to 1 minute by alice@example.org."
|
||||
);
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(bob).await?,
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Enabled { duration: 60 }
|
||||
);
|
||||
|
||||
chat_alice
|
||||
.set_ephemeral_timer(alice, Timer::Disabled)
|
||||
.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).await?, Timer::Disabled);
|
||||
assert_eq!(
|
||||
chat_bob.get_ephemeral_timer(&bob.ctx).await?,
|
||||
Timer::Disabled
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -93,6 +93,10 @@ pub enum HeaderDef {
|
||||
/// This message obsoletes the text of the message defined here by rfc724_mid.
|
||||
ChatEdit,
|
||||
|
||||
/// The secret shared amongst all recipients of this broadcast channel.
|
||||
/// This secret.
|
||||
ChatBroadcastSecret,
|
||||
|
||||
/// [Autocrypt](https://autocrypt.org/) header.
|
||||
Autocrypt,
|
||||
AutocryptGossip,
|
||||
|
||||
+59
-102
@@ -14,10 +14,10 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure, format_err};
|
||||
use async_channel::{self, Receiver, Sender};
|
||||
use async_channel::Receiver;
|
||||
use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
|
||||
use deltachat_contact_tools::ContactAddress;
|
||||
use futures::{FutureExt as _, TryStreamExt};
|
||||
use futures::{FutureExt as _, StreamExt, TryStreamExt};
|
||||
use futures_lite::FutureExt;
|
||||
use num_traits::FromPrimitive;
|
||||
use rand::Rng;
|
||||
@@ -562,7 +562,7 @@ impl Imap {
|
||||
let read_cnt = msgs.len();
|
||||
|
||||
let download_limit = context.download_limit().await?;
|
||||
let mut uids_fetch = Vec::<(u32, bool /* partially? */)>::with_capacity(msgs.len() + 1);
|
||||
let mut uids_fetch = Vec::<(_, bool /* partially? */)>::with_capacity(msgs.len() + 1);
|
||||
let mut uid_message_ids = BTreeMap::new();
|
||||
let mut largest_uid_skipped = None;
|
||||
let delete_target = context.get_delete_msgs_target().await?;
|
||||
@@ -695,72 +695,51 @@ impl Imap {
|
||||
self.connectivity.set_working(context).await;
|
||||
}
|
||||
|
||||
let (sender, receiver) = async_channel::unbounded();
|
||||
|
||||
// Actually download messages.
|
||||
let mut largest_uid_fetched: u32 = 0;
|
||||
let mut received_msgs = Vec::with_capacity(uids_fetch.len());
|
||||
let mut uids_fetch_in_batch = Vec::with_capacity(max(uids_fetch.len(), 1));
|
||||
let mut fetch_partially = false;
|
||||
uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1));
|
||||
for (uid, fp) in uids_fetch {
|
||||
if fp != fetch_partially {
|
||||
let (largest_uid_fetched_in_batch, received_msgs_in_batch) = session
|
||||
.fetch_many_msgs(
|
||||
context,
|
||||
folder,
|
||||
uid_validity,
|
||||
uids_fetch_in_batch.split_off(0),
|
||||
&uid_message_ids,
|
||||
fetch_partially,
|
||||
)
|
||||
.await
|
||||
.context("fetch_many_msgs")?;
|
||||
received_msgs.extend(received_msgs_in_batch);
|
||||
largest_uid_fetched = max(
|
||||
largest_uid_fetched,
|
||||
largest_uid_fetched_in_batch.unwrap_or(0),
|
||||
);
|
||||
fetch_partially = fp;
|
||||
}
|
||||
uids_fetch_in_batch.push(uid);
|
||||
}
|
||||
|
||||
// Advance uid_next to the maximum of the largest known UID plus 1
|
||||
// and mailbox UIDNEXT.
|
||||
// Largest known UID is normally less than UIDNEXT,
|
||||
// but a message may have arrived between determining UIDNEXT
|
||||
// and executing the FETCH command.
|
||||
let mailbox_uid_next = session
|
||||
.selected_mailbox
|
||||
.as_ref()
|
||||
.with_context(|| format!("Expected {folder:?} to be selected"))?
|
||||
.uid_next
|
||||
.unwrap_or_default();
|
||||
let new_uid_next = max(
|
||||
max(largest_uid_fetched, largest_uid_skipped.unwrap_or(0)) + 1,
|
||||
mailbox_uid_next,
|
||||
);
|
||||
|
||||
let update_uids_future = async {
|
||||
let mut largest_uid_fetched: u32 = 0;
|
||||
|
||||
while let Ok((uid, received_msg_opt)) = receiver.recv().await {
|
||||
largest_uid_fetched = max(largest_uid_fetched, uid);
|
||||
if let Some(received_msg) = received_msg_opt {
|
||||
received_msgs.push(received_msg)
|
||||
}
|
||||
}
|
||||
|
||||
largest_uid_fetched
|
||||
};
|
||||
|
||||
let actually_download_messages_future = async move {
|
||||
let mut uids_fetch_in_batch = Vec::with_capacity(max(uids_fetch.len(), 1));
|
||||
let mut fetch_partially = false;
|
||||
uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1));
|
||||
for (uid, fp) in uids_fetch {
|
||||
if fp != fetch_partially {
|
||||
session
|
||||
.fetch_many_msgs(
|
||||
context,
|
||||
folder,
|
||||
uid_validity,
|
||||
uids_fetch_in_batch.split_off(0),
|
||||
&uid_message_ids,
|
||||
fetch_partially,
|
||||
sender.clone(),
|
||||
)
|
||||
.await
|
||||
.context("fetch_many_msgs")?;
|
||||
fetch_partially = fp;
|
||||
}
|
||||
uids_fetch_in_batch.push(uid);
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let (largest_uid_fetched, fetch_res) =
|
||||
tokio::join!(update_uids_future, actually_download_messages_future);
|
||||
|
||||
// Advance uid_next to the largest fetched UID plus 1.
|
||||
//
|
||||
// This may be larger than `mailbox_uid_next`
|
||||
// if the message has arrived after selecting mailbox
|
||||
// and determining its UIDNEXT and before prefetch.
|
||||
let mut new_uid_next = largest_uid_fetched + 1;
|
||||
if fetch_res.is_ok() {
|
||||
// If we have successfully fetched all messages we planned during prefetch,
|
||||
// then we have covered at least the range between old UIDNEXT
|
||||
// and UIDNEXT of the mailbox at the time of selecting it.
|
||||
new_uid_next = max(new_uid_next, mailbox_uid_next);
|
||||
|
||||
new_uid_next = max(new_uid_next, largest_uid_skipped.unwrap_or(0) + 1);
|
||||
}
|
||||
if new_uid_next > old_uid_next {
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
}
|
||||
@@ -773,10 +752,6 @@ impl Imap {
|
||||
|
||||
chat::mark_old_messages_as_noticed(context, received_msgs).await?;
|
||||
|
||||
// Now fail if fetching failed, so we will
|
||||
// establish a new session if this one is broken.
|
||||
fetch_res?;
|
||||
|
||||
Ok(read_cnt > 0)
|
||||
}
|
||||
|
||||
@@ -1325,19 +1300,9 @@ impl Session {
|
||||
|
||||
/// Fetches a list of messages by server UID.
|
||||
///
|
||||
/// Sends pairs of UID and info about each downloaded message to the provided channel.
|
||||
/// Received message info is optional because UID may be ignored
|
||||
/// if the message has a `\Deleted` flag.
|
||||
///
|
||||
/// The channel is used to return the results because the function may fail
|
||||
/// due to network errors before it finishes fetching all the messages.
|
||||
/// In this case caller still may want to process all the results
|
||||
/// received over the channel and persist last seen UID in the database
|
||||
/// before bubbling up the failure.
|
||||
///
|
||||
/// Returns the last UID fetched successfully and the info about each downloaded message.
|
||||
/// If the message is incorrect or there is a failure to write a message to the database,
|
||||
/// it is skipped and the error is logged.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub(crate) async fn fetch_many_msgs(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -1346,10 +1311,12 @@ impl Session {
|
||||
request_uids: Vec<u32>,
|
||||
uid_message_ids: &BTreeMap<u32, String>,
|
||||
fetch_partially: bool,
|
||||
received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
|
||||
) -> Result<()> {
|
||||
) -> Result<(Option<u32>, Vec<ReceivedMsg>)> {
|
||||
let mut last_uid = None;
|
||||
let mut received_msgs = Vec::new();
|
||||
|
||||
if request_uids.is_empty() {
|
||||
return Ok(());
|
||||
return Ok((last_uid, received_msgs));
|
||||
}
|
||||
|
||||
for (request_uids, set) in build_sequence_sets(&request_uids)? {
|
||||
@@ -1384,15 +1351,14 @@ impl Session {
|
||||
|
||||
// Try to find a requested UID in returned FETCH responses.
|
||||
while fetch_response.is_none() {
|
||||
let Some(next_fetch_response) = fetch_responses
|
||||
.try_next()
|
||||
.await
|
||||
.context("Failed to process IMAP FETCH result")?
|
||||
else {
|
||||
let Some(next_fetch_response) = fetch_responses.next().await else {
|
||||
// No more FETCH responses received from the server.
|
||||
break;
|
||||
};
|
||||
|
||||
let next_fetch_response =
|
||||
next_fetch_response.context("Failed to process IMAP FETCH result")?;
|
||||
|
||||
if let Some(next_uid) = next_fetch_response.uid {
|
||||
if next_uid == request_uid {
|
||||
fetch_response = Some(next_fetch_response);
|
||||
@@ -1436,7 +1402,7 @@ impl Session {
|
||||
|
||||
if is_deleted {
|
||||
info!(context, "Not processing deleted msg {}.", request_uid);
|
||||
received_msgs_channel.send((request_uid, None)).await?;
|
||||
last_uid = Some(request_uid);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1447,7 +1413,7 @@ impl Session {
|
||||
context,
|
||||
"Not processing message {} without a BODY.", request_uid
|
||||
);
|
||||
received_msgs_channel.send((request_uid, None)).await?;
|
||||
last_uid = Some(request_uid);
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -1479,29 +1445,20 @@ impl Session {
|
||||
.await
|
||||
{
|
||||
Ok(received_msg) => {
|
||||
received_msgs_channel
|
||||
.send((request_uid, received_msg))
|
||||
.await?;
|
||||
if let Some(m) = received_msg {
|
||||
received_msgs.push(m);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "receive_imf error: {:#}.", err);
|
||||
received_msgs_channel.send((request_uid, None)).await?;
|
||||
}
|
||||
};
|
||||
last_uid = Some(request_uid)
|
||||
}
|
||||
|
||||
// If we don't process the whole response, IMAP client is left in a broken state where
|
||||
// it will try to process the rest of response as the next response.
|
||||
//
|
||||
// Make sure to not ignore the errors, because
|
||||
// if connection times out, it will return
|
||||
// infinite stream of `Some(Err(_))` results.
|
||||
while fetch_responses
|
||||
.try_next()
|
||||
.await
|
||||
.context("Failed to drain FETCH responses")?
|
||||
.is_some()
|
||||
{}
|
||||
while fetch_responses.next().await.is_some() {}
|
||||
|
||||
if count != request_uids.len() {
|
||||
warn!(
|
||||
@@ -1520,7 +1477,7 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok((last_uid, received_msgs))
|
||||
}
|
||||
|
||||
/// Retrieves server metadata if it is supported.
|
||||
@@ -1698,7 +1655,7 @@ impl Session {
|
||||
.uid_store(uid_set, &query)
|
||||
.await
|
||||
.with_context(|| format!("IMAP failed to store: ({uid_set}, {query})"))?;
|
||||
while let Some(_response) = responses.try_next().await? {
|
||||
while let Some(_response) = responses.next().await {
|
||||
// Read all the responses
|
||||
}
|
||||
Ok(())
|
||||
|
||||
+25
-43
@@ -8,13 +8,15 @@ use tokio::io::BufWriter;
|
||||
|
||||
use super::capabilities::Capabilities;
|
||||
use crate::context::Context;
|
||||
use crate::log::{LoggingStream, info, warn};
|
||||
use crate::log::{info, warn};
|
||||
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
|
||||
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::net::tls::wrap_tls;
|
||||
use crate::net::{connect_tcp_inner, run_connection_attempts, update_connection_history};
|
||||
use crate::net::{
|
||||
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
|
||||
};
|
||||
use crate::tools::time;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -124,12 +126,12 @@ impl Client {
|
||||
);
|
||||
let res = match security {
|
||||
ConnectionSecurity::Tls => {
|
||||
Client::connect_secure(context, resolved_addr, host, strict_tls).await
|
||||
Client::connect_secure(resolved_addr, host, strict_tls).await
|
||||
}
|
||||
ConnectionSecurity::Starttls => {
|
||||
Client::connect_starttls(context, resolved_addr, host, strict_tls).await
|
||||
Client::connect_starttls(resolved_addr, host, strict_tls).await
|
||||
}
|
||||
ConnectionSecurity::Plain => Client::connect_insecure(context, resolved_addr).await,
|
||||
ConnectionSecurity::Plain => Client::connect_insecure(resolved_addr).await,
|
||||
};
|
||||
match res {
|
||||
Ok(client) => {
|
||||
@@ -200,61 +202,40 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_secure(
|
||||
context: &Context,
|
||||
addr: SocketAddr,
|
||||
hostname: &str,
|
||||
strict_tls: bool,
|
||||
) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp_inner(addr).await?;
|
||||
let account_id = context.get_id();
|
||||
let events = context.events.clone();
|
||||
let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, alpn(addr.port()), logging_stream).await?;
|
||||
async fn connect_secure(addr: SocketAddr, hostname: &str, strict_tls: bool) -> Result<Self> {
|
||||
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = Client::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn connect_insecure(context: &Context, addr: SocketAddr) -> Result<Self> {
|
||||
async fn connect_insecure(addr: SocketAddr) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp_inner(addr).await?;
|
||||
let account_id = context.get_id();
|
||||
let events = context.events.clone();
|
||||
let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
|
||||
let buffered_stream = BufWriter::new(logging_stream);
|
||||
let buffered_stream = BufWriter::new(tcp_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = Client::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn connect_starttls(
|
||||
context: &Context,
|
||||
addr: SocketAddr,
|
||||
host: &str,
|
||||
strict_tls: bool,
|
||||
) -> Result<Self> {
|
||||
async fn connect_starttls(addr: SocketAddr, host: &str, strict_tls: bool) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp_inner(addr).await?;
|
||||
|
||||
let account_id = context.get_id();
|
||||
let events = context.events.clone();
|
||||
let tcp_stream = LoggingStream::new(tcp_stream, account_id, events)?;
|
||||
|
||||
// Run STARTTLS command and convert the client back into a stream.
|
||||
let buffered_tcp_stream = BufWriter::new(tcp_stream);
|
||||
let mut client = async_imap::Client::new(buffered_tcp_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
client
|
||||
.run_command_and_check_ok("STARTTLS", None)
|
||||
.await
|
||||
@@ -265,6 +246,7 @@ impl Client {
|
||||
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
|
||||
.await
|
||||
.context("STARTTLS upgrade failed")?;
|
||||
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let client = Client::new(session_stream);
|
||||
@@ -287,8 +269,8 @@ impl Client {
|
||||
let mut client = Client::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
@@ -304,8 +286,8 @@ impl Client {
|
||||
let mut client = Client::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
@@ -325,8 +307,8 @@ impl Client {
|
||||
let mut client = ImapClient::new(buffered_proxy_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
client
|
||||
.run_command_and_check_ok("STARTTLS", None)
|
||||
.await
|
||||
|
||||
+1
-1
@@ -804,7 +804,7 @@ async fn export_database(
|
||||
"UPDATE backup.config SET value='0' WHERE keyname='verified_one_on_one_chats';",
|
||||
[],
|
||||
)
|
||||
.ok(); // Deprecated 2025-07. If verified_one_on_one_chats was not set, this errors, which we ignore
|
||||
.ok(); // If verified_one_on_one_chats was not set, this errors, which we ignore
|
||||
conn.execute("DETACH DATABASE backup", [])
|
||||
.context("failed to detach backup database")?;
|
||||
res?;
|
||||
|
||||
@@ -250,7 +250,7 @@ impl BackupProvider {
|
||||
Err(format_err!("Backup provider dropped"))
|
||||
}
|
||||
).await {
|
||||
error!(context, "Error while handling backup connection: {err:#}.");
|
||||
warn!(context, "Error while handling backup connection: {err:#}.");
|
||||
context.emit_event(EventType::ImexProgress(0));
|
||||
break;
|
||||
} else {
|
||||
@@ -367,8 +367,7 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
Err(format_err!("Backup reception cancelled"))
|
||||
})
|
||||
.await;
|
||||
if let Err(ref res) = res {
|
||||
error!(context, "{:#}", res);
|
||||
if res.is_err() {
|
||||
context.emit_event(EventType::ImexProgress(0));
|
||||
}
|
||||
context.free_ongoing().await;
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
|
||||
use crate::context::Context;
|
||||
|
||||
mod stream;
|
||||
|
||||
pub(crate) use stream::LoggingStream;
|
||||
|
||||
macro_rules! info {
|
||||
($ctx:expr, $msg:expr) => {
|
||||
info!($ctx, $msg,)
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
//! Stream that logs errors as events.
|
||||
//!
|
||||
//! This stream can be used to wrap IMAP,
|
||||
//! SMTP and HTTP streams so errors
|
||||
//! that occur are logged before
|
||||
//! they are processed.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use pin_project::pin_project;
|
||||
|
||||
use crate::events::{Event, EventType, Events};
|
||||
use crate::net::session::SessionStream;
|
||||
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Metrics {
|
||||
/// Total number of bytes read.
|
||||
pub total_read: usize,
|
||||
|
||||
/// Total number of bytes written.
|
||||
pub total_written: usize,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
total_read: 0,
|
||||
total_written: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stream that logs errors to the event channel.
|
||||
#[derive(Debug)]
|
||||
#[pin_project]
|
||||
pub(crate) struct LoggingStream<S: SessionStream> {
|
||||
#[pin]
|
||||
inner: S,
|
||||
|
||||
/// Account ID for logging.
|
||||
account_id: u32,
|
||||
|
||||
/// Event channel.
|
||||
events: Events,
|
||||
|
||||
/// Metrics for this stream.
|
||||
metrics: Metrics,
|
||||
|
||||
/// Peer address at the time of creation.
|
||||
///
|
||||
/// Socket may become disconnected later,
|
||||
/// so we save it when `LoggingStream` is created.
|
||||
peer_addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl<S: SessionStream> LoggingStream<S> {
|
||||
pub fn new(inner: S, account_id: u32, events: Events) -> Result<Self> {
|
||||
let peer_addr: SocketAddr = inner
|
||||
.peer_addr()
|
||||
.context("Attempt to create LoggingStream over an unconnected stream")?;
|
||||
Ok(Self {
|
||||
inner,
|
||||
account_id,
|
||||
events,
|
||||
metrics: Metrics::new(),
|
||||
peer_addr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: SessionStream> AsyncRead for LoggingStream<S> {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
let this = self.project();
|
||||
let old_remaining = buf.remaining();
|
||||
|
||||
let res = this.inner.poll_read(cx, buf);
|
||||
|
||||
if let Poll::Ready(Err(ref err)) = res {
|
||||
let peer_addr = this.peer_addr;
|
||||
let log_message = format!(
|
||||
"Read error on stream {peer_addr:?} after reading {} and writing {} bytes: {err}.",
|
||||
this.metrics.total_read, this.metrics.total_written
|
||||
);
|
||||
this.events.emit(Event {
|
||||
id: *this.account_id,
|
||||
typ: EventType::Warning(log_message),
|
||||
});
|
||||
}
|
||||
|
||||
let n = old_remaining - buf.remaining();
|
||||
this.metrics.total_read = this.metrics.total_read.saturating_add(n);
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: SessionStream> AsyncWrite for LoggingStream<S> {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<std::io::Result<usize>> {
|
||||
let this = self.project();
|
||||
let res = this.inner.poll_write(cx, buf);
|
||||
if let Poll::Ready(Ok(n)) = res {
|
||||
this.metrics.total_written = this.metrics.total_written.saturating_add(n);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
self.project().inner.poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
self.project().inner.poll_shutdown(cx)
|
||||
}
|
||||
|
||||
fn poll_write_vectored(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
bufs: &[std::io::IoSlice<'_>],
|
||||
) -> Poll<std::io::Result<usize>> {
|
||||
let this = self.project();
|
||||
let res = this.inner.poll_write_vectored(cx, bufs);
|
||||
if let Poll::Ready(Ok(n)) = res {
|
||||
this.metrics.total_written = this.metrics.total_written.saturating_add(n);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn is_write_vectored(&self) -> bool {
|
||||
self.inner.is_write_vectored()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: SessionStream> SessionStream for LoggingStream<S> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.inner.set_read_timeout(timeout)
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.inner.peer_addr()
|
||||
}
|
||||
}
|
||||
+3
-41
@@ -2,7 +2,6 @@
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::VecDeque;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str;
|
||||
|
||||
@@ -964,7 +963,6 @@ impl Message {
|
||||
| SystemMessage::SecurejoinMessage
|
||||
| SystemMessage::LocationStreamingEnabled
|
||||
| SystemMessage::LocationOnly
|
||||
| SystemMessage::ChatE2ee
|
||||
| SystemMessage::ChatProtectionEnabled
|
||||
| SystemMessage::ChatProtectionDisabled
|
||||
| SystemMessage::InvalidUnencryptedMail
|
||||
@@ -1714,35 +1712,6 @@ pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Resu
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_webxdc_info_messages(
|
||||
context: &Context,
|
||||
msg: &Message,
|
||||
) -> Result<Vec<MsgId>> {
|
||||
let msg_ids = context
|
||||
.sql
|
||||
.query_map(
|
||||
r#"SELECT id, param
|
||||
FROM msgs
|
||||
WHERE chat_id=?1 AND hidden=0 AND mime_in_reply_to = ?2
|
||||
"#,
|
||||
(msg.chat_id, &msg.rfc724_mid),
|
||||
|row| {
|
||||
let info_msg_id: MsgId = row.get(0)?;
|
||||
let last_param: Params = row.get::<_, String>(1)?.parse().unwrap_or_default();
|
||||
Ok((info_msg_id, last_param))
|
||||
},
|
||||
|row| {
|
||||
Ok(row
|
||||
.filter_map(Result::ok)
|
||||
.filter(|(_, param)| param.get_cmd() == SystemMessage::WebxdcInfoMessage)
|
||||
.map(|(msg_id, _)| msg_id)
|
||||
.collect::<Vec<_>>())
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(msg_ids)
|
||||
}
|
||||
|
||||
/// Do final events and jobs after batch deletion using calls to delete_msg_locally().
|
||||
/// To avoid additional database queries, collecting data is up to the caller.
|
||||
pub(crate) async fn delete_msgs_locally_done(
|
||||
@@ -1781,10 +1750,8 @@ pub async fn delete_msgs_ex(
|
||||
let mut modified_chat_ids = HashSet::new();
|
||||
let mut deleted_rfc724_mid = Vec::new();
|
||||
let mut res = Ok(());
|
||||
let mut msg_ids_queue = VecDeque::from_iter(msg_ids.iter().copied());
|
||||
let mut msg_ids = Vec::from(msg_ids);
|
||||
|
||||
while let Some(msg_id) = msg_ids_queue.pop_front() {
|
||||
for &msg_id in msg_ids {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
ensure!(
|
||||
!delete_for_all || msg.from_id == ContactId::SELF,
|
||||
@@ -1795,11 +1762,6 @@ pub async fn delete_msgs_ex(
|
||||
"Cannot request deletion of unencrypted message for others"
|
||||
);
|
||||
|
||||
if msg.viewtype == Viewtype::Webxdc {
|
||||
let info_msgs = get_webxdc_info_messages(context, &msg).await?;
|
||||
msg_ids_queue.extend(&info_msgs);
|
||||
msg_ids.extend(info_msgs);
|
||||
}
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
deleted_rfc724_mid.push(msg.rfc724_mid.clone());
|
||||
|
||||
@@ -1845,11 +1807,11 @@ pub async fn delete_msgs_ex(
|
||||
.await?;
|
||||
}
|
||||
|
||||
for &msg_id in &msg_ids {
|
||||
for &msg_id in msg_ids {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
delete_msg_locally(context, &msg).await?;
|
||||
}
|
||||
delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
|
||||
delete_msgs_locally_done(context, msg_ids, modified_chat_ids).await?;
|
||||
|
||||
// Interrupt Inbox loop to start message deletion, run housekeeping and call send_sync_msg().
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::config::Config;
|
||||
use crate::reaction::send_reaction;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils;
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager};
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
#[test]
|
||||
fn test_guess_msgtype_from_suffix() {
|
||||
@@ -347,7 +347,7 @@ async fn test_markseen_msgs() -> Result<()> {
|
||||
let chats = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?;
|
||||
assert_eq!(msgs.len(), E2EE_INFO_MSGS + 2);
|
||||
assert_eq!(msgs.len(), 2);
|
||||
assert_eq!(bob.get_fresh_msgs().await?.len(), 0);
|
||||
|
||||
// that has no effect in contact request
|
||||
@@ -358,7 +358,7 @@ async fn test_markseen_msgs() -> Result<()> {
|
||||
assert_eq!(bob_chat.blocked, Blocked::Request);
|
||||
|
||||
let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?;
|
||||
assert_eq!(msgs.len(), E2EE_INFO_MSGS + 2);
|
||||
assert_eq!(msgs.len(), 2);
|
||||
bob_chat_id.accept(&bob).await.unwrap();
|
||||
|
||||
// bob sends to alice,
|
||||
@@ -761,22 +761,19 @@ async fn test_delete_msgs_sync() -> Result<()> {
|
||||
|
||||
// Alice sends a messsage and receives it on the other device
|
||||
let sent1 = alice.send_text(alice_chat_id, "foo").await;
|
||||
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 1);
|
||||
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, 1);
|
||||
|
||||
let msg = alice2.recv_msg(&sent1).await;
|
||||
let alice2_chat_id = msg.chat_id;
|
||||
assert_eq!(alice2.get_last_msg_in(alice2_chat_id).await.id, msg.id);
|
||||
assert_eq!(
|
||||
alice2_chat_id.get_msg_cnt(alice2).await?,
|
||||
E2EE_INFO_MSGS + 1
|
||||
);
|
||||
assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, 1);
|
||||
|
||||
// Alice deletes the message; this should happen on both devices as well
|
||||
delete_msgs(alice, &[sent1.sender_msg_id]).await?;
|
||||
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS);
|
||||
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, 0);
|
||||
|
||||
test_utils::sync(alice, alice2).await;
|
||||
assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, E2EE_INFO_MSGS);
|
||||
assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -808,22 +805,3 @@ async fn test_sanitize_filename_message() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that empty file can be sent and received.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_empty_file() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file_from_bytes(alice, "myfile", b"", None)?;
|
||||
chat::send_msg(alice, alice_chat.id, &mut msg).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
let bob_received_msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_received_msg.get_filename().unwrap(), "myfile");
|
||||
assert_eq!(bob_received_msg.get_viewtype(), Viewtype::File);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+66
-53
@@ -5,9 +5,7 @@ use std::io::Cursor;
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use base64::Engine as _;
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
use deltachat_contact_tools::sanitize_bidi_characters;
|
||||
use iroh_gossip::proto::TopicId;
|
||||
use mail_builder::headers::HeaderType;
|
||||
use mail_builder::headers::address::{Address, EmailAddress};
|
||||
use mail_builder::mime::MimePart;
|
||||
@@ -24,14 +22,14 @@ use crate::context::Context;
|
||||
use crate::e2ee::EncryptHelper;
|
||||
use crate::ensure_and_debug_assert;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::key::{DcKey, SignedPublicKey, self_fingerprint};
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::location;
|
||||
use crate::log::{info, warn};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{SystemMessage, is_hidden};
|
||||
use crate::param::Param;
|
||||
use crate::peer_channels::{create_iroh_header, get_iroh_topic_for_msg};
|
||||
use crate::peer_channels::create_iroh_header;
|
||||
use crate::simplify::escape_message_footer_marks;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{
|
||||
@@ -142,9 +140,6 @@ pub struct MimeFactory {
|
||||
|
||||
/// True if the avatar should be attached.
|
||||
pub attach_selfavatar: bool,
|
||||
|
||||
/// This field is used to sustain the topic id of webxdcs needed for peer channels.
|
||||
webxdc_topic: Option<TopicId>,
|
||||
}
|
||||
|
||||
/// Result of rendering a message, ready to be submitted to a send job.
|
||||
@@ -253,10 +248,6 @@ impl MimeFactory {
|
||||
let mut missing_key_addresses = BTreeSet::new();
|
||||
context
|
||||
.sql
|
||||
// Sort recipients by `add_timestamp DESC` so that if the group is large and there
|
||||
// are multiple SMTP messages, a newly added member receives the member addition
|
||||
// message earlier and has gossiped keys of other members (otherwise the new member
|
||||
// may receive messages from other members earlier and fail to verify them).
|
||||
.query_map(
|
||||
"SELECT
|
||||
c.authname,
|
||||
@@ -270,8 +261,7 @@ impl MimeFactory {
|
||||
LEFT JOIN contacts c ON cc.contact_id=c.id
|
||||
LEFT JOIN public_keys k ON k.fingerprint=c.fingerprint
|
||||
WHERE cc.chat_id=?
|
||||
AND (cc.contact_id>9 OR (cc.contact_id=1 AND ?))
|
||||
ORDER BY cc.add_timestamp DESC",
|
||||
AND (cc.contact_id>9 OR (cc.contact_id=1 AND ?))",
|
||||
(msg.chat_id, chat.typ == Chattype::Group),
|
||||
|row| {
|
||||
let authname: String = row.get(0)?;
|
||||
@@ -378,14 +368,8 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
|
||||
ensure_and_debug_assert!(
|
||||
member_timestamps.len() >= to.len(),
|
||||
"member_timestamps.len() ({}) < to.len() ({})",
|
||||
member_timestamps.len(), to.len());
|
||||
ensure_and_debug_assert!(
|
||||
member_fingerprints.is_empty() || member_fingerprints.len() >= to.len(),
|
||||
"member_fingerprints.len() ({}) < to.len() ({})",
|
||||
member_fingerprints.len(), to.len());
|
||||
ensure_and_debug_assert!(member_timestamps.len() >= to.len());
|
||||
ensure_and_debug_assert!(member_fingerprints.is_empty() || member_fingerprints.len() >= to.len());
|
||||
|
||||
if to.len() > 1 {
|
||||
if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
|
||||
@@ -464,13 +448,9 @@ impl MimeFactory {
|
||||
|
||||
ensure_and_debug_assert!(
|
||||
member_timestamps.is_empty()
|
||||
|| to.len() + past_members.len() == member_timestamps.len(),
|
||||
"to.len() ({}) + past_members.len() ({}) != member_timestamps.len() ({})",
|
||||
to.len(),
|
||||
past_members.len(),
|
||||
member_timestamps.len(),
|
||||
|| to.len() + past_members.len() == member_timestamps.len()
|
||||
);
|
||||
let webxdc_topic = get_iroh_topic_for_msg(context, msg.id).await?;
|
||||
|
||||
let factory = MimeFactory {
|
||||
from_addr,
|
||||
from_displayname,
|
||||
@@ -490,7 +470,6 @@ impl MimeFactory {
|
||||
last_added_location_id: None,
|
||||
sync_ids_to_delete: None,
|
||||
attach_selfavatar,
|
||||
webxdc_topic,
|
||||
};
|
||||
Ok(factory)
|
||||
}
|
||||
@@ -538,7 +517,6 @@ impl MimeFactory {
|
||||
last_added_location_id: None,
|
||||
sync_ids_to_delete: None,
|
||||
attach_selfavatar: false,
|
||||
webxdc_topic: None,
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
@@ -693,11 +671,7 @@ impl MimeFactory {
|
||||
|
||||
ensure_and_debug_assert!(
|
||||
self.member_timestamps.is_empty()
|
||||
|| to.len() + past_members.len() == self.member_timestamps.len(),
|
||||
"to.len() ({}) + past_members.len() ({}) != self.member_timestamps.len() ({})",
|
||||
to.len(),
|
||||
past_members.len(),
|
||||
self.member_timestamps.len(),
|
||||
|| to.len() + past_members.len() == self.member_timestamps.len()
|
||||
);
|
||||
if to.is_empty() {
|
||||
to.push(hidden_recipients());
|
||||
@@ -815,7 +789,7 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
|
||||
if let Loaded::Message { chat, .. } = &self.loaded {
|
||||
if let Loaded::Message { msg, chat } = &self.loaded {
|
||||
if chat.typ == Chattype::OutBroadcast || chat.typ == Chattype::InBroadcast {
|
||||
headers.push((
|
||||
"List-ID",
|
||||
@@ -825,6 +799,15 @@ impl MimeFactory {
|
||||
))
|
||||
.into(),
|
||||
));
|
||||
|
||||
if msg.param.get_cmd() == SystemMessage::MemberAddedToGroup {
|
||||
if let Some(secret) = chat.param.get(Param::SymmetricKey) {
|
||||
headers.push((
|
||||
"Chat-Broadcast-Secret",
|
||||
mail_builder::headers::text::Text::new(secret.to_string()).into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1005,6 +988,15 @@ impl MimeFactory {
|
||||
} else {
|
||||
unprotected_headers.push(header.clone());
|
||||
}
|
||||
} else if header_name == "chat-broadcast-secret" {
|
||||
if is_encrypted {
|
||||
protected_headers.push(header.clone());
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Message is unnecrypted, not including broadcast secret"
|
||||
);
|
||||
}
|
||||
} else if is_encrypted {
|
||||
protected_headers.push(header.clone());
|
||||
|
||||
@@ -1143,18 +1135,43 @@ impl MimeFactory {
|
||||
Loaded::Mdn { .. } => true,
|
||||
};
|
||||
|
||||
// Encrypt to self unconditionally,
|
||||
// even for a single-device setup.
|
||||
let mut encryption_keyring = vec![encrypt_helper.public_key.clone()];
|
||||
encryption_keyring.extend(encryption_keys.iter().map(|(_addr, key)| (*key).clone()));
|
||||
let symmetric_key = match &self.loaded {
|
||||
Loaded::Message { chat, .. } if chat.typ == Chattype::OutBroadcast => {
|
||||
// If there is no symmetric key yet
|
||||
// (because this is an old broadcast channel,
|
||||
// created before we had symmetric encryption),
|
||||
// we just encrypt asymmetrically.
|
||||
// Symmetric encryption exists since 2025-08;
|
||||
// some time after that, we can think about requiring everyone
|
||||
// to switch to symmetrically-encrypted broadcast lists.
|
||||
chat.param.get(Param::SymmetricKey)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let encrypted = if let Some(symmetric_key) = symmetric_key {
|
||||
info!(context, "Symmetrically encrypting for broadcast channel.");
|
||||
encrypt_helper
|
||||
.encrypt_for_broadcast(context, symmetric_key, message, compress)
|
||||
.await?
|
||||
} else {
|
||||
// Asymmetric encryption
|
||||
|
||||
// Encrypt to self unconditionally,
|
||||
// even for a single-device setup.
|
||||
let mut encryption_keyring = vec![encrypt_helper.public_key.clone()];
|
||||
encryption_keyring
|
||||
.extend(encryption_keys.iter().map(|(_addr, key)| (*key).clone()));
|
||||
|
||||
encrypt_helper
|
||||
.encrypt(context, encryption_keyring, message, compress)
|
||||
.await?
|
||||
};
|
||||
|
||||
// XXX: additional newline is needed
|
||||
// to pass filtermail at
|
||||
// <https://github.com/deltachat/chatmail/blob/4d915f9800435bf13057d41af8d708abd34dbfa8/chatmaild/src/chatmaild/filtermail.py#L84-L86>
|
||||
let encrypted = encrypt_helper
|
||||
.encrypt(context, encryption_keyring, message, compress)
|
||||
.await?
|
||||
+ "\n";
|
||||
// <https://github.com/deltachat/chatmail/blob/4d915f9800435bf13057d41af8d708abd34dbfa8/chatmaild/src/chatmaild/filtermail.py#L84-L86>:
|
||||
let encrypted = encrypted + "\n";
|
||||
|
||||
// Set the appropriate Content-Type for the outer message
|
||||
MimePart::new(
|
||||
@@ -1387,7 +1404,6 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
SystemMessage::MemberAddedToGroup => {
|
||||
ensure!(chat.typ != Chattype::OutBroadcast);
|
||||
// TODO: lookup the contact by ID rather than email address.
|
||||
// We are adding key-contacts, the cannot be looked up by address.
|
||||
let email_to_add = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
@@ -1522,7 +1538,7 @@ impl MimeFactory {
|
||||
}
|
||||
SystemMessage::IrohNodeAddr => {
|
||||
headers.push((
|
||||
HeaderDef::IrohNodeAddr.into(),
|
||||
"Iroh-Node-Addr",
|
||||
mail_builder::headers::text::Text::new(serde_json::to_string(
|
||||
&context
|
||||
.get_or_try_init_peer_channel()
|
||||
@@ -1703,13 +1719,10 @@ impl MimeFactory {
|
||||
let json = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
parts.push(context.build_status_update_part(json));
|
||||
} else if msg.viewtype == Viewtype::Webxdc {
|
||||
let topic = self
|
||||
.webxdc_topic
|
||||
.map(|top| BASE32_NOPAD.encode(top.as_bytes()).to_ascii_lowercase())
|
||||
.unwrap_or(create_iroh_header(context, msg.id).await?);
|
||||
headers.push((
|
||||
HeaderDef::IrohGossipTopic.get_headername(),
|
||||
mail_builder::headers::raw::Raw::new(topic).into(),
|
||||
"Iroh-Gossip-Topic",
|
||||
mail_builder::headers::raw::Raw::new(create_iroh_header(context, msg.id).await?)
|
||||
.into(),
|
||||
));
|
||||
if let (Some(json), _) = context
|
||||
.render_webxdc_status_update_object(
|
||||
|
||||
@@ -2,7 +2,6 @@ use deltachat_contact_tools::ContactAddress;
|
||||
use mail_builder::headers::Header;
|
||||
use mailparse::{MailHeaderMap, addrparse_header};
|
||||
use std::str;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{
|
||||
@@ -17,7 +16,6 @@ use crate::message;
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager, get_chat_msg};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
fn render_email_address(display_name: &str, addr: &str) -> String {
|
||||
let mut output = Vec::<u8>::new();
|
||||
@@ -683,7 +681,6 @@ async fn test_selfavatar_unencrypted_signed() {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap();
|
||||
assert_eq!(alice_contact.is_key_contact(), false);
|
||||
assert!(
|
||||
alice_contact
|
||||
.get_profile_image(&bob.ctx)
|
||||
@@ -870,43 +867,6 @@ async fn test_dont_remove_self() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_new_member_is_first_recipient() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let charlie_id = alice.add_or_lookup_contact_id(charlie).await;
|
||||
|
||||
let group = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "Group", &[bob])
|
||||
.await;
|
||||
alice.send_text(group, "Hi! I created a group.").await;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(60));
|
||||
add_contact_to_chat(alice, group, charlie_id).await?;
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
assert!(
|
||||
sent_msg
|
||||
.recipients
|
||||
.starts_with(&charlie.get_config(Config::Addr).await?.unwrap())
|
||||
);
|
||||
|
||||
remove_contact_from_chat(alice, group, bob_id).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
SystemTime::shift(Duration::from_secs(60));
|
||||
add_contact_to_chat(alice, group, bob_id).await?;
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
assert!(
|
||||
sent_msg
|
||||
.recipients
|
||||
.starts_with(&bob.get_config(Config::Addr).await?.unwrap())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test: mimefactory should never create an empty to header,
|
||||
/// also not if the Selftalk parameter is missing
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
+109
-90
@@ -181,10 +181,10 @@ pub enum SystemMessage {
|
||||
/// Chat ephemeral message timer is changed.
|
||||
EphemeralTimerChanged = 10,
|
||||
|
||||
/// "Messages are end-to-end encrypted."
|
||||
/// "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
ChatProtectionEnabled = 11,
|
||||
|
||||
/// "%1$s sent a message from another device.", deprecated 2025-07
|
||||
/// "%1$s sent a message from another device."
|
||||
ChatProtectionDisabled = 12,
|
||||
|
||||
/// Message can't be sent because of `Invalid unencrypted mail to <>`
|
||||
@@ -213,9 +213,6 @@ pub enum SystemMessage {
|
||||
|
||||
/// This message contains a users iroh node address.
|
||||
IrohNodeAddr = 40,
|
||||
|
||||
/// "Messages are end-to-end encrypted."
|
||||
ChatE2ee = 50,
|
||||
}
|
||||
|
||||
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
|
||||
@@ -249,7 +246,6 @@ impl MimeMessage {
|
||||
MimeMessage::merge_headers(
|
||||
context,
|
||||
&mut headers,
|
||||
&mut headers_removed,
|
||||
&mut recipients,
|
||||
&mut past_members,
|
||||
&mut from,
|
||||
@@ -277,7 +273,6 @@ impl MimeMessage {
|
||||
MimeMessage::merge_headers(
|
||||
context,
|
||||
&mut headers,
|
||||
&mut headers_removed,
|
||||
&mut recipients,
|
||||
&mut past_members,
|
||||
&mut from,
|
||||
@@ -338,50 +333,63 @@ impl MimeMessage {
|
||||
|
||||
let mail_raw; // Memory location for a possible decrypted message.
|
||||
let decrypted_msg; // Decrypted signed OpenPGP message.
|
||||
let symmetric_secrets: Vec<String> = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT secret FROM broadcasts_shared_secrets",
|
||||
(),
|
||||
|row| row.get(0),
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (mail, is_encrypted) =
|
||||
match tokio::task::block_in_place(|| try_decrypt(&mail, &private_keyring)) {
|
||||
Ok(Some(mut msg)) => {
|
||||
mail_raw = msg.as_data_vec().unwrap_or_default();
|
||||
let (mail, is_encrypted) = match tokio::task::block_in_place(|| {
|
||||
try_decrypt(&mail, &private_keyring, &symmetric_secrets)
|
||||
}) {
|
||||
Ok(Some(mut msg)) => {
|
||||
mail_raw = msg.as_data_vec().unwrap_or_default();
|
||||
|
||||
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
||||
info!(
|
||||
context,
|
||||
"decrypted message mime-body:\n{}",
|
||||
String::from_utf8_lossy(&mail_raw),
|
||||
);
|
||||
}
|
||||
|
||||
decrypted_msg = Some(msg);
|
||||
|
||||
timestamp_sent = Self::get_timestamp_sent(
|
||||
&decrypted_mail.headers,
|
||||
timestamp_sent,
|
||||
timestamp_rcvd,
|
||||
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
||||
info!(
|
||||
context,
|
||||
"decrypted message mime-body:\n{}",
|
||||
String::from_utf8_lossy(&mail_raw),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(protected_aheader_value) = decrypted_mail
|
||||
.headers
|
||||
.get_header_value(HeaderDef::Autocrypt)
|
||||
{
|
||||
aheader_value = Some(protected_aheader_value);
|
||||
}
|
||||
decrypted_msg = Some(msg);
|
||||
|
||||
(Ok(decrypted_mail), true)
|
||||
timestamp_sent = Self::get_timestamp_sent(
|
||||
&decrypted_mail.headers,
|
||||
timestamp_sent,
|
||||
timestamp_rcvd,
|
||||
);
|
||||
|
||||
if let Some(protected_aheader_value) = decrypted_mail
|
||||
.headers
|
||||
.get_header_value(HeaderDef::Autocrypt)
|
||||
{
|
||||
aheader_value = Some(protected_aheader_value);
|
||||
}
|
||||
Ok(None) => {
|
||||
mail_raw = Vec::new();
|
||||
decrypted_msg = None;
|
||||
(Ok(mail), false)
|
||||
}
|
||||
Err(err) => {
|
||||
mail_raw = Vec::new();
|
||||
decrypted_msg = None;
|
||||
warn!(context, "decryption failed: {:#}", err);
|
||||
(Err(err), false)
|
||||
}
|
||||
};
|
||||
|
||||
(Ok(decrypted_mail), true)
|
||||
}
|
||||
Ok(None) => {
|
||||
mail_raw = Vec::new();
|
||||
decrypted_msg = None;
|
||||
(Ok(mail), false)
|
||||
}
|
||||
Err(err) => {
|
||||
mail_raw = Vec::new();
|
||||
decrypted_msg = None;
|
||||
warn!(context, "decryption failed: {:#}", err);
|
||||
(Err(err), false)
|
||||
}
|
||||
};
|
||||
|
||||
let autocrypt_header = if !incoming {
|
||||
None
|
||||
@@ -451,11 +459,26 @@ impl MimeMessage {
|
||||
});
|
||||
if let (Ok(mail), true) = (mail, is_encrypted) {
|
||||
if !signatures.is_empty() {
|
||||
// Unsigned "Subject" mustn't be prepended to messages shown as encrypted
|
||||
// (<https://github.com/deltachat/deltachat-core-rust/issues/1790>).
|
||||
// Other headers are removed by `MimeMessage::merge_headers()` except for "List-ID".
|
||||
remove_header(&mut headers, "subject", &mut headers_removed);
|
||||
remove_header(&mut headers, "list-id", &mut headers_removed);
|
||||
// Remove unsigned opportunistically protected headers from messages considered
|
||||
// Autocrypt-encrypted / displayed with padlock.
|
||||
// For "Subject" see <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
|
||||
for h in [
|
||||
HeaderDef::Subject,
|
||||
HeaderDef::ChatGroupId,
|
||||
HeaderDef::ChatGroupName,
|
||||
HeaderDef::ChatGroupNameChanged,
|
||||
HeaderDef::ChatGroupNameTimestamp,
|
||||
HeaderDef::ChatGroupAvatar,
|
||||
HeaderDef::ChatGroupMemberRemoved,
|
||||
HeaderDef::ChatGroupMemberAdded,
|
||||
HeaderDef::ChatGroupMemberTimestamps,
|
||||
HeaderDef::ChatGroupPastMembers,
|
||||
HeaderDef::ChatDelete,
|
||||
HeaderDef::ChatEdit,
|
||||
HeaderDef::ChatUserAvatar,
|
||||
] {
|
||||
remove_header(&mut headers, h.get_headername(), &mut headers_removed);
|
||||
}
|
||||
}
|
||||
|
||||
// let known protected headers from the decrypted
|
||||
@@ -468,7 +491,6 @@ impl MimeMessage {
|
||||
MimeMessage::merge_headers(
|
||||
context,
|
||||
&mut headers,
|
||||
&mut headers_removed,
|
||||
&mut recipients,
|
||||
&mut past_members,
|
||||
&mut inner_from,
|
||||
@@ -1322,6 +1344,10 @@ impl MimeMessage {
|
||||
filename: &str,
|
||||
is_related: bool,
|
||||
) -> Result<()> {
|
||||
if decoded_data.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Process attached PGP keys.
|
||||
if mime_type.type_() == mime::APPLICATION
|
||||
&& mime_type.subtype().as_str() == "pgp-keys"
|
||||
@@ -1545,7 +1571,6 @@ impl MimeMessage {
|
||||
fn merge_headers(
|
||||
context: &Context,
|
||||
headers: &mut HashMap<String, String>,
|
||||
headers_removed: &mut HashSet<String>,
|
||||
recipients: &mut Vec<SingleInfo>,
|
||||
past_members: &mut Vec<SingleInfo>,
|
||||
from: &mut Option<SingleInfo>,
|
||||
@@ -1553,25 +1578,23 @@ impl MimeMessage {
|
||||
chat_disposition_notification_to: &mut Option<SingleInfo>,
|
||||
fields: &[mailparse::MailHeader<'_>],
|
||||
) {
|
||||
headers.retain(|k, _| {
|
||||
!is_protected(k) || {
|
||||
headers_removed.insert(k.to_string());
|
||||
false
|
||||
}
|
||||
});
|
||||
for field in fields {
|
||||
// lowercasing all headers is technically not correct, but makes things work better
|
||||
let key = field.get_key().to_lowercase();
|
||||
if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
|
||||
match addrparse_header(field) {
|
||||
Ok(addrlist) => {
|
||||
*chat_disposition_notification_to = addrlist.extract_single_info();
|
||||
if !headers.contains_key(&key) || // key already exists, only overwrite known types (protected headers)
|
||||
is_known(&key) || key.starts_with("chat-")
|
||||
{
|
||||
if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
|
||||
match addrparse_header(field) {
|
||||
Ok(addrlist) => {
|
||||
*chat_disposition_notification_to = addrlist.extract_single_info();
|
||||
}
|
||||
Err(e) => warn!(context, "Could not read {} address: {}", key, e),
|
||||
}
|
||||
Err(e) => warn!(context, "Could not read {} address: {}", key, e),
|
||||
} else {
|
||||
let value = field.get_value();
|
||||
headers.insert(key.to_string(), value);
|
||||
}
|
||||
} else {
|
||||
let value = field.get_value();
|
||||
headers.insert(key.to_string(), value);
|
||||
}
|
||||
}
|
||||
let recipients_new = get_recipients(fields);
|
||||
@@ -1999,30 +2022,26 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the outer header value must be ignored if the message contains a signed (and
|
||||
/// optionally encrypted) part.
|
||||
///
|
||||
/// NB: There are known cases when Subject and List-ID only appear in the outer headers of
|
||||
/// signed-only messages. Such messages are shown as unencrypted anyway.
|
||||
fn is_protected(key: &str) -> bool {
|
||||
key.starts_with("chat-")
|
||||
|| matches!(
|
||||
key,
|
||||
"return-path"
|
||||
| "auto-submitted"
|
||||
| "autocrypt-setup-message"
|
||||
| "date"
|
||||
| "from"
|
||||
| "sender"
|
||||
| "reply-to"
|
||||
| "to"
|
||||
| "cc"
|
||||
| "bcc"
|
||||
| "message-id"
|
||||
| "in-reply-to"
|
||||
| "references"
|
||||
| "secure-join"
|
||||
)
|
||||
/// Returns true if the header overwrites outer header
|
||||
/// when it comes from protected headers.
|
||||
fn is_known(key: &str) -> bool {
|
||||
matches!(
|
||||
key,
|
||||
"return-path"
|
||||
| "date"
|
||||
| "from"
|
||||
| "sender"
|
||||
| "reply-to"
|
||||
| "to"
|
||||
| "cc"
|
||||
| "bcc"
|
||||
| "message-id"
|
||||
| "in-reply-to"
|
||||
| "references"
|
||||
| "subject"
|
||||
| "secure-join"
|
||||
| "list-id"
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns if the header is hidden and must be ignored in the IMF section.
|
||||
|
||||
@@ -1402,26 +1402,6 @@ async fn test_x_microsoft_original_message_id_precedence() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_extra_imf_chat_header() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let chat_id = t.get_self_chat().await.id;
|
||||
|
||||
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
|
||||
let sent_msg = t.pop_sent_msg().await;
|
||||
// Check removal of some nonexistent "Chat-*" header to protect the code from future breakages.
|
||||
let payload = sent_msg
|
||||
.payload
|
||||
.replace("Message-ID:", "Chat-Forty-Two: 42\r\nMessage-ID:");
|
||||
let msg = MimeMessage::from_bytes(t, payload.as_bytes(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(msg.headers.contains_key("chat-version"));
|
||||
assert!(!msg.headers.contains_key("chat-forty-two"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_long_in_reply_to() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
+3
-3
@@ -244,7 +244,7 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result<Response> {
|
||||
.clone();
|
||||
|
||||
let req = hyper::Request::builder()
|
||||
.uri(parsed_url)
|
||||
.uri(parsed_url.path())
|
||||
.header(hyper::header::HOST, authority.as_str())
|
||||
.body(http_body_util::Empty::<Bytes>::new())?;
|
||||
let response = sender.send_request(req).await?;
|
||||
@@ -378,7 +378,7 @@ pub(crate) async fn post_string(context: &Context, url: &str, body: String) -> R
|
||||
.context("URL has no authority")?
|
||||
.clone();
|
||||
|
||||
let request = hyper::Request::post(parsed_url)
|
||||
let request = hyper::Request::post(parsed_url.path())
|
||||
.header(hyper::header::HOST, authority.as_str())
|
||||
.body(body)?;
|
||||
let response = sender.send_request(request).await?;
|
||||
@@ -408,7 +408,7 @@ pub(crate) async fn post_form<T: Serialize + ?Sized>(
|
||||
.authority()
|
||||
.context("URL has no authority")?
|
||||
.clone();
|
||||
let request = hyper::Request::post(parsed_url)
|
||||
let request = hyper::Request::post(parsed_url.path())
|
||||
.header(hyper::header::HOST, authority.as_str())
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.body(encoded_body)?;
|
||||
|
||||
+3
-42
@@ -1,10 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use fast_socks5::client::Socks5Stream;
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncBufRead, AsyncRead, AsyncWrite, BufStream, BufWriter};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
|
||||
pub(crate) trait SessionStream:
|
||||
@@ -12,90 +9,54 @@ pub(crate) trait SessionStream:
|
||||
{
|
||||
/// Change the read timeout on the session stream.
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>);
|
||||
|
||||
/// Returns the remote address that this stream is connected to.
|
||||
fn peer_addr(&self) -> Result<SocketAddr>;
|
||||
}
|
||||
|
||||
impl SessionStream for Box<dyn SessionStream> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.as_mut().set_read_timeout(timeout);
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.as_ref().peer_addr()
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for async_native_tls::TlsStream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().set_read_timeout(timeout);
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.get_ref().peer_addr()
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for tokio_rustls::client::TlsStream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().0.set_read_timeout(timeout);
|
||||
}
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.get_ref().0.peer_addr()
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for BufStream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().set_read_timeout(timeout);
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.get_ref().peer_addr()
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for BufWriter<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().set_read_timeout(timeout);
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.get_ref().peer_addr()
|
||||
}
|
||||
}
|
||||
impl SessionStream for Pin<Box<TimeoutStream<TcpStream>>> {
|
||||
impl<T: AsyncRead + AsyncWrite + Send + Sync + std::fmt::Debug> SessionStream
|
||||
for Pin<Box<TimeoutStream<T>>>
|
||||
{
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.as_mut().set_read_timeout_pinned(timeout);
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
Ok(self.get_ref().peer_addr()?)
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for Socks5Stream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_socket_mut().set_read_timeout(timeout)
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.get_socket_ref().peer_addr()
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for shadowsocks::ProxyClientStream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().set_read_timeout(timeout)
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.get_ref().peer_addr()
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for async_imap::DeflateStream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().set_read_timeout(timeout)
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.get_ref().peer_addr()
|
||||
}
|
||||
}
|
||||
|
||||
/// Session stream with a read buffer.
|
||||
|
||||
@@ -169,6 +169,13 @@ pub enum Param {
|
||||
/// post something to the mailing list.
|
||||
ListPost = b'p',
|
||||
|
||||
/// For Chats and Messages:
|
||||
/// For chats of type [`Chattype::OutBroadcast`] and [`Chattype::InBroadcast`] // TODO (or just OutBroadcast)
|
||||
/// and for messages adding members to such a chat.
|
||||
/// The symmetric key shared among all chat participants,
|
||||
/// used to encrypt and decrypt messages.
|
||||
SymmetricKey = b'z', // TODO remove this again
|
||||
|
||||
/// For Contacts: If this is the List-Post address of a mailing list, contains
|
||||
/// the List-Id of the mailing list (which is also used as the group id of the chat).
|
||||
ListId = b's',
|
||||
|
||||
+25
-125
@@ -48,13 +48,13 @@ use crate::mimeparser::SystemMessage;
|
||||
const PUBLIC_KEY_LENGTH: usize = 32;
|
||||
const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
|
||||
|
||||
/// Store Iroh peer channels for the context.
|
||||
/// Store iroh peer channels for the context.
|
||||
#[derive(Debug)]
|
||||
pub struct Iroh {
|
||||
/// Iroh router needed for Iroh peer channels.
|
||||
/// iroh router needed for iroh peer channels.
|
||||
pub(crate) router: iroh::protocol::Router,
|
||||
|
||||
/// [Gossip] needed for Iroh peer channels.
|
||||
/// [Gossip] needed for iroh peer channels.
|
||||
pub(crate) gossip: Gossip,
|
||||
|
||||
/// Sequence numbers for gossip channels.
|
||||
@@ -109,7 +109,7 @@ impl Iroh {
|
||||
|
||||
info!(
|
||||
ctx,
|
||||
"IROH_REALTIME: Joining gossip {topic} with peers: {:?}.", node_ids,
|
||||
"IROH_REALTIME: Joining gossip with peers: {:?}", node_ids,
|
||||
);
|
||||
|
||||
// Inform iroh of potentially new node addresses
|
||||
@@ -138,11 +138,17 @@ impl Iroh {
|
||||
Ok(Some(join_rx))
|
||||
}
|
||||
|
||||
/// Add gossip peer to realtime channel if it is already active.
|
||||
pub async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: NodeAddr) -> Result<()> {
|
||||
/// Add gossip peers to realtime channel if it is already active.
|
||||
pub async fn maybe_add_gossip_peers(&self, topic: TopicId, peers: Vec<NodeAddr>) -> Result<()> {
|
||||
if self.iroh_channels.read().await.get(&topic).is_some() {
|
||||
self.router.endpoint().add_node_addr(peer.clone())?;
|
||||
self.gossip.subscribe(topic, vec![peer.node_id])?;
|
||||
for peer in &peers {
|
||||
self.router.endpoint().add_node_addr(peer.clone())?;
|
||||
}
|
||||
|
||||
self.gossip.subscribe_with_opts(
|
||||
topic,
|
||||
JoinOptions::with_bootstrap(peers.into_iter().map(|peer| peer.node_id)),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -310,17 +316,6 @@ impl Context {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: NodeAddr) -> Result<()> {
|
||||
if let Some(iroh) = &*self.iroh.read().await {
|
||||
info!(
|
||||
self,
|
||||
"Adding (maybe existing) peer with id {} to {topic}.", peer.node_id
|
||||
);
|
||||
iroh.maybe_add_gossip_peer(topic, peer).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache a peers [NodeId] for one topic.
|
||||
@@ -353,13 +348,12 @@ pub async fn add_gossip_peer_from_header(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let node_addr =
|
||||
serde_json::from_str::<NodeAddr>(node_addr).context("Failed to parse node address")?;
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Adding iroh peer with node id {} to the topic of {instance_id}.", node_addr.node_id
|
||||
"Adding iroh peer with address {node_addr:?} to the topic of {instance_id}."
|
||||
);
|
||||
let node_addr =
|
||||
serde_json::from_str::<NodeAddr>(node_addr).context("Failed to parse node address")?;
|
||||
|
||||
context.emit_event(EventType::WebxdcRealtimeAdvertisementReceived {
|
||||
msg_id: instance_id,
|
||||
@@ -377,7 +371,8 @@ pub async fn add_gossip_peer_from_header(
|
||||
let relay_server = node_addr.relay_url().map(|relay| relay.as_str());
|
||||
iroh_add_peer_for_topic(context, instance_id, topic, node_id, relay_server).await?;
|
||||
|
||||
context.maybe_add_gossip_peer(topic, node_addr).await?;
|
||||
let iroh = context.get_or_try_init_peer_channel().await?;
|
||||
iroh.maybe_add_gossip_peers(topic, vec![node_addr]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -560,9 +555,9 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
EventType,
|
||||
chat::{self, ChatId, ProtectionStatus, add_contact_to_chat, resend_msgs, send_msg},
|
||||
chat::send_msg,
|
||||
message::{Message, Viewtype},
|
||||
test_utils::{TestContext, TestContextManager},
|
||||
test_utils::TestContextManager,
|
||||
};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -929,30 +924,8 @@ mod tests {
|
||||
let alice = &mut tcm.alice().await;
|
||||
let bob = &mut tcm.bob().await;
|
||||
|
||||
let chat = alice.create_chat(bob).await.id;
|
||||
|
||||
let mut instance = Message::new(Viewtype::File);
|
||||
instance
|
||||
.set_file_from_bytes(
|
||||
alice,
|
||||
"minimal.xdc",
|
||||
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
connect_alice_bob(alice, chat, &mut instance, bob).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_webxdc_resend() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &mut tcm.alice().await;
|
||||
let bob = &mut tcm.bob().await;
|
||||
let group = chat::create_group_chat(alice, ProtectionStatus::Unprotected, "group chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Alice sends webxdc to bob
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
let mut instance = Message::new(Viewtype::File);
|
||||
instance
|
||||
.set_file_from_bytes(
|
||||
@@ -962,78 +935,7 @@ mod tests {
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
add_contact_to_chat(alice, group, alice.add_or_lookup_contact_id(bob).await)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
connect_alice_bob(alice, group, &mut instance, bob).await;
|
||||
|
||||
// fiona joins late
|
||||
let fiona = &mut tcm.fiona().await;
|
||||
|
||||
add_contact_to_chat(alice, group, alice.add_or_lookup_contact_id(fiona).await)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
resend_msgs(alice, &[instance.id]).await.unwrap();
|
||||
let msg = alice.pop_sent_msg().await;
|
||||
let fiona_instance = fiona.recv_msg(&msg).await;
|
||||
fiona_instance.chat_id.accept(fiona).await.unwrap();
|
||||
assert!(fiona.ctx.iroh.read().await.is_none());
|
||||
|
||||
let fiona_connect_future = send_webxdc_realtime_advertisement(fiona, fiona_instance.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let fiona_advert = fiona.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&fiona_advert).await;
|
||||
|
||||
fiona_connect_future.await.unwrap();
|
||||
|
||||
let realtime_send_loop = async {
|
||||
// Keep sending in a loop because right after joining
|
||||
// Fiona may miss messages.
|
||||
loop {
|
||||
send_webxdc_realtime_data(alice, instance.id, b"alice -> bob & fiona".into())
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
};
|
||||
|
||||
let realtime_receive_loop = async {
|
||||
loop {
|
||||
let event = fiona.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == b"alice -> bob & fiona" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
"Unexpected status update: {}",
|
||||
String::from_utf8_lossy(&data)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
tokio::select!(
|
||||
_ = realtime_send_loop => {
|
||||
panic!("Send loop should never finish");
|
||||
},
|
||||
_ = realtime_receive_loop => {
|
||||
return;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async fn connect_alice_bob(
|
||||
alice: &mut TestContext,
|
||||
alice_chat_id: ChatId,
|
||||
instance: &mut Message,
|
||||
bob: &mut TestContext,
|
||||
) {
|
||||
send_msg(alice, alice_chat_id, instance).await.unwrap();
|
||||
send_msg(alice, alice_chat.id, &mut instance).await.unwrap();
|
||||
let alice_webxdc = alice.get_last_msg().await;
|
||||
|
||||
let webxdc = alice.pop_sent_msg().await;
|
||||
@@ -1050,9 +952,8 @@ mod tests {
|
||||
.unwrap();
|
||||
let alice_advertisement = alice.pop_sent_msg().await;
|
||||
|
||||
let bob_advertisement_future = send_webxdc_realtime_advertisement(bob, bob_webxdc.id)
|
||||
send_webxdc_realtime_advertisement(bob, bob_webxdc.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let bob_advertisement = bob.pop_sent_msg().await;
|
||||
|
||||
@@ -1060,9 +961,8 @@ mod tests {
|
||||
bob.recv_msg_trash(&alice_advertisement).await;
|
||||
alice.recv_msg_trash(&bob_advertisement).await;
|
||||
|
||||
eprintln!("Alice and Bob wait for connection");
|
||||
eprintln!("Alice waits for connection");
|
||||
alice_advertisement_future.await.unwrap();
|
||||
bob_advertisement_future.await.unwrap();
|
||||
|
||||
// Alice sends ephemeral message
|
||||
eprintln!("Sending ephemeral message");
|
||||
|
||||
+78
-5
@@ -12,6 +12,7 @@ use pgp::composed::{
|
||||
SecretKeyParamsBuilder, SignedPublicKey, SignedPublicSubKey, SignedSecretKey,
|
||||
StandaloneSignature, SubkeyParamsBuilder, TheRing,
|
||||
};
|
||||
use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
|
||||
use pgp::crypto::ecc_curve::ECCCurve;
|
||||
use pgp::crypto::hash::HashAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
@@ -235,9 +236,10 @@ pub fn pk_calc_signature(
|
||||
///
|
||||
/// Receiver private keys are provided in
|
||||
/// `private_keys_for_decryption`.
|
||||
pub fn pk_decrypt(
|
||||
pub fn decrypt(
|
||||
ctext: Vec<u8>,
|
||||
private_keys_for_decryption: &[SignedSecretKey],
|
||||
symmetric_secrets: &[String],
|
||||
) -> Result<pgp::composed::Message<'static>> {
|
||||
let cursor = Cursor::new(ctext);
|
||||
let (msg, _headers) = Message::from_armor(cursor)?;
|
||||
@@ -245,10 +247,17 @@ pub fn pk_decrypt(
|
||||
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.iter().collect();
|
||||
let empty_pw = Password::empty();
|
||||
|
||||
// TODO it may degrade performance that we always try out all passwords here
|
||||
let message_password: Vec<Password> = symmetric_secrets
|
||||
.iter()
|
||||
.map(|p| Password::from(p.as_str()))
|
||||
.collect();
|
||||
let message_password: Vec<&Password> = message_password.iter().collect();
|
||||
|
||||
let ring = TheRing {
|
||||
secret_keys: skeys,
|
||||
key_passwords: vec![&empty_pw],
|
||||
message_password: vec![],
|
||||
message_password,
|
||||
session_keys: vec![],
|
||||
allow_legacy: false,
|
||||
};
|
||||
@@ -311,7 +320,7 @@ pub async fn symm_encrypt(passphrase: &str, plain: Vec<u8>) -> Result<String> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut rng = thread_rng();
|
||||
let s2k = StringToKey::new_default(&mut rng);
|
||||
let builder = MessageBuilder::from_bytes("", plain);
|
||||
let builder: MessageBuilder<'_> = MessageBuilder::from_bytes("", plain);
|
||||
let mut builder = builder.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM);
|
||||
builder.encrypt_with_password(s2k, &passphrase)?;
|
||||
|
||||
@@ -322,6 +331,39 @@ pub async fn symm_encrypt(passphrase: &str, plain: Vec<u8>) -> Result<String> {
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Symmetric encryption.
|
||||
pub async fn encrypt_for_broadcast(
|
||||
plain: Vec<u8>,
|
||||
passphrase: &str,
|
||||
private_key_for_signing: SignedSecretKey,
|
||||
compress: bool,
|
||||
) -> Result<String> {
|
||||
let passphrase = Password::from(passphrase.to_string());
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut rng = thread_rng();
|
||||
let s2k = StringToKey::new_default(&mut rng);
|
||||
let msg = MessageBuilder::from_bytes("", plain);
|
||||
let mut msg = msg.seipd_v2(
|
||||
&mut rng,
|
||||
SymmetricKeyAlgorithm::AES128,
|
||||
AeadAlgorithm::Ocb,
|
||||
ChunkSize::C8KiB,
|
||||
);
|
||||
msg.encrypt_with_password(&mut rng, s2k, &passphrase)?;
|
||||
|
||||
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
|
||||
if compress {
|
||||
msg.compression(CompressionAlgorithm::ZLIB);
|
||||
}
|
||||
|
||||
let encoded_msg = msg.to_armored_string(&mut rng, Default::default())?;
|
||||
|
||||
Ok(encoded_msg)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Symmetric decryption.
|
||||
pub async fn symm_decrypt<T: BufRead + std::fmt::Debug + 'static + Send>(
|
||||
passphrase: &str,
|
||||
@@ -345,7 +387,10 @@ mod tests {
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::{alice_keypair, bob_keypair};
|
||||
use crate::{
|
||||
key::load_self_secret_key,
|
||||
test_utils::{TestContextManager, alice_keypair, bob_keypair},
|
||||
};
|
||||
|
||||
fn pk_decrypt_and_validate<'a>(
|
||||
ctext: &'a [u8],
|
||||
@@ -356,7 +401,7 @@ mod tests {
|
||||
HashSet<Fingerprint>,
|
||||
Vec<u8>,
|
||||
)> {
|
||||
let mut msg = pk_decrypt(ctext.to_vec(), private_keys_for_decryption)?;
|
||||
let mut msg = decrypt(ctext.to_vec(), private_keys_for_decryption, &[])?;
|
||||
let content = msg.as_data_vec()?;
|
||||
let ret_signature_fingerprints =
|
||||
valid_signature_fingerprints(&msg, public_keys_for_validation)?;
|
||||
@@ -542,4 +587,32 @@ mod tests {
|
||||
assert_eq!(content, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_encrypt_decrypt_broadcast() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let plain = Vec::from(b"this is the secret message");
|
||||
let shared_secret = "shared secret";
|
||||
let ctext = encrypt_for_broadcast(
|
||||
plain.clone(),
|
||||
shared_secret,
|
||||
load_self_secret_key(alice).await?,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let bob_private_keyring = crate::key::load_self_secret_keyring(bob).await?;
|
||||
let mut decrypted = decrypt(
|
||||
ctext.into(),
|
||||
&bob_private_keyring,
|
||||
&[shared_secret.to_string()],
|
||||
)?;
|
||||
|
||||
assert_eq!(decrypted.as_data_vec()?, plain);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+6
-25
@@ -407,7 +407,6 @@ mod tests {
|
||||
use crate::message::{MessageState, delete_msgs};
|
||||
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
|
||||
use crate::sql::housekeeping;
|
||||
use crate::test_utils::E2EE_INFO_MSGS;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools::SystemTime;
|
||||
@@ -654,25 +653,13 @@ Here's my footer -- bob@example.net"
|
||||
let chat_alice = alice.create_chat(&bob).await;
|
||||
let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
|
||||
let bob_msg = bob.recv_msg(&alice_msg).await;
|
||||
assert_eq!(
|
||||
get_chat_msgs(&alice, chat_alice.id).await?.len(),
|
||||
E2EE_INFO_MSGS + 1
|
||||
);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 1
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 1);
|
||||
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 1);
|
||||
|
||||
let alice_msg2 = alice.send_text(chat_alice.id, "Hi again!").await;
|
||||
bob.recv_msg(&alice_msg2).await;
|
||||
assert_eq!(
|
||||
get_chat_msgs(&alice, chat_alice.id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2);
|
||||
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
|
||||
|
||||
bob_msg.chat_id.accept(&bob).await?;
|
||||
|
||||
@@ -680,18 +667,12 @@ Here's my footer -- bob@example.net"
|
||||
send_reaction(&bob, bob_msg.id, "👍").await.unwrap();
|
||||
expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
|
||||
expect_no_unwanted_events(&bob).await;
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
|
||||
|
||||
let bob_reaction_msg = bob.pop_sent_msg().await;
|
||||
let alice_reaction_msg = alice.recv_msg_hidden(&bob_reaction_msg).await;
|
||||
assert_eq!(alice_reaction_msg.state, MessageState::InFresh);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&alice, chat_alice.id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2);
|
||||
|
||||
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
|
||||
assert_eq!(reactions.to_string(), "👍1");
|
||||
|
||||
+99
-104
@@ -18,7 +18,7 @@ use crate::chat::{
|
||||
self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, remove_from_chat_contacts_table,
|
||||
};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
|
||||
use crate::constants::{Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
|
||||
use crate::contact::{Contact, ContactId, Origin, mark_contact_id_as_verified};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc_inner;
|
||||
@@ -31,10 +31,8 @@ use crate::key::self_fingerprint_opt;
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
|
||||
use crate::log::LogExt;
|
||||
use crate::log::{info, warn};
|
||||
use crate::logged_debug_assert;
|
||||
use crate::message::{
|
||||
self, Message, MessageState, MessengerMessage, MsgId, Viewtype, get_webxdc_info_messages,
|
||||
rfc724_mid_exists,
|
||||
self, Message, MessageState, MessengerMessage, MsgId, Viewtype, rfc724_mid_exists,
|
||||
};
|
||||
use crate::mimeparser::{AvatarAction, MimeMessage, SystemMessage, parse_message_ids};
|
||||
use crate::param::{Param, Params};
|
||||
@@ -298,16 +296,18 @@ async fn get_to_and_past_contact_ids(
|
||||
past_member_fingerprints = &[];
|
||||
}
|
||||
|
||||
let pgp_to_ids = add_or_lookup_key_contacts_by_address_list(
|
||||
context,
|
||||
&mime_parser.recipients,
|
||||
&mime_parser.gossiped_keys,
|
||||
to_member_fingerprints,
|
||||
Origin::Hidden,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match chat_assignment {
|
||||
ChatAssignment::GroupChat { .. } => {
|
||||
to_ids = add_or_lookup_key_contacts(
|
||||
context,
|
||||
&mime_parser.recipients,
|
||||
&mime_parser.gossiped_keys,
|
||||
to_member_fingerprints,
|
||||
Origin::Hidden,
|
||||
)
|
||||
.await?;
|
||||
to_ids = pgp_to_ids;
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
past_ids = lookup_key_contacts_by_address_list(
|
||||
@@ -318,7 +318,7 @@ async fn get_to_and_past_contact_ids(
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
past_ids = add_or_lookup_key_contacts(
|
||||
past_ids = add_or_lookup_key_contacts_by_address_list(
|
||||
context,
|
||||
&mime_parser.past_members,
|
||||
&mime_parser.gossiped_keys,
|
||||
@@ -335,14 +335,7 @@ async fn get_to_and_past_contact_ids(
|
||||
ChatAssignment::ExistingChat { chat_id, .. } => {
|
||||
let chat = Chat::load_from_db(context, *chat_id).await?;
|
||||
if chat.is_encrypted(context).await? {
|
||||
to_ids = add_or_lookup_key_contacts(
|
||||
context,
|
||||
&mime_parser.recipients,
|
||||
&mime_parser.gossiped_keys,
|
||||
to_member_fingerprints,
|
||||
Origin::Hidden,
|
||||
)
|
||||
.await?;
|
||||
to_ids = pgp_to_ids;
|
||||
past_ids = lookup_key_contacts_by_address_list(
|
||||
context,
|
||||
&mime_parser.past_members,
|
||||
@@ -394,14 +387,6 @@ async fn get_to_and_past_contact_ids(
|
||||
.await?;
|
||||
}
|
||||
ChatAssignment::OneOneChat => {
|
||||
let pgp_to_ids = add_or_lookup_key_contacts(
|
||||
context,
|
||||
&mime_parser.recipients,
|
||||
&mime_parser.gossiped_keys,
|
||||
to_member_fingerprints,
|
||||
Origin::Hidden,
|
||||
)
|
||||
.await?;
|
||||
if pgp_to_ids
|
||||
.first()
|
||||
.is_some_and(|contact_id| contact_id.is_some())
|
||||
@@ -443,7 +428,7 @@ async fn get_to_and_past_contact_ids(
|
||||
context,
|
||||
&mime_parser.recipients,
|
||||
to_member_fingerprints,
|
||||
None,
|
||||
chat_id,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
@@ -1471,20 +1456,20 @@ async fn do_chat_assignment(
|
||||
false => None,
|
||||
};
|
||||
if let Some(chat) = chat {
|
||||
ensure_and_debug_assert!(
|
||||
chat.typ == Chattype::Single,
|
||||
"Chat {chat_id} is not Single",
|
||||
);
|
||||
let new_protection = match verified_encryption {
|
||||
ensure_and_debug_assert!(chat.typ == Chattype::Single);
|
||||
let mut new_protection = match verified_encryption {
|
||||
VerifiedEncryption::Verified => ProtectionStatus::Protected,
|
||||
VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
|
||||
};
|
||||
|
||||
ensure_and_debug_assert!(
|
||||
chat.protected == ProtectionStatus::Unprotected
|
||||
|| new_protection == ProtectionStatus::Protected,
|
||||
"Chat {chat_id} can't downgrade to Unprotected",
|
||||
);
|
||||
if chat.protected != ProtectionStatus::Unprotected
|
||||
&& new_protection == ProtectionStatus::Unprotected
|
||||
// `chat.protected` must be maintained regardless of the `Config::VerifiedOneOnOneChats`.
|
||||
// That's why the config is checked here, and not above.
|
||||
&& context.get_config_bool(Config::VerifiedOneOnOneChats).await?
|
||||
{
|
||||
new_protection = ProtectionStatus::ProtectionBroken;
|
||||
}
|
||||
if chat.protected != new_protection {
|
||||
// The message itself will be sorted under the device message since the device
|
||||
// message is `MessageState::InNoticed`, which means that all following
|
||||
@@ -1555,10 +1540,25 @@ async fn do_chat_assignment(
|
||||
if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await?
|
||||
{
|
||||
id
|
||||
} else {
|
||||
} else if let Some(secret) =
|
||||
mime_parser.get_header(HeaderDef::ChatBroadcastSecret)
|
||||
{
|
||||
let name =
|
||||
compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
|
||||
chat::create_broadcast_ex(context, Nosync, listid, name).await?
|
||||
chat::create_broadcast_ex(
|
||||
context,
|
||||
Nosync,
|
||||
listid,
|
||||
name,
|
||||
secret.to_string(),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Unknown shared secret for outgoing broadcast (TRASH)"
|
||||
);
|
||||
DC_CHAT_ID_TRASH
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1849,8 +1849,6 @@ async fn add_parts(
|
||||
{
|
||||
Some(stock_str::msg_location_enabled_by(context, from_id).await)
|
||||
} else if mime_parser.is_system_message == SystemMessage::EphemeralTimerChanged {
|
||||
let better_msg = stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await;
|
||||
|
||||
// Do not delete the system message itself.
|
||||
//
|
||||
// This prevents confusion when timer is changed
|
||||
@@ -1859,7 +1857,7 @@ async fn add_parts(
|
||||
// week is left.
|
||||
ephemeral_timer = EphemeralTimer::Disabled;
|
||||
|
||||
Some(better_msg)
|
||||
Some(stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -2158,11 +2156,11 @@ RETURNING id
|
||||
// afterwards insert additional parts.
|
||||
replace_msg_id = None;
|
||||
|
||||
ensure_and_debug_assert!(!row_id.is_special(), "Rowid {row_id} is special");
|
||||
ensure_and_debug_assert!(!row_id.is_special());
|
||||
created_db_entries.push(row_id);
|
||||
}
|
||||
|
||||
// Maybe set logging xdc and add gossip topics for webxdcs.
|
||||
// check all parts whether they contain a new logging webxdc
|
||||
for (part, msg_id) in mime_parser.parts.iter().zip(&created_db_entries) {
|
||||
// check if any part contains a webxdc topic id
|
||||
if part.typ == Viewtype::Webxdc {
|
||||
@@ -2296,18 +2294,12 @@ async fn handle_edit_delete(
|
||||
let mut modified_chat_ids = HashSet::new();
|
||||
let mut msg_ids = Vec::new();
|
||||
|
||||
let mut deleted_info_msgs = vec![];
|
||||
let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
|
||||
for rfc724_mid in rfc724_mid_vec {
|
||||
if let Some((msg_id, _)) =
|
||||
message::rfc724_mid_exists(context, rfc724_mid).await?
|
||||
{
|
||||
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
|
||||
if msg.viewtype == Viewtype::Webxdc {
|
||||
let info_msgs = get_webxdc_info_messages(context, &msg).await?;
|
||||
deleted_info_msgs.extend(&info_msgs);
|
||||
}
|
||||
|
||||
if msg.from_id == from_id {
|
||||
message::delete_msg_locally(context, &msg).await?;
|
||||
msg_ids.push(msg.id);
|
||||
@@ -2322,15 +2314,6 @@ async fn handle_edit_delete(
|
||||
warn!(context, "Delete message: {rfc724_mid:?} not found.");
|
||||
}
|
||||
}
|
||||
if !deleted_info_msgs.is_empty() {
|
||||
message::delete_msgs(context, &deleted_info_msgs).await?;
|
||||
for msg_id in deleted_info_msgs {
|
||||
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
|
||||
message::delete_msg_locally(context, &msg).await?;
|
||||
msg_ids.push(msg.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
|
||||
} else {
|
||||
warn!(context, "Delete message: Not encrypted.");
|
||||
@@ -2437,9 +2420,7 @@ async fn lookup_chat_by_reply(
|
||||
// as we can directly assign the message to the chat
|
||||
// by its group ID.
|
||||
ensure_and_debug_assert!(
|
||||
mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted(),
|
||||
"Encrypted message has group ID {}",
|
||||
mime_parser.get_chat_group_id().unwrap_or_default(),
|
||||
mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted()
|
||||
);
|
||||
|
||||
// Try to assign message to the same chat as the parent message.
|
||||
@@ -3484,21 +3465,46 @@ async fn apply_out_broadcast_changes(
|
||||
chat: &mut Chat,
|
||||
from_id: ContactId,
|
||||
) -> Result<GroupChangesInfo> {
|
||||
// TODO code duplication with apply_in_broadcast_changes()
|
||||
ensure!(chat.typ == Chattype::OutBroadcast);
|
||||
|
||||
if let Some(_removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
|
||||
// The sender of the message left the broadcast channel
|
||||
remove_from_chat_contacts_table(context, chat.id, from_id).await?;
|
||||
let mut send_event_chat_modified = false;
|
||||
let mut better_msg = None;
|
||||
|
||||
return Ok(GroupChangesInfo {
|
||||
better_msg: Some("".to_string()),
|
||||
added_removed_id: None,
|
||||
silent: true,
|
||||
extra_msgs: vec![],
|
||||
});
|
||||
apply_chat_name_and_avatar_changes(
|
||||
context,
|
||||
mime_parser,
|
||||
from_id,
|
||||
chat,
|
||||
&mut send_event_chat_modified,
|
||||
&mut better_msg,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(_removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
|
||||
if from_id != ContactId::SELF {
|
||||
// The sender of the message left the broadcast channel
|
||||
remove_from_chat_contacts_table(context, chat.id, from_id).await?;
|
||||
|
||||
return Ok(GroupChangesInfo {
|
||||
better_msg: Some("".to_string()),
|
||||
added_removed_id: None,
|
||||
silent: true,
|
||||
extra_msgs: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(GroupChangesInfo::default())
|
||||
if send_event_chat_modified {
|
||||
context.emit_event(EventType::ChatModified(chat.id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat.id);
|
||||
}
|
||||
Ok(GroupChangesInfo {
|
||||
better_msg,
|
||||
added_removed_id: None,
|
||||
silent: false,
|
||||
extra_msgs: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
async fn apply_in_broadcast_changes(
|
||||
@@ -3531,6 +3537,17 @@ async fn apply_in_broadcast_changes(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(secret) = mime_parser.get_header(HeaderDef::ChatBroadcastSecret) {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO broadcasts_shared_secrets (chat_id, secret) VALUES (?, ?)
|
||||
ON CONFLICT(chat_id) DO UPDATE SET secret=excluded.chat_id",
|
||||
(chat.id, secret),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if send_event_chat_modified {
|
||||
context.emit_event(EventType::ChatModified(chat.id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat.id);
|
||||
@@ -3670,7 +3687,7 @@ async fn mark_recipients_as_verified(
|
||||
return Ok(());
|
||||
}
|
||||
for to_id in to_ids.iter().filter_map(|&x| x) {
|
||||
if to_id == ContactId::SELF || to_id == from_id {
|
||||
if to_id == ContactId::SELF {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3758,7 +3775,7 @@ async fn add_or_lookup_contacts_by_address_list(
|
||||
}
|
||||
|
||||
/// Looks up contact IDs from the database given the list of recipients.
|
||||
async fn add_or_lookup_key_contacts(
|
||||
async fn add_or_lookup_key_contacts_by_address_list(
|
||||
context: &Context,
|
||||
address_list: &[SingleInfo],
|
||||
gossiped_keys: &HashMap<String, SignedPublicKey>,
|
||||
@@ -3778,9 +3795,6 @@ async fn add_or_lookup_key_contacts(
|
||||
fp.hex()
|
||||
} else if let Some(key) = gossiped_keys.get(addr) {
|
||||
key.dc_fingerprint().hex()
|
||||
} else if context.is_self_addr(addr).await? {
|
||||
contact_ids.push(Some(ContactId::SELF));
|
||||
continue;
|
||||
} else {
|
||||
contact_ids.push(None);
|
||||
continue;
|
||||
@@ -3809,7 +3823,7 @@ async fn add_or_lookup_key_contacts(
|
||||
/// Looks up a key-contact by email address.
|
||||
///
|
||||
/// If provided, `chat_id` must be an encrypted chat ID that has key-contacts inside.
|
||||
/// Otherwise the function searches in all contacts, preferring accepted and most recently seen ones.
|
||||
/// Otherwise the function searches in all contacts, returning the recently seen one.
|
||||
async fn lookup_key_contact_by_address(
|
||||
context: &Context,
|
||||
addr: &str,
|
||||
@@ -3852,26 +3866,11 @@ async fn lookup_key_contact_by_address(
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT id FROM contacts
|
||||
WHERE addr=?
|
||||
WHERE contacts.addr=?1
|
||||
AND fingerprint<>''
|
||||
ORDER BY
|
||||
(
|
||||
SELECT COUNT(*) FROM chats c
|
||||
INNER JOIN chats_contacts cc
|
||||
ON c.id=cc.chat_id
|
||||
WHERE c.type=?
|
||||
AND c.id>?
|
||||
AND c.blocked=?
|
||||
AND cc.contact_id=contacts.id
|
||||
) DESC,
|
||||
last_seen DESC, id DESC
|
||||
ORDER BY last_seen DESC, id DESC
|
||||
",
|
||||
(
|
||||
addr,
|
||||
Chattype::Single,
|
||||
constants::DC_CHAT_ID_LAST_SPECIAL,
|
||||
Blocked::Not,
|
||||
),
|
||||
(addr,),
|
||||
|row| {
|
||||
let contact_id: ContactId = row.get(0)?;
|
||||
Ok(contact_id)
|
||||
@@ -3887,11 +3886,7 @@ async fn lookup_key_contact_by_fingerprint(
|
||||
context: &Context,
|
||||
fingerprint: &str,
|
||||
) -> Result<Option<ContactId>> {
|
||||
logged_debug_assert!(
|
||||
context,
|
||||
!fingerprint.is_empty(),
|
||||
"lookup_key_contact_by_fingerprint: fingerprint is empty."
|
||||
);
|
||||
debug_assert!(!fingerprint.is_empty());
|
||||
if fingerprint.is_empty() {
|
||||
// Avoid accidentally looking up a non-key-contact.
|
||||
return Ok(None);
|
||||
|
||||
@@ -15,9 +15,8 @@ use crate::download::MIN_DOWNLOAD_LIMIT;
|
||||
use crate::imap::prefetch_should_download;
|
||||
use crate::imex::{ImexMode, imex};
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{
|
||||
E2EE_INFO_MSGS, TestContext, TestContextManager, get_chat_msg, mark_as_verified,
|
||||
};
|
||||
use crate::test_utils::mark_as_verified;
|
||||
use crate::test_utils::{TestContext, TestContextManager, get_chat_msg};
|
||||
use crate::tools::{SystemTime, time};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -134,7 +133,7 @@ async fn test_adhoc_group_outgoing_show_accepted_contact_unaccepted() -> Result<
|
||||
let chats = Chatlist::try_load(bob, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chat_id = chats.get_chat_id(0)?;
|
||||
assert_eq!(chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1);
|
||||
assert_eq!(chat_id.get_msg_cnt(bob).await?, 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -991,7 +990,6 @@ async fn test_other_device_writes_to_mailinglist() -> Result<()> {
|
||||
.await?
|
||||
.unwrap();
|
||||
let list_post_contact = Contact::get_by_id(&t, list_post_contact_id).await?;
|
||||
assert_eq!(list_post_contact.is_key_contact(), false);
|
||||
assert_eq!(
|
||||
list_post_contact.param.get(Param::ListId).unwrap(),
|
||||
"delta.codespeak.net"
|
||||
@@ -1454,7 +1452,6 @@ async fn test_apply_mailinglist_changes_assigned_by_reply() {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.is_key_contact(), false);
|
||||
assert_eq!(
|
||||
contact.param.get(Param::ListId).unwrap(),
|
||||
"deltachat-core-rust.deltachat.github.com"
|
||||
@@ -3052,39 +3049,6 @@ async fn test_auto_accept_protected_group_for_bots() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for a bug where receive_imf() failed
|
||||
/// if the sender of a verification-gossiping message
|
||||
/// also put itself into the To header.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_verification_gossip() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
mark_as_verified(alice, bob).await;
|
||||
mark_as_verified(bob, alice).await;
|
||||
|
||||
// This is message sent by Alice with verified encryption
|
||||
// that gossips Fiona's verification,
|
||||
// and for some reason, Alice also put herself into the To: header.
|
||||
let imf_raw =
|
||||
include_bytes!("../../test-data/message/verification-gossip-also-sent-to-from.eml");
|
||||
|
||||
// The regression test is that receive_imf() doesn't panic:
|
||||
let msg = receive_imf(bob, imf_raw, false).await?.unwrap();
|
||||
let msg = Message::load_from_db(bob, msg.msg_ids[0]).await?;
|
||||
assert_eq!(msg.text, "Hello!");
|
||||
assert!(
|
||||
bob.add_or_lookup_contact(fiona)
|
||||
.await
|
||||
.is_verified(bob)
|
||||
.await?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_bot_accepts_another_group_after_qr_scan() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -3684,13 +3648,10 @@ async fn test_mua_user_adds_recipient_to_single_chat() -> Result<()> {
|
||||
chat::get_chat_contacts(&alice, group_chat.id).await?.len(),
|
||||
4
|
||||
);
|
||||
let fiona_contact_id =
|
||||
Contact::lookup_id_by_addr(&alice, "fiona@example.net", Origin::IncomingTo)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert!(chat::is_contact_in_chat(&alice, group_chat.id, fiona_contact_id).await?);
|
||||
let fiona_contact = Contact::get_by_id(&alice, fiona_contact_id).await?;
|
||||
assert_eq!(fiona_contact.is_key_contact(), false);
|
||||
let fiona = Contact::lookup_id_by_addr(&alice, "fiona@example.net", Origin::IncomingTo)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert!(chat::is_contact_in_chat(&alice, group_chat.id, fiona).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3721,24 +3682,6 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ignore_protected_headers_in_outer_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
|
||||
send_text_msg(bob, bob_chat_id, "hi all!".to_string()).await?;
|
||||
let mut sent_msg = bob.pop_sent_msg().await;
|
||||
sent_msg.payload = sent_msg.payload.replace(
|
||||
"Chat-Version:",
|
||||
"Auto-Submitted: auto-generated\r\nChat-Version:",
|
||||
);
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
let ab_contact = alice.add_or_lookup_contact(bob).await;
|
||||
assert!(!ab_contact.is_bot());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_member_list_on_rejoin() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -4449,7 +4392,7 @@ async fn test_create_group_with_big_msg() -> Result<()> {
|
||||
|
||||
// The big message must go away from the 1:1 chat.
|
||||
let msgs = chat::get_chat_msgs(&alice, ab_chat_id).await?;
|
||||
assert_eq!(msgs.len(), E2EE_INFO_MSGS);
|
||||
assert!(msgs.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -5307,48 +5250,3 @@ async fn test_outgoing_unencrypted_chat_assignment() {
|
||||
let chat = alice.create_email_chat(bob).await;
|
||||
assert_eq!(received.chat_id, chat.id);
|
||||
}
|
||||
|
||||
/// Tests Bob receiving a message from Alice
|
||||
/// in a new group she just created
|
||||
/// with only Alice and Bob.
|
||||
///
|
||||
/// The message has no Autocrypt-Gossip
|
||||
/// headers and no Chat-Group-Member-Fpr header.
|
||||
/// Such messages were created by core 1.159.5
|
||||
/// when Alice has bcc_self disabled
|
||||
/// as Chat-Group-Member-Fpr header did not exist
|
||||
/// yet and Autocrypt-Gossip is not sent
|
||||
/// as there is only one recipient
|
||||
/// (Bob, and no additional Alice devices).
|
||||
///
|
||||
/// Bob should recognize self as being
|
||||
/// a member of the group by just the e-mail address.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_group_introduction_no_gossip() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let received = receive_imf(
|
||||
bob,
|
||||
include_bytes!("../../test-data/message/group-introduction-no-gossip.eml"),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let msg = Message::load_from_db(bob, *received.msg_ids.last().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.text, "I created a group");
|
||||
let chat = Chat::load_from_db(bob, msg.chat_id).await.unwrap();
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
assert_eq!(chat.blocked, Blocked::Request);
|
||||
assert_eq!(chat.name, "Group!");
|
||||
assert!(chat.is_encrypted(bob).await.unwrap());
|
||||
|
||||
let contacts = get_chat_contacts(bob, chat.id).await?;
|
||||
assert_eq!(contacts.len(), 2);
|
||||
assert!(chat.is_self_in_chat(bob).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+2
-4
@@ -16,7 +16,6 @@ use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::key::{DcKey, Fingerprint, load_self_public_key};
|
||||
use crate::log::{error, info, warn};
|
||||
use crate::logged_debug_assert;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::param::Param;
|
||||
@@ -33,10 +32,9 @@ use qrinvite::QrInvite;
|
||||
use crate::token::Namespace;
|
||||
|
||||
fn inviter_progress(context: &Context, contact_id: ContactId, progress: usize) {
|
||||
logged_debug_assert!(
|
||||
context,
|
||||
debug_assert!(
|
||||
progress <= 1000,
|
||||
"inviter_progress: contact {contact_id}, progress={progress}, but value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success."
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
context.emit_event(EventType::SecurejoinInviterProgress {
|
||||
contact_id,
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::{self, messages_e2e_encrypted};
|
||||
use crate::stock_str::{self, chat_protection_enabled};
|
||||
use crate::test_utils::{
|
||||
TestContext, TestContextManager, TimeShiftFalsePositiveNote, get_chat_msg,
|
||||
};
|
||||
@@ -246,7 +246,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
let chat = alice.get_chat(&bob).await;
|
||||
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
|
||||
assert!(msg.is_info());
|
||||
let expected_text = messages_e2e_encrypted(&alice).await;
|
||||
let expected_text = chat_protection_enabled(&alice).await;
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
if case == SetupContactCase::CheckProtectionTimestamp {
|
||||
assert_eq!(msg.timestamp_sort, vc_request_with_auth_ts_sent + 1);
|
||||
@@ -296,7 +296,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), messages_e2e_encrypted(&bob).await);
|
||||
assert_eq!(msg.get_text(), chat_protection_enabled(&bob).await);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -540,7 +540,7 @@ async fn test_secure_join() -> Result<()> {
|
||||
// - You added member bob@example.net
|
||||
let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await;
|
||||
assert!(msg.is_info());
|
||||
let expected_text = messages_e2e_encrypted(&alice).await;
|
||||
let expected_text = chat_protection_enabled(&alice).await;
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
}
|
||||
|
||||
|
||||
+1
-5
@@ -414,11 +414,7 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
.await?;
|
||||
}
|
||||
SendResult::Failure(ref err) => {
|
||||
if err
|
||||
.to_string()
|
||||
.to_lowercase()
|
||||
.contains("invalid unencrypted mail")
|
||||
{
|
||||
if err.to_string().contains("Invalid unencrypted mail") {
|
||||
let res = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
|
||||
+1
-36
@@ -13,7 +13,6 @@ use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::set_debug_logging_xdc;
|
||||
use crate::ensure_and_debug_assert;
|
||||
use crate::ephemeral::start_ephemeral_timers;
|
||||
use crate::imex::BLOBS_BACKUP_NAME;
|
||||
use crate::location::delete_orphaned_poi_locations;
|
||||
@@ -176,12 +175,10 @@ impl Sql {
|
||||
.await
|
||||
}
|
||||
|
||||
const N_DB_CONNECTIONS: usize = 3;
|
||||
|
||||
/// Creates a new connection pool.
|
||||
fn new_pool(dbfile: &Path, passphrase: String) -> Result<Pool> {
|
||||
let mut connections = Vec::new();
|
||||
for _ in 0..Self::N_DB_CONNECTIONS {
|
||||
for _ in 0..3 {
|
||||
let connection = new_connection(dbfile, &passphrase)?;
|
||||
connections.push(connection);
|
||||
}
|
||||
@@ -640,31 +637,6 @@ impl Sql {
|
||||
pub fn config_cache(&self) -> &RwLock<HashMap<String, Option<String>>> {
|
||||
&self.config_cache
|
||||
}
|
||||
|
||||
/// Runs a checkpoint operation in TRUNCATE mode, so the WAL file is truncated to 0 bytes.
|
||||
pub(crate) async fn wal_checkpoint(&self) -> Result<()> {
|
||||
let lock = self.pool.read().await;
|
||||
let pool = lock.as_ref().context("No SQL connection pool")?;
|
||||
let mut conns = Vec::new();
|
||||
let query_only = true;
|
||||
// Kick out readers to avoid blocking/SQLITE_BUSY.
|
||||
for _ in 0..(Self::N_DB_CONNECTIONS - 1) {
|
||||
conns.push(pool.get(query_only).await?);
|
||||
}
|
||||
let conn = pool.get(query_only).await?;
|
||||
tokio::task::block_in_place(move || {
|
||||
// Execute some transaction causing the WAL file to be opened so that the
|
||||
// `wal_checkpoint()` can proceed, otherwise it fails when called the first time, see
|
||||
// https://sqlite.org/forum/forumpost/7512d76a05268fc8.
|
||||
conn.query_row("PRAGMA table_list", [], |_row| Ok(()))?;
|
||||
let blocked = conn.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |row| {
|
||||
let blocked: i64 = row.get(0)?;
|
||||
Ok(blocked)
|
||||
})?;
|
||||
ensure_and_debug_assert!(blocked == 0,);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new SQLite connection.
|
||||
@@ -788,13 +760,6 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
if let Err(err) = incremental_vacuum(context).await {
|
||||
warn!(context, "Failed to run incremental vacuum: {err:#}.");
|
||||
}
|
||||
// Work around possible checkpoint starvations (there were cases reported when a WAL file is
|
||||
// bigger than 200M) and also make sure we truncate the WAL periodically. Auto-checkponting does
|
||||
// not normally truncate the WAL (unless the `journal_size_limit` pragma is set), see
|
||||
// https://www.sqlite.org/wal.html.
|
||||
if let Err(err) = context.sql.wal_checkpoint().await {
|
||||
warn!(context, "wal_checkpoint() failed: {err:#}.");
|
||||
}
|
||||
|
||||
context
|
||||
.sql
|
||||
|
||||
+25
-37
@@ -1253,9 +1253,11 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
|
||||
|
||||
inc_and_check(&mut migration_version, 133)?;
|
||||
if dbversion < migration_version {
|
||||
// Make `ProtectionBroken` chats `Unprotected`. Chats can't break anymore.
|
||||
sql.execute_migration(
|
||||
"UPDATE chats SET protected=0 WHERE protected!=1",
|
||||
"CREATE TABLE broadcasts_shared_secrets(
|
||||
chat_id INTEGER PRIMARY KEY NOT NULL, -- TODO we don't actually need the chat_id
|
||||
secret TEXT NOT NULL
|
||||
) STRICT",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
@@ -1490,11 +1492,9 @@ fn migrate_key_contacts(
|
||||
} else if addr.is_empty() {
|
||||
Ok(default)
|
||||
} else {
|
||||
Ok(original_contact_id_from_addr_stmt
|
||||
original_contact_id_from_addr_stmt
|
||||
.query_row((addr,), |row| row.get(0))
|
||||
.optional()
|
||||
.with_context(|| format!("Original contact '{addr}' not found"))?
|
||||
.unwrap_or(default))
|
||||
.with_context(|| format!("Original contact '{addr}' not found"))
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1517,28 +1517,19 @@ fn migrate_key_contacts(
|
||||
};
|
||||
let new_id = insert_contact(verified_key).context("Step 13")?;
|
||||
verified_key_contacts.insert(original_id.try_into().context("Step 14")?, new_id);
|
||||
|
||||
let verifier_id = if addr_cmp(&verifier, &addr) {
|
||||
// Earlier versions of Delta Chat signalled a direct verification
|
||||
// by putting the contact's own address into the verifier column
|
||||
1 // 1=ContactId::SELF
|
||||
} else {
|
||||
// If the original verifier is unknown, we represent this in the database
|
||||
// by putting `new_id` into the place of the verifier,
|
||||
// i.e. we say that this contact verified itself.
|
||||
original_contact_id_from_addr(&verifier, new_id).context("Step 15")?
|
||||
};
|
||||
// If the original verifier is unknown, we represent this in the database
|
||||
// by putting `new_id` into the place of the verifier,
|
||||
// i.e. we say that this contact verified itself.
|
||||
let verifier_id =
|
||||
original_contact_id_from_addr(&verifier, new_id).context("Step 15")?;
|
||||
verifications.insert(new_id, verifier_id);
|
||||
|
||||
let Some(secondary_verified_key) = secondary_verified_key else {
|
||||
continue;
|
||||
};
|
||||
let new_id = insert_contact(secondary_verified_key).context("Step 16")?;
|
||||
let verifier_id: u32 = if addr_cmp(&secondary_verifier, &addr) {
|
||||
1 // 1=ContactId::SELF
|
||||
} else {
|
||||
original_contact_id_from_addr(&secondary_verifier, new_id).context("Step 17")?
|
||||
};
|
||||
let verifier_id: u32 =
|
||||
original_contact_id_from_addr(&secondary_verifier, new_id).context("Step 17")?;
|
||||
// Only use secondary verification if there is no primary verification:
|
||||
verifications.entry(new_id).or_insert(verifier_id);
|
||||
}
|
||||
@@ -1663,7 +1654,7 @@ fn migrate_key_contacts(
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.context("Step 26")?;
|
||||
|
||||
let mut keep_address_contacts = |reason: &str| -> Result<()> {
|
||||
let mut keep_address_contacts = |reason: &str| {
|
||||
info!(
|
||||
context,
|
||||
"Chat {chat_id} will be an unencrypted chat with contacts identified by email address: {reason}."
|
||||
@@ -1671,15 +1662,6 @@ fn migrate_key_contacts(
|
||||
for (m, _) in &old_members {
|
||||
orphaned_contacts.remove(m);
|
||||
}
|
||||
|
||||
// Unprotect this chat if it was protected.
|
||||
//
|
||||
// Otherwise we get protected chat with address-contact(s).
|
||||
transaction
|
||||
.execute("UPDATE chats SET protected=0 WHERE id=?", (chat_id,))
|
||||
.context("Step 26.0")?;
|
||||
|
||||
Ok(())
|
||||
};
|
||||
let old_and_new_members: Vec<(u32, bool, Option<u32>)> = match typ {
|
||||
// 1:1 chats retain:
|
||||
@@ -1699,13 +1681,19 @@ fn migrate_key_contacts(
|
||||
};
|
||||
|
||||
let Some(new_contact) = map_to_key_contact(old_member) else {
|
||||
keep_address_contacts("No peerstate, or peerstate in 'reset' state")?;
|
||||
keep_address_contacts("No peerstate, or peerstate in 'reset' state");
|
||||
continue;
|
||||
};
|
||||
if !addr_cmp_stmt
|
||||
.query_row((old_member, new_contact), |row| row.get::<_, bool>(0))?
|
||||
{
|
||||
keep_address_contacts("key contact has different email")?;
|
||||
// Unprotect this 1:1 chat if it was protected.
|
||||
//
|
||||
// Otherwise we get protected chat with address-contact.
|
||||
transaction
|
||||
.execute("UPDATE chats SET protected=0 WHERE id=?", (chat_id,))?;
|
||||
|
||||
keep_address_contacts("key contact has different email");
|
||||
continue;
|
||||
}
|
||||
vec![(*old_member, true, Some(new_contact))]
|
||||
@@ -1716,7 +1704,7 @@ fn migrate_key_contacts(
|
||||
if grpid.is_empty() {
|
||||
// Ad-hoc group that has empty Chat-Group-ID
|
||||
// because it was created in response to receiving a non-chat email.
|
||||
keep_address_contacts("Empty chat-Group-ID")?;
|
||||
keep_address_contacts("Empty chat-Group-ID");
|
||||
continue;
|
||||
} else if protected == 1 {
|
||||
old_members
|
||||
@@ -1735,7 +1723,7 @@ fn migrate_key_contacts(
|
||||
|
||||
// Mailinglist
|
||||
140 => {
|
||||
keep_address_contacts("Mailinglist")?;
|
||||
keep_address_contacts("Mailinglist");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1774,7 +1762,7 @@ fn migrate_key_contacts(
|
||||
transaction
|
||||
.execute("UPDATE chats SET grpid='' WHERE id=?", (chat_id,))
|
||||
.context("Step 26.1")?;
|
||||
keep_address_contacts("Group contains contact without peerstate")?;
|
||||
keep_address_contacts("Group contains contact without peerstate");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
+8
-38
@@ -65,6 +65,9 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "GIF"))]
|
||||
Gif = 23,
|
||||
|
||||
#[strum(props(fallback = "Encrypted message"))]
|
||||
EncryptedMsg = 24,
|
||||
|
||||
#[strum(props(fallback = "End-to-end encryption available"))]
|
||||
E2eAvailable = 25,
|
||||
|
||||
@@ -362,12 +365,6 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
|
||||
MsgEphemeralTimerWeeksBy = 157,
|
||||
|
||||
#[strum(props(fallback = "You set message deletion timer to 1 year."))]
|
||||
MsgYouEphemeralTimerYear = 158,
|
||||
|
||||
#[strum(props(fallback = "Message deletion timer is set to 1 year by %1$s."))]
|
||||
MsgEphemeralTimerYearBy = 159,
|
||||
|
||||
#[strum(props(fallback = "Scan to set up second device for %1$s"))]
|
||||
BackupTransferQr = 162,
|
||||
|
||||
@@ -383,10 +380,9 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "I left the group."))]
|
||||
MsgILeftGroup = 166,
|
||||
|
||||
#[strum(props(fallback = "Messages are end-to-end encrypted."))]
|
||||
#[strum(props(fallback = "Messages are guaranteed to be end-to-end encrypted from now on."))]
|
||||
ChatProtectionEnabled = 170,
|
||||
|
||||
// deprecated 2025-07
|
||||
#[strum(props(fallback = "%1$s sent a message from another device."))]
|
||||
ChatProtectionDisabled = 171,
|
||||
|
||||
@@ -417,16 +413,6 @@ pub enum StockMessage {
|
||||
|
||||
#[strum(props(fallback = "Establishing guaranteed end-to-end encryption, please wait…"))]
|
||||
SecurejoinWait = 190,
|
||||
|
||||
#[strum(props(fallback = "❤️ Seems you're enjoying Delta Chat!
|
||||
|
||||
Please consider donating to help that Delta Chat stays free for everyone.
|
||||
|
||||
While Delta Chat is free to use and open source, development costs money.
|
||||
Help keeping us to keep Delta Chat independent and make it more awesome in the future.
|
||||
|
||||
https://delta.chat/donate"))]
|
||||
DonationRequest = 193,
|
||||
}
|
||||
|
||||
impl StockMessage {
|
||||
@@ -799,11 +785,6 @@ pub(crate) async fn securejoin_wait(context: &Context) -> String {
|
||||
translated(context, StockMessage::SecurejoinWait).await
|
||||
}
|
||||
|
||||
/// Stock string: `❤️ Seems you're enjoying Delta Chat!`…
|
||||
pub(crate) async fn donation_request(context: &Context) -> String {
|
||||
translated(context, StockMessage::DonationRequest).await
|
||||
}
|
||||
|
||||
/// Stock string: `Scan to chat with %1$s`.
|
||||
pub(crate) async fn setup_contact_qr_description(
|
||||
context: &Context,
|
||||
@@ -998,17 +979,6 @@ pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: Cont
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `Message deletion timer is set to 1 year.`.
|
||||
pub(crate) async fn msg_ephemeral_timer_year(context: &Context, by_contact: ContactId) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouEphemeralTimerYear).await
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerYearBy)
|
||||
.await
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock string: `Video chat invitation`.
|
||||
pub(crate) async fn videochat_invitation(context: &Context) -> String {
|
||||
translated(context, StockMessage::VideochatInvitation).await
|
||||
@@ -1046,8 +1016,8 @@ pub(crate) async fn error_no_network(context: &Context) -> String {
|
||||
translated(context, StockMessage::ErrorNoNetwork).await
|
||||
}
|
||||
|
||||
/// Stock string: `Messages are end-to-end encrypted.`
|
||||
pub(crate) async fn messages_e2e_encrypted(context: &Context) -> String {
|
||||
/// Stock string: `Messages are guaranteed to be end-to-end encrypted from now on.`
|
||||
pub(crate) async fn chat_protection_enabled(context: &Context) -> String {
|
||||
translated(context, StockMessage::ChatProtectionEnabled).await
|
||||
}
|
||||
|
||||
@@ -1309,7 +1279,7 @@ impl Context {
|
||||
contact_id: Option<ContactId>,
|
||||
) -> String {
|
||||
match protect {
|
||||
ProtectionStatus::Unprotected => {
|
||||
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {
|
||||
if let Some(contact_id) = contact_id {
|
||||
chat_protection_disabled(self, contact_id).await
|
||||
} else {
|
||||
@@ -1318,7 +1288,7 @@ impl Context {
|
||||
"[Error] No contact_id given".to_string()
|
||||
}
|
||||
}
|
||||
ProtectionStatus::Protected => messages_e2e_encrypted(self).await,
|
||||
ProtectionStatus::Protected => chat_protection_enabled(self).await,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-15
@@ -11,7 +11,7 @@ use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::log::LogExt;
|
||||
use crate::log::{info, warn};
|
||||
use crate::message::{Message, MsgId, Viewtype, get_webxdc_info_messages};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::sync::SyncData::{AddQrToken, AlterChat, DeleteQrToken};
|
||||
@@ -306,15 +306,10 @@ impl Context {
|
||||
|
||||
async fn sync_message_deletion(&self, msgs: &Vec<String>) -> Result<()> {
|
||||
let mut modified_chat_ids = HashSet::new();
|
||||
let mut deleted_info_msgs = vec![];
|
||||
let mut msg_ids = Vec::new();
|
||||
for rfc724_mid in msgs {
|
||||
if let Some((msg_id, _)) = message::rfc724_mid_exists(self, rfc724_mid).await? {
|
||||
if let Some(msg) = Message::load_from_db_optional(self, msg_id).await? {
|
||||
if msg.viewtype == Viewtype::Webxdc {
|
||||
let info_msgs = get_webxdc_info_messages(&self, &msg).await?;
|
||||
deleted_info_msgs.extend(&info_msgs);
|
||||
}
|
||||
message::delete_msg_locally(self, &msg).await?;
|
||||
msg_ids.push(msg.id);
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
@@ -325,15 +320,6 @@ impl Context {
|
||||
warn!(self, "Sync message delete: {rfc724_mid:?} not found.");
|
||||
}
|
||||
}
|
||||
if !deleted_info_msgs.is_empty() {
|
||||
message::delete_msgs(&self, &deleted_info_msgs).await?;
|
||||
for msg_id in deleted_info_msgs {
|
||||
if let Some(msg) = Message::load_from_db_optional(&self, msg_id).await? {
|
||||
message::delete_msg_locally(&self, &msg).await?;
|
||||
msg_ids.push(msg.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
message::delete_msgs_locally_done(self, &msg_ids, modified_chat_ids).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+3
-10
@@ -43,10 +43,6 @@ use crate::securejoin::{get_securejoin_qr, join_securejoin};
|
||||
use crate::stock_str::StockStrings;
|
||||
use crate::tools::time;
|
||||
|
||||
/// The number of info messages added to new e2ee chats.
|
||||
/// Currently this is "End-to-end encryption available", string `E2eAvailable`.
|
||||
pub const E2EE_INFO_MSGS: usize = 1;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png");
|
||||
|
||||
@@ -758,7 +754,7 @@ impl TestContext {
|
||||
pub async fn add_or_lookup_address_contact(&self, other: &TestContext) -> Contact {
|
||||
let contact_id = self.add_or_lookup_address_contact_id(other).await;
|
||||
let contact = Contact::get_by_id(&self.ctx, contact_id).await.unwrap();
|
||||
assert_eq!(contact.is_key_contact(), false);
|
||||
debug_assert_eq!(contact.is_key_contact(), false);
|
||||
contact
|
||||
}
|
||||
|
||||
@@ -1086,6 +1082,8 @@ impl Drop for TestContext {
|
||||
.join(format!("test-account-{}.db", self.name()));
|
||||
tokio::fs::copy(from, &target).await.unwrap();
|
||||
eprintln!("Copied database from {from:?} to {target:?}\n");
|
||||
} else {
|
||||
eprintln!("Hint: If you want to examine the database files, set environment variable DELTACHAT_SAVE_TMP_DB=1\n")
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1170,11 +1168,6 @@ impl Drop for InnerLogSink {
|
||||
while let Ok(event) = self.events.try_recv() {
|
||||
print_logevent(&event);
|
||||
}
|
||||
if std::env::var("DELTACHAT_SAVE_TMP_DB").is_err() {
|
||||
eprintln!(
|
||||
"note: If you want to examine the database files, set environment variable DELTACHAT_SAVE_TMP_DB=1"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-16
@@ -15,9 +15,7 @@ use crate::mimeparser::SystemMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::{get_securejoin_qr, join_securejoin};
|
||||
use crate::stock_str;
|
||||
use crate::test_utils::{
|
||||
E2EE_INFO_MSGS, TestContext, TestContextManager, get_chat_msg, mark_as_verified,
|
||||
};
|
||||
use crate::test_utils::{TestContext, TestContextManager, get_chat_msg, mark_as_verified};
|
||||
use crate::tools::SystemTime;
|
||||
use crate::{e2ee, message};
|
||||
|
||||
@@ -31,7 +29,7 @@ async fn test_verified_oneonone_chat_not_broken_by_device_change() {
|
||||
check_verified_oneonone_chat_protection_not_broken(false).await;
|
||||
}
|
||||
|
||||
async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email: bool) {
|
||||
async fn check_verified_oneonone_chat_protection_not_broken(broken_by_classical_email: bool) {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
@@ -42,7 +40,7 @@ async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email:
|
||||
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
|
||||
assert_verified(&bob, &alice, ProtectionStatus::Protected).await;
|
||||
|
||||
if by_classical_email {
|
||||
if broken_by_classical_email {
|
||||
tcm.section("Bob uses a classical MUA to send a message to Alice");
|
||||
receive_imf(
|
||||
&alice,
|
||||
@@ -58,6 +56,7 @@ async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email:
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
// Bob's contact is still verified, but the chat isn't marked as protected anymore
|
||||
let contact = alice.add_or_lookup_contact(&bob).await;
|
||||
assert_eq!(contact.is_verified(&alice).await.unwrap(), true);
|
||||
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
|
||||
@@ -133,7 +132,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
|
||||
assert!(chat.is_protected());
|
||||
|
||||
let msg = get_chat_msg(&alice, chat.id, 0, 1).await;
|
||||
let expected_text = stock_str::messages_e2e_encrypted(&alice).await;
|
||||
let expected_text = stock_str::chat_protection_enabled(&alice).await;
|
||||
assert_eq!(msg.text, expected_text);
|
||||
}
|
||||
|
||||
@@ -143,7 +142,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
|
||||
assert!(chat.is_protected());
|
||||
|
||||
let msg0 = get_chat_msg(&fiona, chat.id, 0, 1).await;
|
||||
let expected_text = stock_str::messages_e2e_encrypted(&fiona).await;
|
||||
let expected_text = stock_str::chat_protection_enabled(&fiona).await;
|
||||
assert_eq!(msg0.text, expected_text);
|
||||
}
|
||||
|
||||
@@ -163,7 +162,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
|
||||
let chat = alice.get_chat(&fiona_new).await;
|
||||
assert!(!chat.is_protected());
|
||||
|
||||
let msg = get_chat_msg(&alice, chat.id, 1, E2EE_INFO_MSGS + 1).await;
|
||||
let msg = get_chat_msg(&alice, chat.id, 0, 1).await;
|
||||
assert_eq!(msg.text, "I have a new device");
|
||||
|
||||
// After recreating the chat, it should still be unprotected
|
||||
@@ -198,6 +197,7 @@ async fn test_missing_key_reexecute_securejoin() -> Result<()> {
|
||||
let chat_id = tcm.execute_securejoin(bob, alice).await;
|
||||
let chat = Chat::load_from_db(bob, chat_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
assert!(!chat.is_protection_broken());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -211,6 +211,7 @@ async fn test_create_unverified_oneonone_chat() -> Result<()> {
|
||||
// A chat with an unknown contact should be created unprotected
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(!chat.is_protection_broken());
|
||||
|
||||
receive_imf(
|
||||
&alice,
|
||||
@@ -227,12 +228,14 @@ async fn test_create_unverified_oneonone_chat() -> Result<()> {
|
||||
// Now Bob is a known contact, new chats should still be created unprotected
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(!chat.is_protection_broken());
|
||||
|
||||
tcm.send_recv(&bob, &alice, "hi").await;
|
||||
chat.id.delete(&alice).await.unwrap();
|
||||
// Now we have a public key, new chats should still be created unprotected
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(!chat.is_protection_broken());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -265,7 +268,7 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> {
|
||||
.await?;
|
||||
|
||||
let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 1).await;
|
||||
let enabled = stock_str::messages_e2e_encrypted(&alice).await;
|
||||
let enabled = stock_str::chat_protection_enabled(&alice).await;
|
||||
assert_eq!(msg0.text, enabled);
|
||||
assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatProtectionEnabled);
|
||||
|
||||
@@ -520,6 +523,7 @@ async fn test_message_from_old_dc_setup() -> Result<()> {
|
||||
assert!(contact.is_verified(alice).await.unwrap());
|
||||
let chat = alice.get_chat(bob).await;
|
||||
assert!(chat.is_protected());
|
||||
assert_eq!(chat.is_protection_broken(), false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -806,15 +810,19 @@ async fn test_verified_chat_editor_reordering() -> Result<()> {
|
||||
// ============== Helper Functions ==============
|
||||
|
||||
async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) {
|
||||
let contact = this.add_or_lookup_contact(other).await;
|
||||
assert_eq!(contact.is_verified(this).await.unwrap(), true);
|
||||
if protected != ProtectionStatus::ProtectionBroken {
|
||||
let contact = this.add_or_lookup_contact(other).await;
|
||||
assert_eq!(contact.is_verified(this).await.unwrap(), true);
|
||||
}
|
||||
|
||||
let chat = this.get_chat(other).await;
|
||||
assert_eq!(
|
||||
chat.is_protected(),
|
||||
protected == ProtectionStatus::Protected
|
||||
);
|
||||
assert_eq!(chat.is_protection_broken(), false);
|
||||
let (expect_protected, expect_broken) = match protected {
|
||||
ProtectionStatus::Unprotected => (false, false),
|
||||
ProtectionStatus::Protected => (true, false),
|
||||
ProtectionStatus::ProtectionBroken => (false, true),
|
||||
};
|
||||
assert_eq!(chat.is_protected(), expect_protected);
|
||||
assert_eq!(chat.is_protection_broken(), expect_broken);
|
||||
}
|
||||
|
||||
async fn enable_verified_oneonone_chats(test_contexts: &[&TestContext]) {
|
||||
|
||||
+23
-17
@@ -300,6 +300,26 @@ pub(crate) fn create_id() -> String {
|
||||
base64::engine::general_purpose::URL_SAFE.encode(arr)
|
||||
}
|
||||
|
||||
/// Generate a shared secret for a broadcast channel, consisting of 64 characters..
|
||||
///
|
||||
/// The string generated by this function has 384 bits of entropy
|
||||
/// and is returned as 64 Base64 characters, each containing 6 bits of entropy.
|
||||
/// 384 is chosen because it is sufficiently secure
|
||||
/// (larger than AES-128 keys used for message encryption)
|
||||
/// TODO: Is it still true that we use AES-128? This info is taken from create_id() comment above.
|
||||
/// and divides both by 8 (byte size) and 6 (number of bits in a single Base64 character).
|
||||
// TODO ask someone what a good size would be here
|
||||
pub(crate) fn create_broadcast_shared_secret() -> String {
|
||||
// ThreadRng implements CryptoRng trait and is supposed to be cryptographically secure.
|
||||
let mut rng = thread_rng();
|
||||
|
||||
// Generate 384 random bits.
|
||||
let mut arr = [0u8; 48];
|
||||
rng.fill(&mut arr[..]);
|
||||
|
||||
base64::engine::general_purpose::URL_SAFE.encode(arr)
|
||||
}
|
||||
|
||||
/// Returns true if given string is a valid ID.
|
||||
///
|
||||
/// All IDs generated with `create_id()` should be considered valid.
|
||||
@@ -767,10 +787,9 @@ pub(crate) fn inc_and_check<T: PrimInt + AddAssign + std::fmt::Debug>(
|
||||
/// In non-optimized builds, panics instead if so.
|
||||
#[macro_export]
|
||||
macro_rules! ensure_and_debug_assert {
|
||||
($cond:expr, $($arg:tt)*) => {
|
||||
let cond_val = $cond;
|
||||
debug_assert!(cond_val, $($arg)*);
|
||||
anyhow::ensure!(cond_val, $($arg)*);
|
||||
($($arg:tt)*) => {
|
||||
debug_assert!($($arg)*);
|
||||
anyhow::ensure!($($arg)*);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -802,18 +821,5 @@ macro_rules! ensure_and_debug_assert_ne {
|
||||
};
|
||||
}
|
||||
|
||||
/// Logs a warning if a condition is not satisfied.
|
||||
/// In non-optimized builds, panics also if so.
|
||||
#[macro_export]
|
||||
macro_rules! logged_debug_assert {
|
||||
($ctx:expr, $cond:expr, $($arg:tt)*) => {
|
||||
let cond_val = $cond;
|
||||
if !cond_val {
|
||||
warn!($ctx, $($arg)*);
|
||||
}
|
||||
debug_assert!(cond_val, $($arg)*);
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tools_tests;
|
||||
|
||||
+13
-37
@@ -12,9 +12,8 @@ use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::download::DownloadState;
|
||||
use crate::ephemeral;
|
||||
use crate::message::delete_msgs;
|
||||
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager};
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::tools::{self, SystemTime};
|
||||
use crate::{message, sql};
|
||||
|
||||
@@ -251,7 +250,7 @@ async fn test_resend_webxdc_instance_and_info() -> Result<()> {
|
||||
);
|
||||
let bob_grp = bob_instance.chat_id;
|
||||
assert_eq!(bob.get_last_msg_in(bob_grp).await.id, bob_instance.id);
|
||||
assert_eq!(bob_grp.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 1);
|
||||
assert_eq!(bob_grp.get_msg_cnt(&bob).await?, 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -870,14 +869,14 @@ async fn test_send_big_webxdc_status_update() -> Result<()> {
|
||||
let sent2 = &alice.pop_sent_msg().await;
|
||||
let alice_update = sent2.load_from_db().await;
|
||||
assert_eq!(alice_update.text, BODY_DESCR.to_string());
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 1);
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1);
|
||||
|
||||
// Bob receives the instance.
|
||||
let bob_instance = bob.recv_msg(sent1).await;
|
||||
let bob_chat_id = bob_instance.chat_id;
|
||||
assert_eq!(bob_instance.rfc724_mid, alice_instance.rfc724_mid);
|
||||
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 1);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1);
|
||||
|
||||
// Bob receives the status updates.
|
||||
bob.recv_msg_trash(sent2).await;
|
||||
@@ -897,7 +896,7 @@ async fn test_send_big_webxdc_status_update() -> Result<()> {
|
||||
r#"[{"payload":{"foo":"bar2"},"serial":2,"max_serial":3},
|
||||
{"payload":{"foo":"bar3"},"serial":3,"max_serial":3}]"#
|
||||
);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 1);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1486,7 +1485,7 @@ async fn test_webxdc_info_msg() -> Result<()> {
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
|
||||
let sent1 = &alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 1);
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1);
|
||||
|
||||
alice
|
||||
.send_webxdc_status_update(
|
||||
@@ -1496,7 +1495,7 @@ async fn test_webxdc_info_msg() -> Result<()> {
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
let sent2 = &alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 2);
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2);
|
||||
let info_msg = alice.get_last_msg().await;
|
||||
assert!(info_msg.is_info());
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage);
|
||||
@@ -1518,7 +1517,7 @@ async fn test_webxdc_info_msg() -> Result<()> {
|
||||
let bob_instance = bob.recv_msg(sent1).await;
|
||||
let bob_chat_id = bob_instance.chat_id;
|
||||
bob.recv_msg_trash(sent2).await;
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 2);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2);
|
||||
let info_msg = bob.get_last_msg().await;
|
||||
assert!(info_msg.is_info());
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage);
|
||||
@@ -1537,10 +1536,7 @@ async fn test_webxdc_info_msg() -> Result<()> {
|
||||
let alice2_instance = alice2.recv_msg(sent1).await;
|
||||
let alice2_chat_id = alice2_instance.chat_id;
|
||||
alice2.recv_msg_trash(sent2).await;
|
||||
assert_eq!(
|
||||
alice2_chat_id.get_msg_cnt(&alice2).await?,
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
assert_eq!(alice2_chat_id.get_msg_cnt(&alice2).await?, 2);
|
||||
let info_msg = alice2.get_last_msg().await;
|
||||
assert!(info_msg.is_info());
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage);
|
||||
@@ -1576,13 +1572,13 @@ async fn test_webxdc_info_msg_cleanup_series() -> Result<()> {
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
let sent2 = &alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 2);
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2);
|
||||
alice
|
||||
.send_webxdc_status_update(alice_instance.id, r#"{"info":"i2", "payload":2}"#)
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
let sent3 = &alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 2);
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2);
|
||||
let info_msg = alice.get_last_msg().await;
|
||||
assert_eq!(info_msg.get_text(), "i2");
|
||||
|
||||
@@ -1590,9 +1586,9 @@ async fn test_webxdc_info_msg_cleanup_series() -> Result<()> {
|
||||
let bob_instance = bob.recv_msg(sent1).await;
|
||||
let bob_chat_id = bob_instance.chat_id;
|
||||
bob.recv_msg_trash(sent2).await;
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 2);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2);
|
||||
bob.recv_msg_trash(sent3).await;
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 2);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2);
|
||||
let info_msg = bob.get_last_msg().await;
|
||||
assert_eq!(info_msg.get_text(), "i2");
|
||||
|
||||
@@ -1617,26 +1613,6 @@ async fn test_webxdc_info_msg_no_cleanup_on_interrupted_series() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_webxdc_info_msg_delete() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "c").await?;
|
||||
let instance = send_webxdc_instance(&t, chat_id).await?;
|
||||
|
||||
t.send_webxdc_status_update(instance.id, r#"{"info":"i1", "payload":1}"#)
|
||||
.await?;
|
||||
assert_eq!(chat_id.get_msg_cnt(&t).await?, 2);
|
||||
send_text_msg(&t, chat_id, "msg between info".to_string()).await?;
|
||||
assert_eq!(chat_id.get_msg_cnt(&t).await?, 3);
|
||||
t.send_webxdc_status_update(instance.id, r#"{"info":"i2", "payload":2}"#)
|
||||
.await?;
|
||||
assert_eq!(chat_id.get_msg_cnt(&t).await?, 4);
|
||||
delete_msgs(&t, &[instance.id]).await?;
|
||||
assert_eq!(chat_id.get_msg_cnt(&t).await?, 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// check that `info.internet_access` is not set for normal, non-integrated webxdc -
|
||||
// even if they use the deprecated option `request_internet_access` in manifest.toml
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
Group#Chat#10: Group chat [3 member(s)]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
|
||||
Msg#11🔒: (Contact#Contact#10): Hi! I created a group. [FRESH]
|
||||
Msg#12🔒: Me (Contact#Contact#Self): You left. [INFO] √
|
||||
Msg#13🔒: (Contact#Contact#10): Member charlie@example.net added by alice@example.org. [FRESH][INFO]
|
||||
Msg#14🔒: (Contact#Contact#10): What a silence! [FRESH]
|
||||
Msg#10🔒: (Contact#Contact#10): Hi! I created a group. [FRESH]
|
||||
Msg#11🔒: Me (Contact#Contact#Self): You left. [INFO] √
|
||||
Msg#12🔒: (Contact#Contact#10): Member charlie@example.net added by alice@example.org. [FRESH][INFO]
|
||||
Msg#13🔒: (Contact#Contact#10): What a silence! [FRESH]
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Single#Chat#10: bob@example.net [KEY bob@example.net] 🛡️
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO 🛡️]
|
||||
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#11🔒: Me (Contact#Contact#Self): Test – This is encrypted, signed, and has an Autocrypt Header without prefer-encrypt=mutual. √
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Single#Chat#10: bob@example.net [KEY bob@example.net] 🛡️
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO 🛡️]
|
||||
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#11🔒: (Contact#Contact#10): Heyho from DC [FRESH]
|
||||
Msg#13🔒: Me (Contact#Contact#Self): Sending with DC again √
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -4,6 +4,6 @@ Msg#11: info (Contact#Contact#Info): alice@example.org invited you to join this
|
||||
|
||||
Waiting for the device of alice@example.org to reply… [NOTICED][INFO]
|
||||
Msg#13: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO]
|
||||
Msg#17: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO 🛡️]
|
||||
Msg#17: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#18🔒: (Contact#Contact#10): Member Me added by alice@example.org. [FRESH][INFO]
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
|
||||
boundary="18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17"
|
||||
MIME-Version: 1.0
|
||||
From: <alice@example.org>
|
||||
To: <bob@example.net>
|
||||
Subject: [...]
|
||||
Date: Mon, 28 Jul 2025 14:15:14 +0000
|
||||
Message-ID: <48b9e9cc-2bae-4d41-89b4-a409e2c60c28@localhost>
|
||||
References: <48b9e9cc-2bae-4d41-89b4-a409e2c60c28@localhost>
|
||||
Chat-Version: 1.0
|
||||
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
|
||||
C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz7CkgQQFggAOgUCaIeF8RYhBC5vossjtTLXKGNLWGSw
|
||||
j2Gp7ZRDAhsDAh4BBQsJCAcCBhUKCQgLAgQWAgMBAScCGQEACgkQZLCPYantlEM66gD/b9qi1/H1Cr
|
||||
UwwlW2akVX86Q0gX6isyKfuNu/CdTdzaQBAIHRxvwlBNZr56qMGL7CyVy6LmBslLlbQwAdclM9t9UE
|
||||
uDgEXlh13RIKKwYBBAGXVQEFAQEHQAbtyNbLZIUBTwqeW2W5tVbrusWLJ+nTUmtF7perLbYdAwEIB8
|
||||
J4BBgWCAAgBQJoh4XxAhsMFiEELm+iyyO1MtcoY0tYZLCPYantlEMACgkQZLCPYantlEPG2QD8DthL
|
||||
48j1wnjw+Kby7CmAm/M+Me82izk8dGNPn442jJ4A/2r+YmqfUPK2XDXPRwvVBAIz5bL44fe7gNkUUu
|
||||
XMnzkP
|
||||
|
||||
|
||||
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17
|
||||
Content-Type: application/pgp-encrypted; charset="utf-8"
|
||||
Content-Description: PGP/MIME version identification
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Version: 1
|
||||
|
||||
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17
|
||||
Content-Type: application/octet-stream; name="encrypted.asc";
|
||||
charset="utf-8"
|
||||
Content-Description: OpenPGP encrypted message
|
||||
Content-Disposition: inline; filename="encrypted.asc";
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
wV4D5tq63hTeebASAQdAg7f2cyYQy+7Xrrlq4j3ik2Ba7L2sbh7Tt398Kke65Rkw
|
||||
sUscOFFConkBj7T5D8XS3e9dX5Bnf1z5jTj15OUZx/2iPTRQFtoVoRB6k6vt/qwq
|
||||
wcBMA+PY3JvEjuMiAQf/U9yB7kWhDMmFI7pMoktqqIvO8woKQG0dx2v1tzjIdtY/
|
||||
KlqMW8qpAqMHHalInGjj0LDDv4iWnNf8yTh71FyGQgipqH4FnYmbRoFW8iicMixN
|
||||
0ps6c6tBiZmyWDq2Ub9SIs9L/W2+vDlbyvFnow08MitOniZLC79KXB2ZRFwp7kOm
|
||||
1gqsZVvsy9fSM7oxXrxAtu2VNyp18emd6jAFWYAz2ISl2KuWYlmSJVexSvQWMRz/
|
||||
IkKWf9kOVAEuIeUnNo45S/DVb9uWQN3M1TrIdJ1BC+nEDBJokCWUGES6kchJc823
|
||||
EkLWpn6JDyRYV/7S9tmYrXodO3x4bSL02OnFbyUjutLCRwEduCtRzg5yrw4IAvJO
|
||||
1ujWf7CaAYE+49oh530HZ/gnBJb03nJhj1SOV0qO9ZquczaW0lhSEtfQF1lVWAVc
|
||||
BCE44YoR9sBqiJEJ0Msj/WLlso5RZHvHa64JrNJ7Jvisgn5vCMSfInzQ4zIZ7LfD
|
||||
sR444bJS9V6MNDSuhKmvPvu4wCFZgNQPs4V51yBX8Rjpn/3xws+NpUtisTt5J+ji
|
||||
KOQg3Thy/9NaNmuXHRbPBxzJKdHzL0bctzVxxDyZPcg6Z2Iteea4gQLEwp5HHw2R
|
||||
VMX97vtamsjp++tMihXRnrwX/a7x9MCAFuzZted4fB87VjHIdhf+CN2KshWsX+X5
|
||||
rPR3+oB6EBVXt8IroGMYLTtmMBS4SzEyiGmNFe/Z4tQSU6pEH+Aeo/FmUhUaMhln
|
||||
BAgRRmhw1Mt9nnuRzLwstpN4W5+mnmccNVg0T6kZz9D7Rbjd7FdzgF8d5K1cJiY/
|
||||
Nv5aajaFKSEwAO9TNHNoP3LD5KxMPiCkRh888V3YhCOwTUfwJG8riWgeyFCN6Xor
|
||||
7k6qHhd3T+1u8QTQkooLWSR7UYu9upQzExvmRPNyAXFyLrZUYjlymC1vn9PfH3Pd
|
||||
31aCGYaYPMdyenoAWTwy7VVSR3wpJuzwHHMeowzCA4TklD/tr2mZSpUrgeBqvS6s
|
||||
k68Pi5WjMs/kH/3Wl5Octb8XYN++DiG7RH5JzWYRchURen8jgPjzJPIUI5t+C8w0
|
||||
vXycuP1PdJcSfKTgxkaQgLs5cUoKEAgO5fA9bUPmjEcizb89im6SoObB+6o7hfwa
|
||||
AIr0TjpOmkdL3TANYA5448gTR4Kq+FwhsxX+fHU6OxwxLBozMcBzvjReKdJko8D+
|
||||
joaTEZBFxyvQUub5/MXmuulTEDhwURgGMbIN0TukdYlhUBfvyJ/wl/U9aHWvk+dz
|
||||
3OJ6d9SqTKPPyluTPV7p3GEDy1AwAex5FrP8SxRGRHiMjVhlbwrQB89ZcUX376ge
|
||||
5MPc4wBn44baPluklYcQtk6kp62KuLpfuLT8VbiLDfKT2FoZzoAnUnw=
|
||||
=HN9M
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
|
||||
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17--
|
||||
@@ -1,123 +0,0 @@
|
||||
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
|
||||
boundary="1854e0cc03e1678e_27ea5f4f17e5cce_dadff6845d0ea716"
|
||||
MIME-Version: 1.0
|
||||
From: <alice@example.org>
|
||||
To: <bob@example.net>, <fiona@example.net>,
|
||||
<alice@example.org>
|
||||
Subject: [...]
|
||||
Date: Thu, 1 Jan 1970 00:00:00 +0000
|
||||
Message-ID: <rfc724_mid>
|
||||
Chat-Version: 1.0
|
||||
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
|
||||
C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz7CkgQQFggAOgUCaIDT6RYhBC5vossjtTLXKGNLWGSw
|
||||
j2Gp7ZRDAhsDAh4BBQsJCAcCBhUKCQgLAgQWAgMBAScCGQEACgkQZLCPYantlEO5iQEAkKyQJYbI/n
|
||||
zh1IplGOQoJDLgObT8W0oicissPmwBskwBANT8jxTqS+y/8dXObksXNgDnByU5SAXe9o3QeeNKqKoN
|
||||
uDgEXlh13RIKKwYBBAGXVQEFAQEHQAbtyNbLZIUBTwqeW2W5tVbrusWLJ+nTUmtF7perLbYdAwEIB8
|
||||
J4BBgWCAAgBQJogNPpAhsMFiEELm+iyyO1MtcoY0tYZLCPYantlEMACgkQZLCPYantlEMrlQD+KJrE
|
||||
wIdwx6fCj6bAuqigKQMsV7If8Bctcv9Wk7QFK1kA/20HB1kPRTUSHyG+6UDyT2g40MOYwllmMMdHV6
|
||||
rCXGEG
|
||||
|
||||
|
||||
--1854e0cc03e1678e_27ea5f4f17e5cce_dadff6845d0ea716
|
||||
Content-Type: application/pgp-encrypted; charset="utf-8"
|
||||
Content-Description: PGP/MIME version identification
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Version: 1
|
||||
|
||||
--1854e0cc03e1678e_27ea5f4f17e5cce_dadff6845d0ea716
|
||||
Content-Type: application/octet-stream; name="encrypted.asc";
|
||||
charset="utf-8"
|
||||
Content-Description: OpenPGP encrypted message
|
||||
Content-Disposition: inline; filename="encrypted.asc";
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
wU4D5tq63hTeebASAQdATHbs7R5uRADpjsyAvrozHqQ/9nSrspwbLN6XJKuR3xcg
|
||||
eksHRdiKf6qnSIrSA5M5f8+jr1zmi6sUZQP/IziqRWnBwEwD49jcm8SO4yIBB/9K
|
||||
EASmrVqvRHc8VZhDR3VUYM8VFtbi+gbcu+/av7fII43AgN3qoluv6Wqj6jrf3zF2
|
||||
psDjegkrDp3GNMYOGR/qDTsouoEM46tqLHhrYB870c/JbVfk/6HbSb4nmrjur3DT
|
||||
63hWoqmh2SCdUAdGBuQMFE+3edrNX3AD3a8wsSVRuK8dpSacY8TgrAwmtaB+Epgv
|
||||
DZQocmOZqJZ6TgOrEeZ2xpn17Yiu3w+WMerfbIFqyD22W8EnxFRp9AAca7pY4KIx
|
||||
WVA1J311E3hmStN8kFKa5hM94Ihgo77YF/45KsLMjKblufvYbC05KExpyHFmrW09
|
||||
tn5KBedjkoUcD0eA8k6SwcBMA1Kpnh2oYq7LAQf/STVBew4ly3d9mFWw9JiBAMbb
|
||||
hFi6NbnwIR/ZynrhA3pK8EW9vYd/xb85/5cBjc+gc/rtDIKaskI2THPaTnfrk1X+
|
||||
EVIYRgua6v9JvPq+599j0rL1neHNPCgVw/zVF5BhI+nx5FfiRBez3GRoJV7sOpjH
|
||||
ftf+zwBT1cZ2z0WkUDHKXUATIqi9nH0ATCYAd7VzIPmlL4GLH5jh2OW9zWlE2sCn
|
||||
RvfRjL/izhAUmFW1Ks+HMTG15Qcok4rpdYRwFCfX3S5EaxLEgnruTKeDjDMEew0t
|
||||
Kzm8FsyW8nL62Jz/OGcCTprCtn8ex4AjgrWnru7PTcXb/4aKh39AGjmytAEYVtLL
|
||||
7gGGoqxH4N3BZ4KeWLzd86gcXEEATg6Wrj211BlSkdFSvL2XHBpLNDjj/MfGKRfa
|
||||
3PSRl+P3bNLF6by7HkdXPAIBqGn3qi1YQ3Bu7JQOSDJ/A0ypjS2TARcAL9c5Oz7/
|
||||
FgwN3b87tksX91w8T6mjhGcj+BNpWt1Xyc03uzt9ky8ZmrXuqF/f6RZ70JLWmKda
|
||||
qmRod8dGxjALgh313tTV1UPtainUBTun5ISiB/nwdlkg6x3VdGHCaPvTcrWdzgBl
|
||||
/bZQXAjyUiBPWwnsDsIS/fTOhoGj7CplJUnXrCLPcm3Dazh8XZcCxd3LgiYh7JX1
|
||||
X/54owVuwuqIh1yIcJGfIpsep7IgC5y27L6pMKaFk7o+HQZWE/yMQLGxWX201bi8
|
||||
t7nkTDUFy80BG/ex3mF1ynwC+Q+Wcrw9C0qNBFODAiiJyx+qHK5cw1OXOULXEnvu
|
||||
jUY2CUvLMYDPTXBT16nkjHq9pjedbL+SAjdxWNPx5x+XlLM8fsv1UPw8m6LQGH8v
|
||||
N0u/yCnyqj7xMFL2q/iIzKAVwkaMQx1kN87xqzm94Wv/TrbCMMiT05z36/uCJCVc
|
||||
NTVyP8d2hwAr02ctlMw9TlOtUIhKKWAaD5Yh49O8WNq7bnH73Ifz+/pQaM2u5eT9
|
||||
di5tuPPM8+SoNIvvUJr0X8/JCphzGj6MDwl2AdG3Iwo87EtUs205CpV7xIkQsgIH
|
||||
dKZNCmClNT3D8paRwHqDVwtMFSKfsqe5d/vt4Er4W2EXeF61fznunk+l2M4nxIRM
|
||||
aBEjRWzYJW2V0NaAaYJQEoULmExW0BqK3dzjhVbrreSeo2/18vzQlGSJ7gkIyUq7
|
||||
+gwgsxvmHTFOBHtvInb9ZozgDhv2/39Ig+S5PLiXwPkte6PwnclVh87e+W0cbbPu
|
||||
53me3jDJrBifsgLsJJP2rn+jL5+Jptb3ocZVnfcfZjHWA+i1YxRSWaalhKCpC4GF
|
||||
+0toGelW8nBkCaetNuaBueFnOvb8XtzZqucLm3li5sRpsqEDiYeL8vz2l1z1Rm6U
|
||||
+aqVP9j9n7RM00Cw2nEyUbJ4BMRqhQ/n5gXA3PG8rgPlcXKwdz0QmCURLU6YdYub
|
||||
Hazkehk9nI9KsXtcVricMWsSY2AzGOHVYK181KUd9Jsx0lClC6lRG7Ykfth9VIWG
|
||||
H+29yiiP0hkGfSY9hTubYqtzAkQ/vHWr7ZSlOM4ADa/6jhq5nLMzofY0+5h+DXkP
|
||||
CPXrNwx9K4demw8EM1KeYergrtROz+k9gFUm4bQTjvjevOknr4vA8PXwKqMLSwUG
|
||||
i+QfPbZNNSfqZK3QEE/9jbdOGPr1qPYvAeQnYkoI+ppCehSMK8sSrIPGCUVHvpj8
|
||||
uniPfK1nJL5c4klsBnWKLAFxhsq81/Eowfov0lsnhJNE58i2SEiSuf52+CJb/7ti
|
||||
vBLFRGP+fx76p2HHxh1A8M/RlgSASjfKVPWCAdszNh7psLtLOiKm/6KS++TW+RYB
|
||||
9sMWb+spy1xLfGz2LHw1n+VaQ9n6+BUj/QSGwMG+ypeOi+N2SmL1WEXx578mbO1A
|
||||
hSPSH5q3RdjxasULdaOr/FMm5TTcEBbMPSTb2Dq/c2Zl6tcPTKGIjisdXpWoPbvp
|
||||
er42Al5f1iu5II/RkkRYyR799ke0868suHUcCWr6qTH3tfPtk6+S+bS2jVXD9L4z
|
||||
KzF06J/B4BQ9v/SyVyQ6E9nd0Cf1bFi7Vw/sA/YyTKOxoPrLd2tUq4oARZyDX3bm
|
||||
liEhPYQEthRQ9e6WlFDbtawGPK9krlJOW+VFUZxyuzc/NJfbuf2aalbYulTicezY
|
||||
ZiqWVg7I7l3oT0iwekle+xy917Xqk4rUPPtnce3DKJOs0AxgjAZmfFvyxYqqVqWS
|
||||
do3f2agJZjfD5Zr3JHzSb/6ponAda9rGGVdkQyI4MBJBAAHmMneiq7OPM7dX0vXh
|
||||
OmRrzoq16bi2nYfDqa+q/vIwOMepyO3Czlwpz8xjVD4ldGEjUhf6hVbdlp82Dg6F
|
||||
yUcHgjnqtrggBksXFKk2+n+3r4X0vktzw9kxnbCmtp9QCBy955mdu5byAK/1V/5G
|
||||
D1iwDALx/arBSXAhw8tb/ocE+VNSnTSXDKsZ5p3IXsTZmrTaMhkHBvU7456JKptE
|
||||
hpguQbSAPCPfO2MA1XACVL2T2eiTkd2Rh5vKr5PhslOVI80tjK9YQF246VOqBur1
|
||||
zr5MqzN65tlXTOg8/SaEB1aNXw0Hr6vOz38f6rECNpcGp9Dzwh5VBq8FbYpgYBg3
|
||||
Dpq/0GYYZrwd4+pUuAjFCn2Ib5+Mr5oIdHVGOTnwICGHoAjjmNouVNPHgdlU/zsp
|
||||
uENVX4Kqu4mz1GAvI2Iv4KXqPXCM5IJfInm+QoqfNCd565iSX0ZSFxO14XYHyqNE
|
||||
CzuirR7hYzKjk7g3s9/zMkra7sZ3I+SgSjrVn5gDfYvfTMmi6JetnExrfiNsLes0
|
||||
bU4iZ06CBWS2RV2fHIBqeVBLONgETsmNO8fd3IKz3L3LBzwIBxdjPzkb7/UnbR0/
|
||||
2XBDquVabit+wrXd8wYBWmWtrE+wZFtXVaRvZESrFe8PxSua5ErCIxFecdb3DQdc
|
||||
P53dKs+TmGw+/R+x08lZTAIZJFGjoMlaafY4xqonv7JEGqDLo8C0Awss1LayfS0+
|
||||
0pnHA+nkVjfx14xjsBnBGPsYmEPUjtI567gMRPppNga9NH9zw0CsSFpxBzfmFe8j
|
||||
Nl/6YeWzZ28F2W+JK45Cj+9IKkciGRbc4dTeRz9p1dbCxwJyLtFOPYM8wBIgc2V5
|
||||
sDMebe74TMbBaBWsIAx9W9fEwj3OCdDTbvaFpbFJ24gsfmZOA6ZMCaWbk8Z8n5x0
|
||||
iClgfXyJZt9noK1SYPssHvNsxSsVpgSk1eKR036azz4syKsaLqxNNdLdfEPHYVo7
|
||||
nn4I9oM0ElvtQcvHg7lK7U7rgyI0RpVFyEjI8x2DHm1jRAFiWrmJIHHEUnVkNXsX
|
||||
kjY2vas5l7lCX7/9JzfxP3vLhrZAuuXAJnUSXHQrLraXvMvgnRSe+zqx67fSfvEQ
|
||||
iwkHeed01c7g7kDp8wI4gNXhsb+bb/hra2fhz/J4EvcVwsl4/u+7Dk0GUO4SpkLX
|
||||
tEK2aCp0M6cL1viZ1IylnReNXhwa7E6mfKShe12+a7HzJO+ZbLzZ9DKTUH34EOeR
|
||||
gU2dyA798azp0ZQu+UtHoYHxE8P+b6W3OKQZEWqULu9HLxQvuM1HSq4a6XJH5Tnz
|
||||
r+H7IH6lSk2l8nFtjJBgI+DqHYqXkeKtlB1oz1bguSXqYOoviWd/GLsuZdne0d/x
|
||||
BIDlYlp2h6rmvp+rVCaG6EJ6qEcMAkgSC2KgXP810/pwBlfFigQsB43WicGE5Znb
|
||||
mhYOtQSCgYy5b49yFJU8n6KAVLHE0mrfmFZozPPWstnHXtriuFzri8E0LVymYnfy
|
||||
ICOSta9MOQK++/CchT8hnjQfKaLx9BdwrUC/qXQjePcz2zTb64ex59pA4rGT0wal
|
||||
gbGc9KkJEl6Dxe+CcAgL/4a5gXvXh1w8cPtimSSx6dpuS8cU7xb3XWiWpZKRsWOV
|
||||
ul9OqXbw2gZ4lh4zfLv7WJN9dwMrKjG0LA0QqEZUA1/I9bpXIts80fz4vld4XPyq
|
||||
wESjCWD6C+Vzvv5iEdKUYS9urL6K3WNQ21foiurcARTE9cUvf4ljc0vG6rpDp6QL
|
||||
ak9EJxxDte75kI/MWup9CAWoZECFpTqX3ETbiMaymGk7We++sP0ULuwcghAM1/sA
|
||||
v6teU5yQMCOjI8Gnhif43sdB9msuHzi+/v+7QFPTOn949o3au5rA+NE4N1Qfp5bi
|
||||
3hYAHpz5q/BgL9IzHoqkGgoJBh3J8V+86GV28E8aiMFodenzvojowvISdAobvY1O
|
||||
Y40VZYmPsN8dDzoD4LBFxKIryz5d6dT5j34vis7/i7UYWmvBzb6Nb/gf77CvjSwL
|
||||
iYEMKLlgsLNBGq68PXCEIG9/sYpQzsFALB4Fx5Hc4GM4/Yo1oQDT10tHZNv3ehLo
|
||||
aTsQQwj9mjObmWC2d4FpWWrnFMayCqY5ZrcPeyA7jrR9+hPGzUlCuha8dPQ3+JKi
|
||||
lijeswzqV0/4Md+0Ghu/sxf2S0hUEQ20m1vXTXrHch3QTrQY7wijvVRJfpYSGdZW
|
||||
hrSE9DrByWEL61imLaOxU+SEPQ8w6ia3m/tREeIo75ZrJ8lgasb5/CKU/gvXnVCY
|
||||
3FtT/YinpYY/FBYhGK1QLX6NQuN3sMr7Jt80i7G9QI6O1g8CFBR5qqNZIRp160/1
|
||||
dE2YxsihlFM2jFWA2V5HF03WQiLakaYc0uxomGpps/BGnb0Gv5pqIpCo5Ii06RaT
|
||||
xicreEfIE24TLmQaI3vrbMqBc6Yg6XTUsvEnwo3lGw==
|
||||
=a+ak
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
|
||||
--1854e0cc03e1678e_27ea5f4f17e5cce_dadff6845d0ea716--
|
||||
|
||||
Reference in New Issue
Block a user