Compare commits

...
Author SHA1 Message Date
link2xt 99a57c7084 refactor(deltachat-jsonrpc): enable verbatimModuleSyntax 2023-11-13 09:24:55 +00:00
iequidoo ad5a5ad3db feat: Multi-device broadcast lists (#4953) 2023-11-13 05:00:57 -03:00
iequidoo 74081d8a36 feat: Sync Chat::name across devices (#4953) 2023-11-13 05:00:57 -03:00
iequidoo 34a434f07c refactor: Move chat-related code from sync to chat module
- Reduce cross-module dependencies.
- Stop bloating the `sync` module while implementing synchronisation of more entities.
- Now there's the only `ChatId` :)
2023-11-13 05:00:57 -03:00
iequidoo dc944d8ca7 test: Broadcast lists synchronisation (#4953) 2023-11-13 05:00:57 -03:00
iequidoo b06a7e7197 fix: Context::execute_sync_items: Ignore all errors (#4817)
An error while executing an item mustn't prevent next items from being executed. There was a comment
that only critical errors like db write failures must be reported upstack, but in fact it's hard to
achieve in the current design, there are no error codes or so, so it's bug-prone. E.g.
`ChatAction::Block` and `Unblock` already reported all errors upstack. So, let's make error handling
the same as everywhere and just ignore any errors in the item execution loop. In the worst case we
just do more unsuccessful db writes f.e.
2023-11-13 05:00:57 -03:00
iequidoo fa61d90115 fix: Ignore errors from generating sync messages
It's sufficient if the local state is updated successfully, no need to fail the whole
operation. Anyway delivery of sync messages and applying them on other devices are beyond of our
control. If an error occurs when generating a sync messages, probably a log message is sufficient,
no need to even show it to a user. As for the tests, anyway there are ones on synchronisation which
perform necessary checks. Particularly, some sync messages can't be generated if an account is
unconfigured. Adding the corresponding checks to the device synchronisation code (and maybe even
more checks in the future) would complicate the code unnecessarily. Even errors caused by bugs in
this code aren't a reason to fail a local operation.
2023-11-13 05:00:57 -03:00
iequidoo 7977c9ab44 feat: Sync creating broadcast lists across devices (#4953) 2023-11-13 05:00:57 -03:00
iequidoo 6273a7d54e refactor: Rename Chat::add_sync_item() to sync() 2023-11-13 05:00:57 -03:00
iequidoo 4d1a9c2aa1 feat: Sync chat contacts across devices (#4953)
Sync chat contacts across devices for broadcast lists and groups. This needs the corresponding chat
to exist on other devices which is not the case for unpromoted groups, so it fails for them now but
it's only a warning and will work once creation of unpromoted groups is synchronised too.
2023-11-13 05:00:57 -03:00
Hocuri b26ded423b Fix #4982: Allow to send unverified securejoin messages to protected chats 2023-11-12 20:15:38 +01:00
Hocuri e4b6eba5d7 Correct comment 2023-11-12 20:15:38 +01:00
Hocuri bc225024a1 Add tests 2023-11-12 20:15:38 +01:00
Hocuri e616ecf160 Fix CI 2023-11-12 20:15:38 +01:00
Hocuri f93562c6bf Refactorings 2023-11-12 20:15:38 +01:00
Hocuri ac39c3699b Make sure the resetup happy path works 2023-11-12 20:15:38 +01:00
link2xt 091bc1ab13 fix: encode chat name in the List-ID header
List-ID header is added for broadcast lists.
UTF-8 in email headers is allowed only if
all recipient MTAs support SMTPUTF8 extension,
which is not always the case even if our submission service
reports SMTPUTF8 support.
2023-11-12 19:56:09 +01:00
link2xt fcbb66a788 chore: update dependencies 2023-11-12 13:35:13 +00:00
link2xt ab2bc3bfb2 chore: update dependencies 2023-11-10 21:28:28 +00:00
link2xt 42dd6f9d08 chore(release): prepare for 1.130.0 2023-11-10 21:26:27 +00:00
Sebastian Klähn 465bcd46f8 test: test chatlist can load for obfuscated chats (#4979)
close #4598
2023-11-10 20:23:51 +00:00
iequidoo cc88a6cb58 fix: smtp_loop(): Don't grow timeout if interrupted early (#4833)
Don't grow timeout if interrupted early and slept not enough. Also:
- Don't grow timeout too fast, but 1.5--2 times (randomly) per iteration.
- Don't interrupt if rate-limited.
- Reset timeout if rate-limited. Rate limit isn't an error, so we can start from 30 secs again if an
  error happens then.
2023-11-10 17:17:47 -03:00
iequidoo ba8f1bfcfd feat: Grow sleep durations on errors in Imap::fake_idle() (#4424) 2023-11-10 17:17:47 -03:00
link2xt d4d6ced957 fix: do not return hidden chat from dc_get_chat_id_by_contact_id 2023-11-10 20:00:28 +00:00
Hocuri 0b664e75cb docs: mention that people need to set the chatmail server to run the rpc tests 2023-11-10 19:28:29 +00:00
link2xt 1a4c2953f7 refactor: get rid of InterruptInfo
It was passed around, but the boolean inside was not used.
2023-11-10 16:38:01 +00:00
link2xt 765c95de39 refactor(imap): do not check if IDLE is supported twice
We already check if IDLE is supported outside of idle()
2023-11-10 16:38:01 +00:00
link2xt b2ea8f54df refactor(imap): reduce indentation level in fetch_idle() 2023-11-10 16:38:01 +00:00
link2xt d7aecabcaa fix: do not apply group changes to special chats
This is a similar check to the one we have in `save_locations`.
2023-11-10 15:46:45 +00:00
holger krekel 9ca049051c fix and streamline a little 2023-11-10 16:46:40 +01:00
Sebastian Klähn 790509676f fix: remove unused --liveconfig option
close  #4892
2023-11-10 16:46:40 +01:00
link2xt ce016eb567 feat: add secondary verified key
When a key is gossiped for the contact in a verified chat,
it is stored in the secondary verified key slot.

The messages are then encrypted to the secondary verified key
if they are also encrypted to the contact introducing this secondary key.

Chat-Group-Member-Added no longer updates the verified key.
Verified group recovery only relies on the secondary verified key.

When a message is received from a contact
signed with a secondary verified key,
secondary verified key replaces the primary verified key.
When verified key is changed for the contact
in response to receiving a message
signed with a secondary verified key,
"Setup changed" message is added
to the same chat where the message is received.
2023-11-10 15:45:10 +00:00
link2xt 57e34abe98 fix: make UidGrouper robust against duplicate UIDs 2023-11-10 15:42:40 +00:00
link2xt fd92b7c455 fix: remove Reporting-UA from read receipts
Do not leak Delta Chat core verison to everyone
2023-11-10 15:37:30 +00:00
bjoern 0ee68d1dfc back to two verification-check functions (#4966)
this pr keeps and refines documentation added in #4951, however, reverts
the api introduced by #4951
which turns out to be not useful for UI in practise:

UI anyway check for chat/no-chat beforehand,
so a simple condition in profiles as
`green_checkmark = chat_exist ? chat_is_protected() :
contact_is_verified()`
is more useful in practise and is waht UI need and did already in the
past. (https://github.com/deltachat/deltachat-android/pull/2836 shows a
detailed discussion)

(as a side effect, beside saving code,
this PR saves up to three database calls
(get contact from chat in UI to pass it to profile_is_verified(), get
chat from contact in core, load is_protected in core) - instead, core
can use already is_protected from already loaded chat object)

/me did check rust-tests, fingers crossed for python tests
/me should re-setup python tests on local machine at some point :)
2023-11-09 23:31:18 +01:00
link2xt 1856c622a1 test(deltachat-rpc-client): log all events as debug messages
This is necessary to debug the tests.
2023-11-09 10:06:05 +01:00
link2xt 0a48a2effa refactor: replace inviter_progress macro with a function 2023-11-09 01:58:25 +00:00
link2xt 543864f0f5 test(deltachat-rpc-client): fix securejoin tests
Wait for Bob (joiner) progress 1000
and do not return too early in test_qr_setup_contact().
2023-11-09 01:57:13 +00:00
link2xt 0fe94e47cc test: enable verified 1:1 chats in deltachat-rpc-client tests 2023-11-09 01:57:13 +00:00
link2xt fc09210aea api: emit JoinerProgress(1000) event when Bob verifies Alice 2023-11-09 01:57:13 +00:00
Hocuri 3e194969c0 fix: Mark 1:1 chat as protected when joining a group 2023-11-09 01:57:13 +00:00
link2xt 391cffb454 docs: add dc_chat_is_protection_broken() to 1.127.0 changelog 2023-11-08 17:53:17 +00:00
Sebastian Klähn 47486f8bab Add Changelog entry for is_protection_broken (#4959)
close #4885
2023-11-08 18:41:55 +01:00
link2xt 620e363ce6 refactor(deltachat-rpc-client): use itertools for thread-safe request ID generation 2023-11-08 17:06:25 +00:00
link2xt 0c2276775d test: test that joining a group verifies Alice and vice versa 2023-11-08 01:56:45 +00:00
iequidoo ad51a7cd85 feat: apply_group_changes: Add system messages about implicitly added members 2023-11-07 21:09:25 -03:00
B. Petersen 28952789a4 fix: raise lower auto-download limit to 160k 2023-11-07 23:27:02 +00:00
link2xt 14adcdb517 fix: treat reset state as encryption not preferred
This will still degrade 1:1 chats to no encryption,
but will not cause the group to disable encryption
simply because one user got into reset state.
2023-11-07 21:24:23 +00:00
link2xt cc80590488 test(deltachat-rpc-client): move securejoin tests to a separate file 2023-11-07 18:21:44 +00:00
link2xt 48416289ac api: add dc_contact_is_profile_verified() 2023-11-07 18:14:33 +00:00
link2xt 003a27f625 refactor: hide ChatId::get_for_contact() from public API 2023-11-07 01:19:14 +00:00
bjoern bff4a2259f chore: update provider-db (#4949) 2023-11-07 01:12:31 +00:00
link2xt 9adf856705 chore: upgrade toml dependency 2023-11-06 22:22:12 +00:00
55 changed files with 1843 additions and 1004 deletions
+42
View File
@@ -1,5 +1,44 @@
# Changelog
## [1.130.0] - 2023-11-10
### API-Changes
- Emit JoinerProgress(1000) event when Bob verifies Alice.
- JSON-RPC: add `ContactObject.is_profile_verified` property.
- Hide `ChatId::get_for_contact()` from public API.
### Features / Changes
- Add secondary verified key.
- Add info messages about implicitly added members.
- Treat reset state as encryption not preferred.
- Grow sleep durations on errors in Imap::fake_idle() ([#4424](https://github.com/deltachat/deltachat-core-rust/pull/4424)).
### Fixes
- Mark 1:1 chat as protected when joining a group.
- Raise lower auto-download limit to 160k.
- Remove `Reporting-UA` from read receipts.
- Do not apply group changes to special chats. Avoid adding members to the trash chat.
- imap: make `UidGrouper` robust against duplicate UIDs.
- Do not return hidden chat from `dc_get_chat_id_by_contact_id`.
- Smtp_loop(): Don't grow timeout if interrupted early ([#4833](https://github.com/deltachat/deltachat-core-rust/pull/4833)).
### Refactor
- imap: Do not FETCH right after `scan_folders()`.
- deltachat-rpc-client: Use `itertools` instead of `Lock` for thread-safe request ID generation.
### Tests
- Remove unused `--liveconfig` option.
- Test chatlist can load for corrupted chats ([#4979](https://github.com/deltachat/deltachat-core-rust/pull/4979)).
### Miscellaneous Tasks
- Update provider-db ([#4949](https://github.com/deltachat/deltachat-core-rust/pull/4949)).
## [1.129.1] - 2023-11-06
### Fixes
@@ -98,6 +137,8 @@
- [**breaking**] Remove unused `dc_set_chat_protection()`
- Hide `DcSecretKey` trait from the API.
- Verified 1:1 chats ([#4315](https://github.com/deltachat/deltachat-core-rust/pull/4315)). Disabled by default, enable with `verified_one_on_one_chats` config.
- Add api `chat::Chat::is_protection_broken`
- Add `dc_chat_is_protection_broken()` C API.
### CI
@@ -3114,3 +3155,4 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.128.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.127.2...v1.128.0
[1.129.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.128.0...v1.129.0
[1.129.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.129.0...v1.129.1
[1.130.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.129.1...v1.130.0
Generated
+63 -69
View File
@@ -522,9 +522,9 @@ dependencies = [
[[package]]
name = "bstr"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c79ad7fb2dd38f3dabd76b09c6a5a20c038fc0213ef1e9afd30eb777f120f019"
checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c"
dependencies = [
"memchr",
"serde",
@@ -707,18 +707,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.4.7"
version = "4.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b"
checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.4.7"
version = "4.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663"
checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc"
dependencies = [
"anstyle",
"clap_lex",
@@ -1087,7 +1087,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.129.1"
version = "1.130.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1165,7 +1165,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.129.1"
version = "1.130.0"
dependencies = [
"anyhow",
"async-channel 2.1.0",
@@ -1189,7 +1189,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "1.129.1"
version = "1.130.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1204,7 +1204,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.129.1"
version = "1.130.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1229,7 +1229,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.129.1"
version = "1.130.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1744,9 +1744,9 @@ dependencies = [
[[package]]
name = "env_logger"
version = "0.10.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece"
dependencies = [
"humantime",
"is-terminal",
@@ -1763,9 +1763,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.5"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860"
checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e"
dependencies = [
"libc",
"windows-sys",
@@ -1819,9 +1819,9 @@ dependencies = [
[[package]]
name = "fallible-iterator"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
@@ -1899,9 +1899,9 @@ dependencies = [
[[package]]
name = "fiat-crypto"
version = "0.2.2"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a481586acf778f1b1455424c343f71124b048ffa5f4fc3f8f6ae9dc432dcb3c7"
checksum = "f69037fe1b785e84986b4f2cbcf647381876a00671d25ceef715d7812dd7e1dd"
[[package]]
name = "filetime"
@@ -2099,9 +2099,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.10"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
dependencies = [
"cfg-if",
"js-sys",
@@ -2301,9 +2301,9 @@ dependencies = [
[[package]]
name = "http"
version = "0.2.9"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
checksum = "f95b9abcae896730d42b78e09c155ed4ddf82c07b4de772c64aee5b2d8b7c150"
dependencies = [
"bytes",
"fnv",
@@ -2335,9 +2335,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "human-panic"
version = "1.2.1"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b82da652938b83f94cfdaaf9ae7aaadb8430d84b0dfda226998416318727eac2"
checksum = "7a79a67745be0cb8dd2771f03b24c2f25df98d5471fe7a595d668cfa2e6f843d"
dependencies = [
"backtrace",
"os_info",
@@ -2679,9 +2679,9 @@ dependencies = [
[[package]]
name = "libsqlite3-sys"
version = "0.26.0"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
dependencies = [
"cc",
"openssl-sys",
@@ -2697,9 +2697,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.4.10"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
[[package]]
name = "lock_api"
@@ -2834,7 +2834,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
dependencies = [
"getrandom 0.2.10",
"getrandom 0.2.11",
]
[[package]]
@@ -3558,9 +3558,9 @@ dependencies = [
[[package]]
name = "proptest"
version = "1.3.1"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e"
checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf"
dependencies = [
"bitflags 2.4.1",
"lazy_static",
@@ -3568,7 +3568,7 @@ dependencies = [
"rand 0.8.5",
"rand_chacha 0.3.1",
"rand_xorshift",
"regex-syntax 0.7.5",
"regex-syntax 0.8.2",
"unarray",
]
@@ -3631,9 +3631,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.10.5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c78e758510582acc40acb90458401172d41f1016f8c9dde89e49677afb7eec1"
checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a"
dependencies = [
"bytes",
"rand 0.8.5",
@@ -3744,7 +3744,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.10",
"getrandom 0.2.11",
]
[[package]]
@@ -3825,7 +3825,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4"
dependencies = [
"getrandom 0.2.10",
"getrandom 0.2.11",
"libredox",
"thiserror",
]
@@ -3868,12 +3868,6 @@ version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "regex-syntax"
version = "0.8.2"
@@ -3971,7 +3965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b"
dependencies = [
"cc",
"getrandom 0.2.10",
"getrandom 0.2.11",
"libc",
"spin 0.9.8",
"untrusted 0.9.0",
@@ -4030,9 +4024,9 @@ dependencies = [
[[package]]
name = "rusqlite"
version = "0.29.0"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d"
dependencies = [
"bitflags 2.4.1",
"fallible-iterator",
@@ -4116,9 +4110,9 @@ dependencies = [
[[package]]
name = "rustls-pemfile"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
"base64 0.21.5",
]
@@ -4204,9 +4198,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.15"
version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c"
checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29"
dependencies = [
"dyn-clone",
"schemars_derive",
@@ -4216,9 +4210,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.15"
version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c"
checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967"
dependencies = [
"proc-macro2",
"quote",
@@ -4295,9 +4289,9 @@ dependencies = [
[[package]]
name = "self_cell"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6"
checksum = "e388332cd64eb80cd595a00941baf513caffae8dce9cfd0467fc9c66397dade6"
[[package]]
name = "semver"
@@ -4310,9 +4304,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.190"
version = "1.0.192"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
dependencies = [
"serde_derive",
]
@@ -4337,9 +4331,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.190"
version = "1.0.192"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
dependencies = [
"proc-macro2",
"quote",
@@ -4510,9 +4504,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.11.1"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
[[package]]
name = "smawk"
@@ -4878,9 +4872,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.33.0"
version = "1.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
dependencies = [
"backtrace",
"bytes",
@@ -4907,9 +4901,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
@@ -4995,9 +4989,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.7.8"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
dependencies = [
"serde",
"serde_spanned",
@@ -5016,9 +5010,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.19.15"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
dependencies = [
"indexmap 2.1.0",
"serde",
@@ -5283,7 +5277,7 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
dependencies = [
"getrandom 0.2.10",
"getrandom 0.2.11",
"serde",
]
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.129.1"
version = "1.130.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.70"
@@ -72,7 +72,7 @@ quick-xml = "0.31"
rand = "0.8"
regex = "1.9"
reqwest = { version = "0.11.20", features = ["json"] }
rusqlite = { version = "0.29", features = ["sqlcipher"] }
rusqlite = { version = "0.30", features = ["sqlcipher"] }
rust-hsluv = "0.1"
sanitize-filename = "0.5"
serde_json = "1.0"
@@ -90,7 +90,7 @@ tokio-io-timeout = "1.2.0"
tokio-stream = { version = "0.1.14", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = "0.7.9"
toml = "0.7"
toml = "0.8"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.129.1"
version = "1.130.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
+5 -13
View File
@@ -3744,12 +3744,9 @@ int dc_chat_can_send (const dc_chat_t* chat);
*
* UI should display a green checkmark
* in the chat title,
* in the chat profile title and
* in the chatlist item
* and in the chat profile
* 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.
*
* @memberof dc_chat_t
* @param chat The chat object.
@@ -5056,14 +5053,9 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
*
* If contact is verified
* UI should display green checkmark after the contact name
* in contact list items and
* in chat member list items.
*
* Do not use this function when displaying the contact profile view.
* Display green checkmark in the title of the contact profile
* if 1:1 chat with the contact exists and is protected.
* Use dc_contact_get_verified_id to display the verifier contact
* in the info section of the contact profile.
* 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.
@@ -5073,7 +5065,6 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
int dc_contact_is_verified (dc_contact_t* contact);
/**
* Return the contact ID that verified a contact.
*
@@ -6249,6 +6240,7 @@ void dc_event_unref(dc_event_t* event);
* @param data2 (int) The progress as:
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
* (Bob has verified alice and waits until Alice does the same for him)
* 1000=vg-member-added/vc-contact-confirm received
*/
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.129.1"
version = "1.130.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
+11 -3
View File
@@ -24,11 +24,17 @@ pub struct ContactObject {
///
/// If this is true
/// UI should display green checkmark after the contact name
/// in the title of the contact profile,
/// in contact list items and
/// in chat member list items.
/// 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.
///
/// This indicates whether 1:1 chat has a green checkmark
/// or will have a green checkmark if created.
is_profile_verified: bool,
/// The ID of the contact that verified this contact.
///
/// If this is present,
@@ -52,6 +58,7 @@ impl ContactObject {
None => None,
};
let is_verified = contact.is_verified(context).await? == VerifiedStatus::BidirectVerified;
let is_profile_verified = contact.is_profile_verified(context).await?;
let verifier_id = contact
.get_verifier_id(context)
@@ -70,6 +77,7 @@ impl ContactObject {
name_and_addr: contact.get_name_n_addr(),
is_blocked: contact.is_blocked(),
is_verified,
is_profile_verified,
verifier_id,
last_seen: contact.last_seen(),
was_seen_recently: contact.was_seen_recently(),
@@ -1,4 +1,5 @@
import { DcEvent, DeltaChat } from "../deltachat.js";
import type { DcEvent } from "../deltachat.js";
import { DeltaChat } from "../deltachat.js";
var SELECTED_ACCOUNT = 0;
+1 -1
View File
@@ -53,5 +53,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.129.1"
"version": "1.130.0"
}
+3 -2
View File
@@ -1,8 +1,9 @@
import * as T from "../generated/types.js";
import { EventType } from "../generated/types.js";
import type { EventType } from "../generated/types.js";
import * as RPC from "../generated/jsonrpc.js";
import { RawClient } from "../generated/client.js";
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import type { Request } from "yerpc";
import { WebsocketTransport, BaseTransport } from "yerpc";
import { TinyEmitter } from "@deltachat/tiny-emitter";
type Events = { ALL: (accountId: number, event: EventType) => void } & {
+2 -1
View File
@@ -4,7 +4,8 @@ import chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
import { StdioDeltaChat as DeltaChat } from "../deltachat.js";
import { RpcServerHandle, startServer } from "./test_base.js";
import type { RpcServerHandle } from "./test_base.js";
import { startServer } from "./test_base.js";
describe("basic tests", () => {
let serverHandle: RpcServerHandle;
+4 -2
View File
@@ -1,6 +1,8 @@
import { assert, expect } from "chai";
import { StdioDeltaChat as DeltaChat, DcEvent } from "../deltachat.js";
import { RpcServerHandle, createTempUser, startServer } from "./test_base.js";
import { StdioDeltaChat as DeltaChat } from "../deltachat.js";
import type { DcEvent } from "../deltachat.js";
import type { RpcServerHandle } from "./test_base.js";
import { createTempUser, startServer } from "./test_base.js";
const EVENT_TIMEOUT = 20000;
@@ -10,6 +10,7 @@
"target": "ES2017",
"module": "es2020",
"declaration": true,
"verbatimModuleSyntax": true,
"esModuleInterop": true,
"moduleResolution": "node",
"noImplicitAny": true,
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.129.1"
version = "1.130.0"
license = "MPL-2.0"
edition = "2021"
@@ -11,7 +11,7 @@ deltachat = { path = "..", features = ["internals"]}
dirs = "5"
log = "0.4.20"
pretty_env_logger = "0.5"
rusqlite = "0.29"
rusqlite = "0.30"
rustyline = "12"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
+1 -1
View File
@@ -25,7 +25,7 @@ $ pip install .
## Testing
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
2. Run `PATH="../target/debug:$PATH" tox`.
2. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
@@ -21,7 +21,9 @@ class ACFactory:
self.deltachat = deltachat
def get_unconfigured_account(self) -> Account:
return self.deltachat.add_account()
account = self.deltachat.add_account()
account.set_config("verified_one_on_one_chats", "1")
return account
def get_unconfigured_bot(self) -> Bot:
return Bot(self.get_unconfigured_account())
@@ -1,11 +1,12 @@
import itertools
import json
import logging
import os
import subprocess
import sys
from queue import Queue
from threading import Event, Lock, Thread
from typing import Any, Dict, Optional
from threading import Event, Thread
from typing import Any, Dict, Iterator, Optional
class JsonRpcError(Exception):
@@ -23,8 +24,7 @@ class Rpc:
self._kwargs = kwargs
self.process: subprocess.Popen
self.id: int
self.id_lock: Lock
self.id_iterator: Iterator[int]
self.event_queues: Dict[int, Queue]
# Map from request ID to `threading.Event`.
self.request_events: Dict[int, Event]
@@ -55,8 +55,7 @@ class Rpc:
preexec_fn=os.setpgrp, # noqa: PLW1509
**self._kwargs,
)
self.id = 0
self.id_lock = Lock()
self.id_iterator = itertools.count(start=1)
self.event_queues = {}
self.request_events = {}
self.request_results = {}
@@ -133,7 +132,9 @@ class Rpc:
event = self.get_next_event()
account_id = event["contextId"]
queue = self.get_queue(account_id)
queue.put(event["event"])
event = event["event"]
logging.debug("account_id=%d got an event %s", account_id, event)
queue.put(event)
except Exception:
# Log an exception if the event loop dies.
logging.exception("Exception in the event loop")
@@ -145,11 +146,7 @@ class Rpc:
def __getattr__(self, attr: str):
def method(*args) -> Any:
self.id_lock.acquire()
self.id += 1
request_id = self.id
self.id_lock.release()
request_id = next(self.id_iterator)
request = {
"jsonrpc": "2.0",
"method": attr,
@@ -0,0 +1,249 @@
import logging
from deltachat_rpc_client import Chat, SpecialContactId
def test_qr_setup_contact(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
while True:
event = alice.wait_for_event()
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
while True:
event = bob.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
def test_qr_securejoin(acfactory):
alice, bob = acfactory.get_online_accounts(2)
logging.info("Alice creates a verified group")
alice_chat = alice.create_group("Verified group", protect=True)
logging.info("Bob joins verified group")
qr_code, _svg = alice_chat.get_qr_code()
bob.secure_join(qr_code)
while True:
event = alice.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
while True:
event = bob.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
def test_verified_group_recovery(acfactory, rpc) -> None:
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates verified group")
chat = ac1.create_group("Verified group", protect=True)
assert chat.get_basic_snapshot().is_protected
logging.info("ac2 joins verified group")
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
while True:
event = ac1.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
while True:
event = ac1.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
logging.info("ac2 reverifies with ac3")
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
while True:
event = ac3.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3
ac3_chat.send_text("Hi!")
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("Received message %s", snapshot.text)
assert snapshot.text == "Hi!"
# ac1 contact cannot be verified by ac2 because ac3 did not gossip ac1 key in the "Hi!" message.
ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
assert not ac1_contact.get_snapshot().is_verified
ac3_contact_id_ac1 = rpc.lookup_contact_id_by_addr(ac3.id, ac1.get_config("addr"))
ac3_chat.remove_contact(ac3_contact_id_ac1)
ac3_chat.add_contact(ac3_contact_id_ac1)
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("ac2 got event message: %s", snapshot.text)
assert "removed" in snapshot.text
event = ac2.wait_for_incoming_msg_event()
msg_id = event.msg_id
chat_id = event.chat_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("ac2 got event message: %s", snapshot.text)
assert "added" in snapshot.text
assert ac1_contact.get_snapshot().is_verified
chat = Chat(ac2, chat_id)
chat.send_text("Works again!")
msg_id = ac3.wait_for_incoming_msg_event().msg_id
message = ac3.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.text == "Works again!"
ac1.wait_for_incoming_msg_event() # Hi!
ac1.wait_for_incoming_msg_event() # Member removed
ac1.wait_for_incoming_msg_event() # Member added
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Works again!"
# ac2 is now verified by ac3 for ac1
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
ac1_chat_messages = snapshot.chat.get_messages()
ac2_addr = ac2.get_config("addr")
assert ac1_chat_messages[-1].get_snapshot().text == f"Changed setup for {ac2_addr}"
def test_verified_group_member_added_recovery(acfactory) -> None:
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates verified group")
chat = ac1.create_group("Verified group", protect=True)
assert chat.get_basic_snapshot().is_protected
logging.info("ac2 joins verified group")
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
while True:
event = ac1.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
while True:
event = ac1.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
logging.info("ac2 reverifies with ac3")
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
while True:
event = ac3.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3
ac3_chat.send_text("Hi!")
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("Received message %s", snapshot.text)
assert snapshot.text == "Hi!"
ac1.wait_for_incoming_msg_event() # Hi!
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
ac3_chat.remove_contact(ac3_contact_ac2)
ac3_chat.add_contact(ac3_contact_ac2)
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert "removed" in snapshot.text
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert "removed" in snapshot.text
event = ac2.wait_for_incoming_msg_event()
msg_id = event.msg_id
chat_id = event.chat_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("ac2 got event message: %s", snapshot.text)
assert "added" in snapshot.text
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert "added" in snapshot.text
chat = Chat(ac2, chat_id)
chat.send_text("Works again!")
msg_id = ac3.wait_for_incoming_msg_event().msg_id
message = ac3.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.text == "Works again!"
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Works again!"
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
assert ac1_contact_ac2_snapshot.is_verified
assert ac1_contact_ac2_snapshot.verifier_id == ac1.get_contact_by_addr(ac3.get_config("addr")).id
# ac2 is now verified by ac3 for ac1
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
+1 -201
View File
@@ -1,11 +1,10 @@
import concurrent.futures
import json
import logging
import subprocess
from unittest.mock import MagicMock
import pytest
from deltachat_rpc_client import Chat, EventType, SpecialContactId, events
from deltachat_rpc_client import EventType, events
from deltachat_rpc_client.rpc import JsonRpcError
@@ -378,202 +377,3 @@ def test_provider_info(rpc) -> None:
rpc.set_config(account_id, "socks5_enabled", "1")
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info is None
def test_qr_setup_contact(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
while True:
event = alice.wait_for_event()
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
return
@pytest.mark.xfail()
def test_verified_group_recovery(acfactory, rpc) -> None:
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates verified group")
chat = ac1.create_group("Verified group", protect=True)
assert chat.get_basic_snapshot().is_protected
logging.info("ac2 joins verified group")
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
while True:
event = ac1.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
while True:
event = ac1.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
logging.info("ac2 reverifies with ac3")
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
while True:
event = ac3.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3
ac3_chat.send_text("Hi!")
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("Received message %s", snapshot.text)
assert snapshot.text == "Hi!"
# ac1 contact cannot be verified by ac2 because ac3 did not gossip ac1 key in the "Hi!" message.
ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
assert not ac1_contact.get_snapshot().is_verified
ac3_contact_id_ac1 = rpc.lookup_contact_id_by_addr(ac3.id, ac1.get_config("addr"))
ac3_chat.remove_contact(ac3_contact_id_ac1)
ac3_chat.add_contact(ac3_contact_id_ac1)
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("ac2 got event message: %s", snapshot.text)
assert "removed" in snapshot.text
event = ac2.wait_for_incoming_msg_event()
msg_id = event.msg_id
chat_id = event.chat_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("ac2 got event message: %s", snapshot.text)
assert "added" in snapshot.text
assert ac1_contact.get_snapshot().is_verified
chat = Chat(ac2, chat_id)
chat.send_text("Works again!")
msg_id = ac3.wait_for_incoming_msg_event().msg_id
message = ac3.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.text == "Works again!"
ac1.wait_for_incoming_msg_event() # Hi!
ac1.wait_for_incoming_msg_event() # Member removed
ac1.wait_for_incoming_msg_event() # Member added
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Works again!"
# ac2 is now verified by ac3 for ac1
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
def test_verified_group_member_added_recovery(acfactory) -> None:
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates verified group")
chat = ac1.create_group("Verified group", protect=True)
assert chat.get_basic_snapshot().is_protected
logging.info("ac2 joins verified group")
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
while True:
event = ac1.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
while True:
event = ac1.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
logging.info("ac2 reverifies with ac3")
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
while True:
event = ac3.wait_for_event()
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3
ac3_chat.send_text("Hi!")
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("Received message %s", snapshot.text)
assert snapshot.text == "Hi!"
ac1.wait_for_incoming_msg_event() # Hi!
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
ac3_chat.remove_contact(ac3_contact_ac2)
ac3_chat.add_contact(ac3_contact_ac2)
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert "removed" in snapshot.text
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert "removed" in snapshot.text
event = ac2.wait_for_incoming_msg_event()
msg_id = event.msg_id
chat_id = event.chat_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("ac2 got event message: %s", snapshot.text)
assert "added" in snapshot.text
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert "added" in snapshot.text
logging.info("ac2 address is %s", ac2.get_config("addr"))
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
# assert ac1_contact_ac2_snapshot.is_verified
assert ac1_contact_ac2_snapshot.verifier_id == ac1.get_contact_by_addr(ac3.get_config("addr")).id
chat = Chat(ac2, chat_id)
chat.send_text("Works again!")
msg_id = ac3.wait_for_incoming_msg_event().msg_id
message = ac3.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.text == "Works again!"
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Works again!"
# ac2 is now verified by ac3 for ac1
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
+1 -1
View File
@@ -30,4 +30,4 @@ commands =
[pytest]
timeout = 300
log_cli = true
log_level = info
log_level = debug
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.129.1"
version = "1.130.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
+1 -1
View File
@@ -56,5 +56,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.129.1"
"version": "1.130.0"
}
+7 -32
View File
@@ -23,12 +23,6 @@ from .events import FFIEventLogger, FFIEventTracker
def pytest_addoption(parser):
group = parser.getgroup("deltachat testplugin options")
group.addoption(
"--liveconfig",
action="store",
default=None,
help="a file with >=2 lines where each line contains NAME=VALUE config settings for one account",
)
group.addoption(
"--chatmail",
action="store",
@@ -58,8 +52,6 @@ def pytest_addoption(parser):
def pytest_configure(config):
cfg = config.getoption("--liveconfig")
cfg = config.getoption("--chatmail")
if not cfg:
cfg = os.getenv("CHATMAIL_DOMAIN")
@@ -133,13 +125,10 @@ def pytest_report_header(config, startdir):
),
]
cfg = config.option.liveconfig
if cfg:
if "?" in cfg:
url, token = cfg.split("?", 1)
summary.append(f"Liveconfig provider: {url}?<token omitted>")
else:
summary.append(f"Liveconfig file: {cfg}")
chatmail_opt = config.getoption("--chatmail")
if chatmail_opt:
summary.append(f"Chatmail account provider: {chatmail_opt}")
return summary
@@ -164,11 +153,7 @@ class TestProcess:
def get_liveconfig_producer(self):
"""provide live account configs, cached on a per-test-process scope
so that test functions can re-use already known live configs.
Depending on the --liveconfig option this comes from
a file with a line specifying each accounts config
or a --chatmail domain.
"""
liveconfig_opt = self.pytestconfig.getoption("--liveconfig")
chatmail_opt = self.pytestconfig.getoption("--chatmail")
if chatmail_opt:
# Use a chatmail instance.
@@ -178,7 +163,8 @@ class TestProcess:
try:
yield self._configlist[index]
except IndexError:
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
part = "".join(random.choices("2345789acdefghjkmnpqrstuvwxyz", k=6))
username = f"ci-{part}"
password = f"{username}${username}"
addr = f"{username}@{domain}"
config = {"addr": addr, "mail_pw": password}
@@ -186,20 +172,9 @@ class TestProcess:
self._configlist.append(config)
yield config
pytest.fail(f"more than {MAX_LIVE_CREATED_ACCOUNTS} live accounts requested.")
elif liveconfig_opt:
# Read a list of accounts from file.
for line in open(liveconfig_opt):
if line.strip() and not line.strip().startswith("#"):
d = {}
for part in line.split():
name, value = part.split("=")
d[name] = value
self._configlist.append(d)
yield from iter(self._configlist)
else:
pytest.skip(
"specify --liveconfig, CHATMAIL_DOMAIN or --chatmail to provide live accounts",
"specify CHATMAIL_DOMAIN or --chatmail to provide live accounts",
)
def cache_maybe_retrieve_configured_db_files(self, cache_addr, db_target_path):
+14
View File
@@ -140,6 +140,10 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert msg.is_system_message()
assert "added" in msg.text.lower()
assert any(
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")
assert msg_out.is_encrypted()
@@ -580,6 +584,7 @@ def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
assert msg_in.get_sender_contact().addr == ac1.get_config("addr")
chat2 = msg_in.chat
assert chat2.is_protected()
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
lp.sec("ac2_offl: sending message")
msg_out = chat2.send_text("hello")
@@ -647,6 +652,15 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
chat2.send_text("hi2")
lp.sec("ac2_offl: receiving message")
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 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")
assert ev.data2 == 0
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 not msg_in.is_system_message()
+2 -2
View File
@@ -367,7 +367,7 @@ def test_webxdc_download_on_demand(acfactory, data, lp):
lp.sec("ac2 sets download limit")
ac2.set_config("download_limit", "100")
assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(50000))}, "some test data")
assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(300000))}, "some test data")
ac2_update = ac2._evtracker.wait_next_incoming_message()
assert ac2_update.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
assert not msg2.get_status_updates()
@@ -1445,7 +1445,7 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
message, then processes a partially downloaded message.
- As a result, Bob does not see a reaction
"""
download_limit = 32768
download_limit = 300000
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_addr = ac1.get_config("addr")
chat = ac1.create_chat(ac2)
+1 -1
View File
@@ -1 +1 @@
2023-11-06
2023-11-10
+1 -1
View File
@@ -6,7 +6,7 @@ set -euo pipefail
export TZ=UTC
# Provider database revision.
REV=3c8f7e846c915a183dc44536fb5480d1f25d7c42
REV=18f714cf73d0bdfb8b013fa344494ab80c92b477
CORE_ROOT="$PWD"
TMP="$(mktemp -d)"
+476 -86
View File
@@ -1,13 +1,13 @@
//! # Chat module.
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use anyhow::{bail, ensure, Context as _, Result};
use anyhow::{anyhow, bail, ensure, Context as _, Result};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
@@ -21,7 +21,7 @@ use crate::constants::{
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL,
DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS,
};
use crate::contact::{self, Contact, ContactId, Origin, VerifiedStatus};
use crate::contact::{self, Contact, ContactAddress, ContactId, Origin, VerifiedStatus};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc;
use crate::download::DownloadState;
@@ -29,21 +29,21 @@ use crate::ephemeral::Timer as EphemeralTimer;
use crate::events::EventType;
use crate::html::new_html_mimepart;
use crate::location;
use crate::log::LogExt;
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::receive_imf::ReceivedMsg;
use crate::scheduler::InterruptInfo;
use crate::smtp::send_msg_to_smtp;
use crate::sql;
use crate::stock_str;
use crate::sync::{self, ChatAction, Sync::*, SyncData};
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::{
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
strip_rtlo_characters, time, IsNoneOrEmpty,
smeared_time, strip_rtlo_characters, time, IsNoneOrEmpty,
};
use crate::webxdc::WEBXDC_SUFFIX;
@@ -208,16 +208,24 @@ impl ChatId {
self == DC_CHAT_ID_ALLDONE_HINT
}
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id`
/// if it exists and is not blocked.
///
/// If it does not exist, `None` is returned.
/// If the chat does not exist or is blocked, `None` is returned.
pub async fn lookup_by_contact(
context: &Context,
contact_id: ContactId,
) -> Result<Option<Self>> {
ChatIdBlocked::lookup_by_contact(context, contact_id)
.await
.map(|lookup| lookup.map(|chat| chat.id))
let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, contact_id).await?
else {
return Ok(None);
};
let chat_id = match chat_id_blocked.blocked {
Blocked::Not | Blocked::Request => Some(chat_id_blocked.id),
Blocked::Yes => None,
};
Ok(chat_id)
}
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id`.
@@ -227,7 +235,7 @@ impl ChatId {
/// This is an internal API, if **a user action** needs to get a chat
/// [`ChatId::create_for_contact`] should be used as this also scales up the
/// [`Contact`]'s origin.
pub async fn get_for_contact(context: &Context, contact_id: ContactId) -> Result<Self> {
pub(crate) async fn get_for_contact(context: &Context, contact_id: ContactId) -> Result<Self> {
ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Not)
.await
.map(|chat| chat.id)
@@ -392,7 +400,10 @@ impl ChatId {
if sync.into() {
// NB: For a 1:1 chat this currently triggers `Contact::block()` on other devices.
chat.add_sync_item(context, ChatAction::Block).await?;
chat.sync(context, SyncAction::Block)
.await
.log_err(context)
.ok();
}
Ok(())
}
@@ -410,7 +421,10 @@ impl ChatId {
// TODO: For a 1:1 chat this currently triggers `Contact::unblock()` on other devices.
// Maybe we should unblock the contact locally too, this would also resolve discrepancy
// with `block()` which also blocks the contact.
chat.add_sync_item(context, ChatAction::Unblock).await?;
chat.sync(context, SyncAction::Unblock)
.await
.log_err(context)
.ok();
}
Ok(())
}
@@ -458,7 +472,10 @@ impl ChatId {
}
if sync.into() {
chat.add_sync_item(context, ChatAction::Accept).await?;
chat.sync(context, SyncAction::Accept)
.await
.log_err(context)
.ok();
}
Ok(())
}
@@ -560,6 +577,28 @@ impl ChatId {
}
}
/// Sets the 1:1 chat with the given address to ProtectionStatus::Protected,
/// and posts a `SystemMessage::ChatProtectionEnabled` into it.
///
/// If necessary, creates a hidden chat for this.
pub(crate) async fn set_protection_for_contact(
context: &Context,
contact_id: ContactId,
) -> Result<()> {
let chat_id = ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Yes)
.await
.with_context(|| format!("can't create chat for {}", contact_id))?;
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
smeared_time(context),
Some(contact_id),
)
.await?;
Ok(())
}
/// Archives or unarchives a chat.
pub async fn set_visibility(self, context: &Context, visibility: ChatVisibility) -> Result<()> {
self.set_visibility_ex(context, Sync, visibility).await
@@ -598,8 +637,10 @@ impl ChatId {
if sync.into() {
let chat = Chat::load_from_db(context, self).await?;
chat.add_sync_item(context, ChatAction::SetVisibility(visibility))
.await?;
chat.sync(context, SyncAction::SetVisibility(visibility))
.await
.log_err(context)
.ok();
}
Ok(())
}
@@ -701,10 +742,7 @@ impl ChatId {
context.emit_msgs_changed_without_ids();
context.set_config(Config::LastHousekeeping, None).await?;
context
.scheduler
.interrupt_inbox(InterruptInfo::new(false))
.await;
context.scheduler.interrupt_inbox().await;
if chat.is_self_talk() {
let mut msg = Message::new(Viewtype::Text);
@@ -1251,6 +1289,16 @@ impl ChatId {
Ok(())
}
/// Returns true if the chat is protected.
pub async fn is_protected(self, context: &Context) -> Result<ProtectionStatus> {
let protection_status = context
.sql
.query_get_value("SELECT protected FROM chats WHERE id=?", (self,))
.await?
.unwrap_or_default();
Ok(protection_status)
}
}
impl std::fmt::Display for ChatId {
@@ -1683,7 +1731,11 @@ impl Chat {
// send_sync_msg() is called (usually) a moment later at send_msg_to_smtp()
// when the group-creation message is actually sent though SMTP -
// this makes sure, the other devices are aware of grpid that is used in the sync-message.
context.sync_qr_code_tokens(Some(self.id)).await?;
context
.sync_qr_code_tokens(Some(self.id))
.await
.log_err(context)
.ok();
}
// reset encrypt error state eg. for forwarding
@@ -1880,8 +1932,18 @@ impl Chat {
Ok(msg.id)
}
/// Sends a `SyncAction` synchronising chat contacts to other devices.
pub(crate) async fn sync_contacts(&self, context: &Context) -> Result<()> {
let mut addrs = Vec::new();
for contact_id in get_chat_contacts(context, self.id).await? {
let contact = Contact::get_by_id(context, contact_id).await?;
addrs.push(contact.get_addr().to_string());
}
self.sync(context, SyncAction::SetContacts(addrs)).await
}
/// Returns chat id for the purpose of synchronisation across devices.
async fn get_sync_id(&self, context: &Context) -> Result<Option<sync::ChatId>> {
async fn get_sync_id(&self, context: &Context) -> Result<Option<SyncId>> {
match self.typ {
Chattype::Single => {
let mut r = None;
@@ -1893,7 +1955,7 @@ impl Chat {
return Ok(None);
}
let contact = Contact::get_by_id(context, contact_id).await?;
r = Some(sync::ChatId::ContactAddr(contact.get_addr().to_string()));
r = Some(SyncId::ContactAddr(contact.get_addr().to_string()));
}
Ok(r)
}
@@ -1901,23 +1963,28 @@ impl Chat {
if self.grpid.is_empty() {
return Ok(None);
}
Ok(Some(sync::ChatId::Grpid(self.grpid.clone())))
Ok(Some(SyncId::Grpid(self.grpid.clone())))
}
}
}
/// Adds a chat action to the list of items to synchronise to other devices.
pub(crate) async fn add_sync_item(&self, context: &Context, action: ChatAction) -> Result<()> {
/// Synchronises a chat action to other devices.
pub(crate) async fn sync(&self, context: &Context, action: SyncAction) -> Result<()> {
if let Some(id) = self.get_sync_id(context).await? {
context
.add_sync_item(SyncData::AlterChat { id, action })
.await?;
context.send_sync_msg().await?;
sync(context, id, action).await?;
}
Ok(())
}
}
async fn sync(context: &Context, id: SyncId, action: SyncAction) -> Result<()> {
context
.add_sync_item(SyncData::AlterChat { id, action })
.await?;
context.send_sync_msg().await?;
Ok(())
}
/// Whether the chat is pinned or archived.
#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize, EnumIter)]
#[repr(i8)]
@@ -2492,10 +2559,7 @@ async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
}
context
.scheduler
.interrupt_smtp(InterruptInfo::new(false))
.await;
context.scheduler.interrupt_smtp().await;
}
Ok(msg.id)
@@ -3182,26 +3246,79 @@ async fn find_unused_broadcast_list_name(context: &Context) -> Result<String> {
pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
let chat_name = find_unused_broadcast_list_name(context).await?;
let grpid = create_id();
let row_id = context
.sql
.insert(
"INSERT INTO chats
(type, name, grpid, param, created_timestamp)
VALUES(?, ?, ?, \'U=1\', ?);",
(
Chattype::Broadcast,
chat_name,
grpid,
create_smeared_timestamp(context),
),
)
.await?;
create_broadcast_list_ex(context, Sync, grpid, chat_name).await
}
pub(crate) async fn create_broadcast_list_ex(
context: &Context,
sync: sync::Sync,
grpid: String,
chat_name: String,
) -> Result<ChatId> {
let row_id = {
let chat_name = &chat_name;
let grpid = &grpid;
let trans_fn = |t: &mut rusqlite::Transaction| {
let cnt = t.execute("UPDATE chats SET name=? WHERE grpid=?", (chat_name, grpid))?;
ensure!(cnt <= 1, "{cnt} chats exist with grpid {grpid}");
if cnt == 1 {
return Ok(t.query_row(
"SELECT id FROM chats WHERE grpid=? AND type=?",
(grpid, Chattype::Broadcast),
|row| {
let id: isize = row.get(0)?;
Ok(id)
},
)?);
}
t.execute(
"INSERT INTO chats \
(type, name, grpid, param, created_timestamp) \
VALUES(?, ?, ?, \'U=1\', ?);",
(
Chattype::Broadcast,
&chat_name,
&grpid,
create_smeared_timestamp(context),
),
)?;
Ok(t.last_insert_rowid().try_into()?)
};
context.sql.transaction(trans_fn).await?
};
let chat_id = ChatId::new(u32::try_from(row_id)?);
context.emit_msgs_changed_without_ids();
if sync.into() {
let id = SyncId::Grpid(grpid);
let action = SyncAction::CreateBroadcast(chat_name);
self::sync(context, id, action).await.log_err(context).ok();
}
Ok(chat_id)
}
/// Set chat contacts in the `chats_contacts` table.
pub(crate) async fn update_chat_contacts_table(
context: &Context,
id: ChatId,
contacts: &HashSet<ContactId>,
) -> Result<()> {
context
.sql
.transaction(move |transaction| {
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
for contact_id in contacts {
transaction.execute(
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
(id, contact_id),
)?;
}
Ok(())
})
.await?;
Ok(())
}
/// Adds contacts to the `chats_contacts` table.
pub(crate) async fn add_to_chat_contacts_table(
context: &Context,
@@ -3247,12 +3364,13 @@ pub async fn add_contact_to_chat(
chat_id: ChatId,
contact_id: ContactId,
) -> Result<()> {
add_contact_to_chat_ex(context, chat_id, contact_id, false).await?;
add_contact_to_chat_ex(context, Sync, chat_id, contact_id, false).await?;
Ok(())
}
pub(crate) async fn add_contact_to_chat_ex(
context: &Context,
mut sync: sync::Sync,
chat_id: ChatId,
contact_id: ContactId,
from_handshake: bool,
@@ -3291,8 +3409,12 @@ pub(crate) async fn add_contact_to_chat_ex(
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
chat.param.remove(Param::Unpromoted);
chat.update_param(context).await?;
context.sync_qr_code_tokens(Some(chat_id)).await?;
context.send_sync_msg().await?;
let _ = context
.sync_qr_code_tokens(Some(chat_id))
.await
.log_err(context)
.is_ok()
&& context.send_sync_msg().await.log_err(context).is_ok();
}
if context.is_self_addr(contact.get_addr()).await? {
@@ -3334,8 +3456,12 @@ pub(crate) async fn add_contact_to_chat_ex(
msg.param.set(Param::Arg, contact_addr);
msg.param.set_int(Param::Arg2, from_handshake.into());
msg.id = send_msg(context, chat_id, &mut msg).await?;
sync = Nosync;
}
context.emit_event(EventType::ChatModified(chat_id));
if sync.into() {
chat.sync_contacts(context).await.log_err(context).ok();
}
Ok(true)
}
@@ -3442,8 +3568,10 @@ pub(crate) async fn set_muted_ex(
context.emit_event(EventType::ChatModified(chat_id));
if sync.into() {
let chat = Chat::load_from_db(context, chat_id).await?;
chat.add_sync_item(context, ChatAction::SetMuted(duration))
.await?;
chat.sync(context, SyncAction::SetMuted(duration))
.await
.log_err(context)
.ok();
}
Ok(())
}
@@ -3475,6 +3603,7 @@ pub async fn remove_contact_from_chat(
context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone()));
bail!("{}", err_msg);
} else {
let mut sync = Nosync;
// We do not return an error if the contact does not exist in the database.
// This allows to delete dangling references to deleted contacts
// in case of the database becoming inconsistent due to a bug.
@@ -3495,6 +3624,8 @@ pub async fn remove_contact_from_chat(
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
msg.param.set(Param::Arg, contact.get_addr());
msg.id = send_msg(context, chat_id, &mut msg).await?;
} else {
sync = Sync;
}
}
// we remove the member from the chat after constructing the
@@ -3509,6 +3640,9 @@ pub async fn remove_contact_from_chat(
// check/encryption logic.
remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
context.emit_event(EventType::ChatModified(chat_id));
if sync.into() {
chat.sync_contacts(context).await.log_err(context).ok();
}
}
} else {
bail!("Cannot remove members from non-group chats.");
@@ -3538,6 +3672,16 @@ pub(crate) async fn is_group_explicitly_left(context: &Context, grpid: &str) ->
/// Sets group or mailing list chat name.
pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -> Result<()> {
rename_ex(context, Sync, chat_id, new_name).await
}
async fn rename_ex(
context: &Context,
sync: sync::Sync,
chat_id: ChatId,
new_name: &str,
) -> Result<()> {
let sync_name = new_name;
let new_name = improve_single_line_input(new_name);
/* the function only sets the names of group chats; normal chats get their names from the contacts */
let mut success = false;
@@ -3589,7 +3733,13 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
if !success {
bail!("Failed to set name");
}
if sync.into() {
let sync_name = sync_name.to_string();
chat.sync(context, SyncAction::Rename(sync_name))
.await
.log_err(context)
.ok();
}
Ok(())
}
@@ -3725,10 +3875,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
.await?;
curr_timestamp += 1;
if create_send_msg_job(context, &mut msg).await?.is_some() {
context
.scheduler
.interrupt_smtp(InterruptInfo::new(false))
.await;
context.scheduler.interrupt_smtp().await;
}
}
created_chats.push(chat_id);
@@ -3785,10 +3932,7 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
msg_id: msg.id,
});
if create_send_msg_job(context, &mut msg).await?.is_some() {
context
.scheduler
.interrupt_smtp(InterruptInfo::new(false))
.await;
context.scheduler.interrupt_smtp().await;
}
}
Ok(())
@@ -4069,15 +4213,59 @@ pub(crate) async fn update_msg_text_and_timestamp(
Ok(())
}
/// Set chat contacts by their addresses creating the corresponding contacts if necessary.
async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String]) -> Result<()> {
let chat = Chat::load_from_db(context, id).await?;
ensure!(
chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast,
"{id} is not a group/broadcast",
);
let mut contacts = HashSet::new();
for addr in addrs {
let contact_addr = ContactAddress::new(addr)?;
let contact = Contact::add_or_lookup(context, "", contact_addr, Origin::Hidden)
.await?
.0;
contacts.insert(contact);
}
let contacts_old = HashSet::<ContactId>::from_iter(get_chat_contacts(context, id).await?);
if contacts == contacts_old {
return Ok(());
}
update_chat_contacts_table(context, id, &contacts).await?;
context.emit_event(EventType::ChatModified(id));
Ok(())
}
/// A cross-device chat id used for synchronisation.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub(crate) enum SyncId {
ContactAddr(String),
Grpid(String),
// NOTE: Ad-hoc groups lack an identifier that can be used across devices so
// block/mute/etc. actions on them are not synchronized to other devices.
}
/// An action synchronised to other devices.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub(crate) enum SyncAction {
Block,
Unblock,
Accept,
SetVisibility(ChatVisibility),
SetMuted(MuteDuration),
/// Create broadcast list with the given name.
CreateBroadcast(String),
Rename(String),
/// Set chat contacts by their addresses.
SetContacts(Vec<String>),
}
impl Context {
/// Executes [`SyncData::AlterChat`] item sent by other device.
pub(crate) async fn sync_alter_chat(
&self,
id: &sync::ChatId,
action: &ChatAction,
) -> Result<()> {
pub(crate) async fn sync_alter_chat(&self, id: &SyncId, action: &SyncAction) -> Result<()> {
let chat_id = match id {
sync::ChatId::ContactAddr(addr) => {
SyncId::ContactAddr(addr) => {
let Some(contact_id) =
Contact::lookup_id_by_addr_ex(self, addr, Origin::Unknown, None).await?
else {
@@ -4085,10 +4273,10 @@ impl Context {
return Ok(());
};
match action {
ChatAction::Block => {
SyncAction::Block => {
return contact::set_blocked(self, Nosync, contact_id, true).await
}
ChatAction::Unblock => {
SyncAction::Unblock => {
return contact::set_blocked(self, Nosync, contact_id, false).await
}
_ => (),
@@ -4099,7 +4287,11 @@ impl Context {
};
chat_id
}
sync::ChatId::Grpid(grpid) => {
SyncId::Grpid(grpid) => {
if let SyncAction::CreateBroadcast(name) = action {
create_broadcast_list_ex(self, Nosync, grpid.clone(), name.clone()).await?;
return Ok(());
}
let Some((chat_id, ..)) = get_chat_id_by_grpid(self, grpid).await? else {
warn!(self, "sync_alter_chat: No chat for grpid '{grpid}'.");
return Ok(());
@@ -4108,14 +4300,17 @@ impl Context {
}
};
match action {
ChatAction::Block => chat_id.block_ex(self, Nosync).await,
ChatAction::Unblock => chat_id.unblock_ex(self, Nosync).await,
ChatAction::Accept => chat_id.accept_ex(self, Nosync).await,
ChatAction::SetVisibility(v) => chat_id.set_visibility_ex(self, Nosync, *v).await,
ChatAction::SetMuted(duration) => set_muted_ex(self, Nosync, chat_id, *duration).await,
SyncAction::Block => chat_id.block_ex(self, Nosync).await,
SyncAction::Unblock => chat_id.unblock_ex(self, Nosync).await,
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(_) => {
Err(anyhow!("sync_alter_chat({id:?}, {action:?}): Bad request."))
}
SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,
SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
}
.ok();
Ok(())
}
}
@@ -4128,6 +4323,7 @@ mod tests {
use crate::message::delete_msgs;
use crate::receive_imf::receive_imf;
use crate::test_utils::{TestContext, TestContextManager};
use strum::IntoEnumIterator;
use tokio::fs;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -4338,7 +4534,7 @@ mod tests {
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
let added = add_contact_to_chat_ex(&t, chat_id, ContactId::SELF, false)
let added = add_contact_to_chat_ex(&t, Nosync, chat_id, ContactId::SELF, false)
.await
.unwrap();
assert_eq!(added, false);
@@ -4462,13 +4658,11 @@ mod tests {
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
// Bob receives a msg about Alice adding Claire to the group.
let bob_received_add_msg = bob.recv_msg(&alice_sent_add_msg).await;
bob.recv_msg(&alice_sent_add_msg).await;
// Test that add message is rewritten.
assert_eq!(
bob_received_add_msg.get_text(),
"Member claire@example.net added by alice@example.org."
);
bob.golden_test_chat(bob_chat_id, "chat_test_simultaneous_member_remove")
.await;
// Bob receives a msg about Alice removing him from the group.
let bob_received_remove_msg = bob.recv_msg(&alice_sent_remove_msg).await;
@@ -4690,7 +4884,7 @@ mod tests {
// adding or removing contacts from one-to-one-chats result in an error
let claire = Contact::create(&ctx, "", "claire@foo.de").await.unwrap();
let added = add_contact_to_chat_ex(&ctx, chat.id, claire, false).await;
let added = add_contact_to_chat_ex(&ctx, Nosync, chat.id, claire, false).await;
assert!(added.is_err());
assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1);
@@ -6320,6 +6514,47 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_broadcast_multidev() -> Result<()> {
let alices = [
TestContext::new_alice().await,
TestContext::new_alice().await,
];
let bob = TestContext::new_bob().await;
let a1b_contact_id = alices[1].add_or_lookup_contact(&bob).await.id;
let a0_broadcast_id = create_broadcast_list(&alices[0]).await?;
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
set_chat_name(&alices[0], a0_broadcast_id, "Broadcast list 42").await?;
let sent_msg = alices[0].send_text(a0_broadcast_id, "hi").await;
let msg = alices[1].recv_msg(&sent_msg).await;
let a1_broadcast_id = get_chat_id_by_grpid(&alices[1], &a0_broadcast_chat.grpid)
.await?
.unwrap()
.0;
assert_eq!(msg.chat_id, a1_broadcast_id);
let a1_broadcast_chat = Chat::load_from_db(&alices[1], a1_broadcast_id).await?;
assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast);
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
assert!(get_chat_contacts(&alices[1], a1_broadcast_id)
.await?
.is_empty());
add_contact_to_chat(&alices[1], a1_broadcast_id, a1b_contact_id).await?;
set_chat_name(&alices[1], a1_broadcast_id, "Broadcast list 43").await?;
let sent_msg = alices[1].send_text(a1_broadcast_id, "hi").await;
let msg = alices[0].recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, a0_broadcast_id);
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
assert_eq!(a0_broadcast_chat.get_type(), Chattype::Broadcast);
assert_eq!(a0_broadcast_chat.get_name(), "Broadcast list 42");
assert!(get_chat_contacts(&alices[0], a0_broadcast_id)
.await?
.is_empty());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_for_contact_with_blocked() -> Result<()> {
let t = TestContext::new().await;
@@ -6608,4 +6843,159 @@ mod tests {
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_alter_chat() -> Result<()> {
let alices = [
TestContext::new_alice().await,
TestContext::new_alice().await,
];
for a in &alices {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let bob = TestContext::new_bob().await;
let ba_chat = bob.create_chat(&alices[0]).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
let a0b_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
alices[1].recv_msg(&sent_msg).await;
let ab_contact_ids = [
alices[0].add_or_lookup_contact(&bob).await.id,
alices[1].add_or_lookup_contact(&bob).await.id,
];
async fn sync(alices: &[TestContext]) -> Result<()> {
let sync_msg = alices.get(0).unwrap().pop_sent_msg().await;
alices.get(1).unwrap().recv_msg(&sync_msg).await;
Ok(())
}
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Request);
a0b_chat_id.accept(&alices[0]).await?;
sync(&alices).await?;
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
a0b_chat_id.block(&alices[0]).await?;
sync(&alices).await?;
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Yes);
a0b_chat_id.unblock(&alices[0]).await?;
sync(&alices).await?;
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
// Unblocking a 1:1 chat doesn't unblock the contact currently.
Contact::unblock(&alices[0], ab_contact_ids[0]).await?;
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
Contact::block(&alices[0], ab_contact_ids[0]).await?;
sync(&alices).await?;
assert!(alices[1].add_or_lookup_contact(&bob).await.is_blocked());
Contact::unblock(&alices[0], ab_contact_ids[0]).await?;
sync(&alices).await?;
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
// Test accepting and blocking groups. This way we test:
// - Group chats synchronisation.
// - That blocking a group deletes it on other devices.
let fiona = TestContext::new_fiona().await;
let fiona_grp_chat_id = fiona
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&alices[0]])
.await;
let sent_msg = fiona.send_text(fiona_grp_chat_id, "hi").await;
let a0_grp_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
let a1_grp_chat_id = alices[1].recv_msg(&sent_msg).await.chat_id;
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
assert_eq!(a1_grp_chat.blocked, Blocked::Request);
a0_grp_chat_id.accept(&alices[0]).await?;
sync(&alices).await?;
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
assert_eq!(a1_grp_chat.blocked, Blocked::Not);
a0_grp_chat_id.block(&alices[0]).await?;
sync(&alices).await?;
assert!(Chat::load_from_db(&alices[1], a1_grp_chat_id)
.await
.is_err());
assert!(
!alices[1]
.sql
.exists("SELECT COUNT(*) FROM chats WHERE id=?", (a1_grp_chat_id,))
.await?
);
// Test syncing of chat visibility on a self-chat. This way we test:
// - Self-chat synchronisation.
// - That sync messages don't unarchive the self-chat.
let a0self_chat_id = alices[0].get_self_chat().await.id;
assert_eq!(
alices[1].get_self_chat().await.get_visibility(),
ChatVisibility::Normal
);
let mut visibilities =
ChatVisibility::iter().chain(std::iter::once(ChatVisibility::Normal));
visibilities.next();
for v in visibilities {
a0self_chat_id.set_visibility(&alices[0], v).await?;
sync(&alices).await?;
for a in &alices {
assert_eq!(a.get_self_chat().await.get_visibility(), v);
}
}
assert_eq!(
alices[1].get_chat(&bob).await.mute_duration,
MuteDuration::NotMuted
);
let mute_durations = [
MuteDuration::Forever,
MuteDuration::Until(SystemTime::now() + Duration::from_secs(42)),
MuteDuration::NotMuted,
];
for m in mute_durations {
set_muted(&alices[0], a0b_chat_id, m).await?;
sync(&alices).await?;
let m = match m {
MuteDuration::Until(time) => MuteDuration::Until(
SystemTime::UNIX_EPOCH
+ Duration::from_secs(
time.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
),
),
_ => m,
};
assert_eq!(alices[1].get_chat(&bob).await.mute_duration, m);
}
let a0_broadcast_id = create_broadcast_list(&alices[0]).await?;
sync(&alices).await?;
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
set_chat_name(&alices[0], a0_broadcast_id, "Broadcast list 42").await?;
sync(&alices).await?;
let a1_broadcast_id = get_chat_id_by_grpid(&alices[1], &a0_broadcast_chat.grpid)
.await?
.unwrap()
.0;
let a1_broadcast_chat = Chat::load_from_db(&alices[1], a1_broadcast_id).await?;
assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast);
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
assert!(get_chat_contacts(&alices[1], a1_broadcast_id)
.await?
.is_empty());
add_contact_to_chat(&alices[0], a0_broadcast_id, ab_contact_ids[0]).await?;
sync(&alices).await?;
assert_eq!(
get_chat_contacts(&alices[1], a1_broadcast_id).await?,
vec![ab_contact_ids[1]]
);
let sent_msg = alices[1].send_text(a1_broadcast_id, "hi").await;
let msg = bob.recv_msg(&sent_msg).await;
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
assert_eq!(chat.get_type(), Chattype::Mailinglist);
let msg = alices[0].recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, a0_broadcast_id);
remove_contact_from_chat(&alices[0], a0_broadcast_id, ab_contact_ids[0]).await?;
sync(&alices).await?;
assert!(get_chat_contacts(&alices[1], a1_broadcast_id)
.await?
.is_empty());
Ok(())
}
}
+36
View File
@@ -761,4 +761,40 @@ mod tests {
let summary = chats.get_summary(&t, 0, None).await.unwrap();
assert_eq!(summary.text, "foo: bar test"); // the linebreak should be removed from summary
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_broken() {
let t = TestContext::new_bob().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
.unwrap();
create_group_chat(&t, ProtectionStatus::Unprotected, "b chat")
.await
.unwrap();
create_group_chat(&t, ProtectionStatus::Unprotected, "c chat")
.await
.unwrap();
// check that the chatlist starts with the most recent message
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 3);
// obfuscated one chat
t.sql
.execute("UPDATE chats SET type=10 WHERE id=?", (chat_id1,))
.await
.unwrap();
// obfuscated chat can't be loaded
assert!(Chat::load_from_db(&t, chat_id1).await.is_err());
// chatlist loads fine
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
// only corrupted chat fails to create summary
assert!(chats.get_summary(&t, 0, None).await.is_ok());
assert!(chats.get_summary(&t, 1, None).await.is_ok());
assert!(chats.get_summary(&t, 2, None).await.is_err());
assert_eq!(chats.get_index_for_id(chat_id1).unwrap(), 2);
}
}
+1 -4
View File
@@ -31,7 +31,6 @@ use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::message::{Message, Viewtype};
use crate::oauth2::get_oauth2_addr;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::scheduler::InterruptInfo;
use crate::smtp::Smtp;
use crate::socks::Socks5Config;
use crate::stock_str;
@@ -481,9 +480,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
ctx.set_config_bool(Config::FetchedExistingMsgs, false)
.await?;
ctx.scheduler
.interrupt_inbox(InterruptInfo::new(false))
.await;
ctx.scheduler.interrupt_inbox().await;
progress!(ctx, 940);
update_device_chats_handle.await??;
+27 -7
View File
@@ -19,7 +19,7 @@ use tokio::task;
use tokio::time::{timeout, Duration};
use crate::aheader::EncryptPreference;
use crate::chat::ChatId;
use crate::chat::{ChatId, ProtectionStatus};
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
@@ -1261,9 +1261,8 @@ impl Contact {
/// in contact list items and
/// in chat member list items.
///
/// Do not use this function when displaying the contact profile view.
/// Display green checkmark in the title of the contact profile
/// if 1:1 chat with the contact exists and is protected.
/// In contact profile view, us this function only if there is no chat with the contact,
/// otherwise use is_chat_protected().
/// Use [Self::get_verifier_id] to display the verifier contact
/// in the info section of the contact profile.
pub async fn is_verified(&self, context: &Context) -> Result<VerifiedStatus> {
@@ -1317,6 +1316,27 @@ impl Contact {
}
}
/// Returns if the contact profile title should display a green checkmark.
///
/// This generally should be consistent with the 1:1 chat with the contact
/// so 1:1 chat with the contact and the contact profile
/// either both display the green checkmark or both don't display a green checkmark.
///
/// UI often knows beforehand if a chat exists and can also call
/// `chat.is_protected()` (if there is a chat)
/// or `contact.is_verified()` (if there is no chat) directly.
/// This is often easier and also skips some database calls.
pub async fn is_profile_verified(&self, context: &Context) -> Result<bool> {
let contact_id = self.id;
if let Some(chat_id) = ChatId::lookup_by_contact(context, contact_id).await? {
Ok(chat_id.is_protected(context).await? == ProtectionStatus::Protected)
} else {
// 1:1 chat does not exist.
Ok(self.is_verified(context).await? == VerifiedStatus::BidirectVerified)
}
}
/// Returns the number of real (i.e. non-special) contacts in the database.
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
if !context.sql.is_open().await {
@@ -1460,12 +1480,12 @@ WHERE type=? AND id IN (
if sync.into() {
let action = match new_blocking {
true => sync::ChatAction::Block,
false => sync::ChatAction::Unblock,
true => chat::SyncAction::Block,
false => chat::SyncAction::Unblock,
};
context
.add_sync_item(SyncData::AlterChat {
id: sync::ChatId::ContactAddr(contact.addr.clone()),
id: chat::SyncId::ContactAddr(contact.addr.clone()),
action,
})
.await?;
+2 -6
View File
@@ -23,7 +23,7 @@ use crate::key::{load_self_public_key, DcKey as _};
use crate::login_param::LoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::quota::QuotaInfo;
use crate::scheduler::{InterruptInfo, SchedulerState};
use crate::scheduler::SchedulerState;
use crate::sql::Sql;
use crate::stock_str::StockStrings;
use crate::timesmearing::SmearedTimestamp;
@@ -437,11 +437,7 @@ impl Context {
pub(crate) async fn schedule_resync(&self) -> Result<()> {
self.resync_request.store(true, Ordering::Relaxed);
self.scheduler
.interrupt_inbox(InterruptInfo {
probe_network: false,
})
.await;
self.scheduler.interrupt_inbox().await;
Ok(())
}
+6 -11
View File
@@ -12,18 +12,16 @@ use crate::context::Context;
use crate::imap::{Imap, ImapActionResult};
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, Part};
use crate::scheduler::InterruptInfo;
use crate::tools::time;
use crate::{stock_str, EventType};
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
///
/// Some messages as non-delivery-reports (NDN) or read-receipts (MDN)
/// need to be downloaded completely to handle them correctly,
/// eg. to assign them to the correct chat.
/// As these messages are typically small,
/// they're caught by `MIN_DOWNLOAD_LIMIT`.
pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 32768;
/// For better UX, some messages as add-member, non-delivery-reports (NDN) or read-receipts (MDN)
/// should always be downloaded completely to handle them correctly,
/// also in larger groups and if group and contact avatar are attached.
/// Most of these cases are caught by `MIN_DOWNLOAD_LIMIT`.
pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 163840;
/// If a message is downloaded only partially
/// and `delete_server_after` is set to small timeouts (eg. "at once"),
@@ -94,10 +92,7 @@ impl MsgId {
.sql
.execute("INSERT INTO download (msg_id) VALUES (?)", (self,))
.await?;
context
.scheduler
.interrupt_inbox(InterruptInfo::new(false))
.await;
context.scheduler.interrupt_inbox().await;
}
}
Ok(())
+30 -9
View File
@@ -67,13 +67,8 @@ impl EncryptHelper {
"peerstate for {:?} is {}", addr, peerstate.prefer_encrypt
);
match peerstate.prefer_encrypt {
EncryptPreference::NoPreference => {}
EncryptPreference::NoPreference | EncryptPreference::Reset => {}
EncryptPreference::Mutual => prefer_encrypt_count += 1,
EncryptPreference::Reset => {
if !e2ee_guaranteed {
return Ok(false);
}
}
};
}
None => {
@@ -105,16 +100,39 @@ impl EncryptHelper {
) -> Result<String> {
let mut keyring: Vec<SignedPublicKey> = Vec::new();
let mut verifier_addresses: Vec<&str> = Vec::new();
for (peerstate, addr) in peerstates
.into_iter()
.filter_map(|(state, addr)| state.map(|s| (s, addr)))
.iter()
.filter_map(|(state, addr)| state.clone().map(|s| (s, addr)))
{
let key = peerstate
.take_key(min_verified)
.with_context(|| format!("proper enc-key for {addr} missing, cannot encrypt"))?;
keyring.push(key);
verifier_addresses.push(addr);
}
// Encrypt to self.
keyring.push(self.public_key.clone());
// Encrypt to secondary verified keys
// if we also encrypt to the introducer ("verifier") of the key.
if min_verified == PeerstateVerifiedStatus::BidirectVerified {
for (peerstate, _addr) in peerstates {
if let Some(peerstate) = peerstate {
if let (Some(key), Some(verifier)) = (
peerstate.secondary_verified_key.as_ref(),
peerstate.secondary_verifier.as_deref(),
) {
if verifier_addresses.contains(&verifier) {
keyring.push(key.clone());
}
}
}
}
}
let sign_key = load_self_secret_key(context).await?;
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
@@ -301,8 +319,11 @@ Sent with my Delta Chat Messenger: https://delta.chat";
gossip_key_fingerprint: Some(pub_key.fingerprint()),
verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.fingerprint()),
fingerprint_changed: false,
verifier: None,
secondary_verified_key: None,
secondary_verified_key_fingerprint: None,
secondary_verifier: None,
fingerprint_changed: false,
};
vec![(Some(peerstate), addr)]
}
+1
View File
@@ -250,6 +250,7 @@ pub enum EventType {
/// Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
/// 1000=vg-member-added/vc-contact-confirm received
progress: usize,
},
+32 -9
View File
@@ -35,7 +35,6 @@ use crate::receive_imf::{
from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner, ReceivedMsg,
};
use crate::scheduler::connectivity::ConnectivityStore;
use crate::scheduler::InterruptInfo;
use crate::socks::Socks5Config;
use crate::sql;
use crate::stock_str;
@@ -86,7 +85,7 @@ const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
#[derive(Debug)]
pub struct Imap {
pub(crate) idle_interrupt_receiver: Receiver<InterruptInfo>,
pub(crate) idle_interrupt_receiver: Receiver<()>,
config: ImapConfig,
pub(crate) session: Option<Session>,
login_failed_once: bool,
@@ -195,7 +194,7 @@ impl<T: Iterator<Item = (i64, u32, String)>> Iterator for UidGrouper<T> {
while let Some((next_rowid, next_uid, _)) =
self.inner.next_if(|(_, next_uid, next_folder)| {
next_folder == &folder && *next_uid == end_uid + 1
next_folder == &folder && (*next_uid == end_uid + 1 || *next_uid == end_uid)
})
{
end_uid = next_uid;
@@ -228,7 +227,7 @@ impl Imap {
socks5_config: Option<Socks5Config>,
addr: &str,
provider_strict_tls: bool,
idle_interrupt_receiver: Receiver<InterruptInfo>,
idle_interrupt_receiver: Receiver<()>,
) -> Result<Self> {
if lp.server.is_empty() || lp.user.is_empty() || lp.password.is_empty() {
bail!("Incomplete IMAP connection parameters");
@@ -261,7 +260,7 @@ impl Imap {
/// Creates new disconnected IMAP client using configured parameters.
pub async fn new_configured(
context: &Context,
idle_interrupt_receiver: Receiver<InterruptInfo>,
idle_interrupt_receiver: Receiver<()>,
) -> Result<Self> {
if !context.is_configured().await? {
bail!("IMAP Connect without configured params");
@@ -2290,10 +2289,7 @@ pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str)
(message_id,),
)
.await?;
context
.scheduler
.interrupt_inbox(InterruptInfo::new(false))
.await;
context.scheduler.interrupt_inbox().await;
Ok(())
}
@@ -2841,4 +2837,31 @@ mod tests {
Ok(())
}
#[test]
fn test_uid_grouper() {
// Input: sequence of (rowid: i64, uid: u32, target: String)
// Output: sequence of (target: String, rowid_set: Vec<i64>, uid_set: String)
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string())]);
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
assert_eq!(res, vec![("INBOX".to_string(), vec![1], "2".to_string())]);
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string()), (2, 3, "INBOX".to_string())]);
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
assert_eq!(
res,
vec![("INBOX".to_string(), vec![1, 2], "2:3".to_string())]
);
let grouper = UidGrouper::from([
(1, 2, "INBOX".to_string()),
(2, 2, "INBOX".to_string()),
(3, 3, "INBOX".to_string()),
]);
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
assert_eq!(
res,
vec![("INBOX".to_string(), vec![1, 2, 3], "2:3".to_string())]
);
}
}
+34 -46
View File
@@ -8,9 +8,9 @@ use futures_lite::FutureExt;
use super::session::Session;
use super::Imap;
use crate::config::Config;
use crate::context::Context;
use crate::imap::{client::IMAP_TIMEOUT, get_uid_next, FolderMeaning};
use crate::log::LogExt;
use crate::{context::Context, scheduler::InterruptInfo};
const IDLE_TIMEOUT: Duration = Duration::from_secs(23 * 60);
@@ -18,25 +18,15 @@ impl Session {
pub async fn idle(
mut self,
context: &Context,
idle_interrupt_receiver: Receiver<InterruptInfo>,
idle_interrupt_receiver: Receiver<()>,
folder: &str,
) -> Result<(Self, InterruptInfo)> {
) -> Result<Self> {
use futures::future::FutureExt;
if context.get_config_bool(Config::DisableIdle).await? {
bail!("IMAP IDLE is disabled");
}
if !self.can_idle() {
bail!("IMAP server does not have IDLE capability");
}
let mut info = Default::default();
self.select_folder(context, Some(folder)).await?;
if self.server_sent_unsolicited_exists(context)? {
return Ok((self, info));
return Ok(self);
}
// Despite checking for unsolicited EXISTS above,
@@ -55,16 +45,16 @@ impl Session {
context,
"Skipping IDLE on {folder:?} because UIDNEXT {uid_next}>{expected_uid_next} indicates there are new messages."
);
return Ok((self, info));
return Ok(self);
}
} else {
warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT");
// Go to IDLE anyway if STATUS is broken.
}
if let Ok(info) = idle_interrupt_receiver.try_recv() {
info!(context, "skip idle, got interrupt {:?}", info);
return Ok((self, info));
if let Ok(()) = idle_interrupt_receiver.try_recv() {
info!(context, "skip idle, got interrupt");
return Ok(self);
}
let mut handle = self.inner.idle();
@@ -81,17 +71,17 @@ impl Session {
enum Event {
IdleResponse(IdleResponse),
Interrupt(InterruptInfo),
Interrupt,
}
info!(context, "{folder}: Idle entering wait-on-remote state");
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
let info = idle_interrupt_receiver.recv().await;
idle_interrupt_receiver.recv().await.ok();
// cancel imap idle connection properly
drop(interrupt);
Ok(Event::Interrupt(info.unwrap_or_default()))
Ok(Event::Interrupt)
});
match fut.await {
@@ -104,9 +94,8 @@ impl Session {
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
info!(context, "{folder}: Idle wait was interrupted manually");
}
Ok(Event::Interrupt(i)) => {
info!(context, "{folder}: Idle wait was interrupted: {:?}", &i);
info = i;
Ok(Event::Interrupt) => {
info!(context, "{folder}: Idle wait was interrupted");
}
Err(err) => {
warn!(context, "{folder}: Idle wait errored: {err:?}");
@@ -120,7 +109,7 @@ impl Session {
session.as_mut().set_read_timeout(Some(IMAP_TIMEOUT));
self.inner = session;
Ok((self, info))
Ok(self)
}
}
@@ -130,7 +119,7 @@ impl Imap {
context: &Context,
watch_folder: Option<String>,
folder_meaning: FolderMeaning,
) -> InterruptInfo {
) {
// Idle using polling. This is also needed if we're not yet configured -
// in this case, we're waiting for a configure job (and an interrupt).
@@ -141,32 +130,33 @@ impl Imap {
watch_folder
} else {
info!(context, "IMAP-fake-IDLE: no folder, waiting for interrupt");
return self
.idle_interrupt_receiver
.recv()
.await
.unwrap_or_default();
self.idle_interrupt_receiver.recv().await.ok();
return;
};
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
// check every minute if there are new messages
// TODO: grow sleep durations / make them more flexible
let mut interval = tokio::time::interval(Duration::from_secs(60));
const TIMEOUT_INIT_MS: u64 = 60_000;
let mut timeout_ms: u64 = TIMEOUT_INIT_MS;
enum Event {
Tick,
Interrupt(InterruptInfo),
Interrupt,
}
// loop until we are interrupted or if we fetched something
let info = loop {
loop {
use futures::future::FutureExt;
use rand::Rng;
let mut interval = tokio::time::interval(Duration::from_millis(timeout_ms));
timeout_ms = timeout_ms
.saturating_add(rand::thread_rng().gen_range((timeout_ms / 2)..=timeout_ms));
interval.tick().await; // The first tick completes immediately.
match interval
.tick()
.map(|_| Event::Tick)
.race(
self.idle_interrupt_receiver
.recv()
.map(|probe_network| Event::Interrupt(probe_network.unwrap_or_default())),
.map(|_| Event::Interrupt),
)
.await
{
@@ -188,7 +178,7 @@ impl Imap {
.unwrap_or_default()
{
// we only fake-idled because network was gone during IDLE, probably
break InterruptInfo::new(false);
break;
}
}
info!(context, "fake_idle is connected");
@@ -202,8 +192,9 @@ impl Imap {
{
Ok(res) => {
info!(context, "fetch_new_messages returned {:?}", res);
timeout_ms = TIMEOUT_INIT_MS;
if res {
break InterruptInfo::new(false);
break;
}
}
Err(err) => {
@@ -212,13 +203,12 @@ impl Imap {
}
}
}
Event::Interrupt(info) => {
// Interrupt
Event::Interrupt => {
info!(context, "Fake IDLE interrupted");
break info;
break;
}
}
};
}
info!(
context,
@@ -229,7 +219,5 @@ impl Imap {
.as_millis() as f64
/ 1000.,
);
info
}
}
+2 -9
View File
@@ -24,7 +24,6 @@ use crate::mimeparser::{parse_message_id, SystemMessage};
use crate::param::{Param, Params};
use crate::pgp::split_armored_data;
use crate::reaction::get_msg_reactions;
use crate::scheduler::InterruptInfo;
use crate::sql;
use crate::summary::Summary;
use crate::tools::{
@@ -1527,10 +1526,7 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
}
// Interrupt Inbox loop to start message deletion and run housekeeping.
context
.scheduler
.interrupt_inbox(InterruptInfo::new(false))
.await;
context.scheduler.interrupt_inbox().await;
Ok(())
}
@@ -1648,10 +1644,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
)
.await
.context("failed to insert into smtp_mdns")?;
context
.scheduler
.interrupt_smtp(InterruptInfo::new(false))
.await;
context.scheduler.interrupt_smtp().await;
}
}
updated_chat_ids.insert(curr_chat_id);
+14 -7
View File
@@ -14,7 +14,7 @@ use crate::chat::Chat;
use crate::config::Config;
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
use crate::contact::Contact;
use crate::context::{get_version_str, Context};
use crate::context::Context;
use crate::e2ee::EncryptHelper;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::html::new_html_mimepart;
@@ -316,7 +316,15 @@ impl<'a> MimeFactory<'a> {
match &self.loaded {
Loaded::Message { chat } => {
if chat.is_protected() {
PeerstateVerifiedStatus::BidirectVerified
if self.msg.get_info_type() == SystemMessage::SecurejoinMessage {
// Securejoin messages are supposed to verify a key.
// In order to do this, it is necessary that they can be sent
// to a key that is not yet verified.
// This has to work independently of whether the chat is protected right now.
PeerstateVerifiedStatus::Unverified
} else {
PeerstateVerifiedStatus::BidirectVerified
}
} else {
PeerstateVerifiedStatus::Unverified
}
@@ -596,9 +604,10 @@ impl<'a> MimeFactory<'a> {
if let Loaded::Message { chat } = &self.loaded {
if chat.typ == Chattype::Broadcast {
let encoded_chat_name = encode_words(&chat.name);
headers.protected.push(Header::new(
"List-ID".into(),
format!("{} <{}>", chat.name, chat.grpid),
format!("{encoded_chat_name} <{}>", chat.grpid),
));
}
}
@@ -1357,14 +1366,12 @@ impl<'a> MimeFactory<'a> {
);
// second body part: machine-readable, always REQUIRED by RFC 6522
let version = get_version_str();
let message_text2 = format!(
"Reporting-UA: Delta Chat {}\r\n\
Original-Recipient: rfc822;{}\r\n\
"Original-Recipient: rfc822;{}\r\n\
Final-Recipient: rfc822;{}\r\n\
Original-Message-ID: <{}>\r\n\
Disposition: manual-action/MDN-sent-automatically; displayed\r\n",
version, self.from_addr, self.from_addr, self.msg.rfc724_mid
self.from_addr, self.from_addr, self.msg.rfc724_mid
);
let extension_fields = if additional_msg_ids.is_empty() {
+2 -2
View File
@@ -163,10 +163,10 @@ pub enum SystemMessage {
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged = 10,
/// Chat protection is enabled.
/// "Messages are guaranteed to be end-to-end encrypted from now on."
ChatProtectionEnabled = 11,
/// Chat protection is disabled.
/// "%1$s sent a message from another device."
ChatProtectionDisabled = 12,
/// Self-sent-message that contains only json used for multi-device-sync;
+97 -38
View File
@@ -1,7 +1,5 @@
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module.
use std::collections::HashSet;
use anyhow::{Context as _, Error, Result};
use num_traits::FromPrimitive;
@@ -40,7 +38,7 @@ pub enum PeerstateVerifiedStatus {
}
/// Peerstate represents the state of an Autocrypt peer.
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Peerstate {
/// E-mail address of the contact.
pub addr: String,
@@ -82,13 +80,24 @@ pub struct Peerstate {
/// Fingerprint of the verified public key.
pub verified_key_fingerprint: Option<Fingerprint>,
/// The address that introduced this verified key.
pub verifier: Option<String>,
/// Secondary public verified key of the contact.
/// It could be a contact gossiped by another verified contact in a shared group
/// or a key that was previously used as a verified key.
pub secondary_verified_key: Option<SignedPublicKey>,
/// Fingerprint of the secondary verified public key.
pub secondary_verified_key_fingerprint: Option<Fingerprint>,
/// The address that introduced secondary verified key.
pub secondary_verifier: Option<String>,
/// True if it was detected
/// that the fingerprint of the key used in chats with
/// opportunistic encryption was changed after Peerstate creation.
pub fingerprint_changed: bool,
/// The address that verified this contact
pub verifier: Option<String>,
}
impl Peerstate {
@@ -106,8 +115,11 @@ impl Peerstate {
gossip_timestamp: 0,
verified_key: None,
verified_key_fingerprint: None,
fingerprint_changed: false,
verifier: None,
secondary_verified_key: None,
secondary_verified_key_fingerprint: None,
secondary_verifier: None,
fingerprint_changed: false,
}
}
@@ -132,8 +144,11 @@ impl Peerstate {
gossip_timestamp: message_time,
verified_key: None,
verified_key_fingerprint: None,
fingerprint_changed: false,
verifier: None,
secondary_verified_key: None,
secondary_verified_key_fingerprint: None,
secondary_verifier: None,
fingerprint_changed: false,
}
}
@@ -141,7 +156,10 @@ impl Peerstate {
pub async fn from_addr(context: &Context, addr: &str) -> Result<Option<Peerstate>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint, verifier \
verified_key, verified_key_fingerprint, \
verifier, \
secondary_verified_key, secondary_verified_key_fingerprint, \
secondary_verifier \
FROM acpeerstates \
WHERE addr=? COLLATE NOCASE LIMIT 1;";
Self::from_stmt(context, query, (addr,)).await
@@ -154,7 +172,10 @@ impl Peerstate {
) -> Result<Option<Peerstate>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint, verifier \
verified_key, verified_key_fingerprint, \
verifier, \
secondary_verified_key, secondary_verified_key_fingerprint, \
secondary_verifier \
FROM acpeerstates \
WHERE public_key_fingerprint=? \
OR gossip_key_fingerprint=? \
@@ -174,7 +195,10 @@ impl Peerstate {
) -> Result<Option<Peerstate>> {
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
verified_key, verified_key_fingerprint, verifier \
verified_key, verified_key_fingerprint, \
verifier, \
secondary_verified_key, secondary_verified_key_fingerprint, \
secondary_verifier \
FROM acpeerstates \
WHERE verified_key_fingerprint=? \
OR addr=? COLLATE NOCASE \
@@ -191,11 +215,6 @@ impl Peerstate {
let peerstate = context
.sql
.query_row_optional(query, params, |row| {
// all the above queries start with this: SELECT
// addr, last_seen, last_seen_autocrypt, prefer_encrypted,
// public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
// gossip_key_fingerprint, verified_key, verified_key_fingerprint
let res = Peerstate {
addr: row.get("addr")?,
last_seen: row.get("last_seen")?,
@@ -230,11 +249,24 @@ impl Peerstate {
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
fingerprint_changed: false,
verifier: {
let verifier: Option<String> = row.get("verifier")?;
verifier.filter(|verifier| !verifier.is_empty())
verifier.filter(|s| !s.is_empty())
},
secondary_verified_key: row
.get("secondary_verified_key")
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
secondary_verified_key_fingerprint: row
.get::<_, Option<String>>("secondary_verified_key_fingerprint")?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
secondary_verifier: {
let secondary_verifier: Option<String> = row.get("secondary_verifier")?;
secondary_verifier.filter(|s| !s.is_empty())
},
fingerprint_changed: false,
};
Ok(res)
@@ -421,8 +453,8 @@ impl Peerstate {
/// Make sure to call `self.save_to_db` to save these changes
/// Params:
/// verifier:
/// The address which verifies the given contact
/// If we are verifying the contact, use that contacts address
/// The address which introduces the given contact.
/// If we are verifying the contact, use that contacts address.
pub fn set_verified(
&mut self,
which_key: PeerstateKeyType,
@@ -461,6 +493,19 @@ impl Peerstate {
}
}
/// Sets current gossiped key as the secondary verified key.
///
/// If gossiped key is the same as the current verified key,
/// do nothing to avoid overwriting secondary verified key
/// which may be different.
pub fn set_secondary_verified_key_from_gossip(&mut self, verifier: String) {
if self.gossip_key_fingerprint != self.verified_key_fingerprint {
self.secondary_verified_key = self.gossip_key.clone();
self.secondary_verified_key_fingerprint = self.gossip_key_fingerprint.clone();
self.secondary_verifier = Some(verifier);
}
}
/// Saves the peerstate to the database.
pub async fn save_to_db(&self, sql: &Sql) -> Result<()> {
sql.execute(
@@ -475,9 +520,12 @@ impl Peerstate {
gossip_key_fingerprint,
verified_key,
verified_key_fingerprint,
addr,
verifier)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
verifier,
secondary_verified_key,
secondary_verified_key_fingerprint,
secondary_verifier,
addr)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT (addr)
DO UPDATE SET
last_seen = excluded.last_seen,
@@ -490,7 +538,10 @@ impl Peerstate {
gossip_key_fingerprint = excluded.gossip_key_fingerprint,
verified_key = excluded.verified_key,
verified_key_fingerprint = excluded.verified_key_fingerprint,
verifier = excluded.verifier",
verifier = excluded.verifier,
secondary_verified_key = excluded.secondary_verified_key,
secondary_verified_key_fingerprint = excluded.secondary_verified_key_fingerprint,
secondary_verifier = excluded.secondary_verifier",
(
self.last_seen,
self.last_seen_autocrypt,
@@ -502,23 +553,19 @@ impl Peerstate {
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
self.verified_key.as_ref().map(|k| k.to_bytes()),
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
&self.addr,
self.verifier.as_deref().unwrap_or(""),
self.secondary_verified_key.as_ref().map(|k| k.to_bytes()),
self.secondary_verified_key_fingerprint
.as_ref()
.map(|fp| fp.hex()),
self.secondary_verifier.as_deref().unwrap_or(""),
&self.addr,
),
)
.await?;
Ok(())
}
/// Returns true if verified key is contained in the given set of fingerprints.
pub fn has_verified_key(&self, fingerprints: &HashSet<Fingerprint>) -> bool {
if let Some(vkc) = &self.verified_key_fingerprint {
fingerprints.contains(vkc) && self.verified_key.is_some()
} else {
false
}
}
/// Returns the address that verified the contact
pub fn get_verifier(&self) -> Option<&str> {
self.verifier.as_deref()
@@ -769,8 +816,11 @@ mod tests {
gossip_key_fingerprint: Some(pub_key.fingerprint()),
verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.fingerprint()),
fingerprint_changed: false,
verifier: None,
secondary_verified_key: None,
secondary_verified_key_fingerprint: None,
secondary_verifier: None,
fingerprint_changed: false,
};
assert!(
@@ -809,8 +859,11 @@ mod tests {
gossip_key_fingerprint: None,
verified_key: None,
verified_key_fingerprint: None,
fingerprint_changed: false,
verifier: None,
secondary_verified_key: None,
secondary_verified_key_fingerprint: None,
secondary_verifier: None,
fingerprint_changed: false,
};
assert!(
@@ -842,8 +895,11 @@ mod tests {
gossip_key_fingerprint: None,
verified_key: None,
verified_key_fingerprint: None,
fingerprint_changed: false,
verifier: None,
secondary_verified_key: None,
secondary_verified_key_fingerprint: None,
secondary_verifier: None,
fingerprint_changed: false,
};
assert!(
@@ -905,8 +961,11 @@ mod tests {
gossip_key_fingerprint: None,
verified_key: None,
verified_key_fingerprint: None,
fingerprint_changed: false,
verifier: None,
secondary_verified_key: None,
secondary_verified_key_fingerprint: None,
secondary_verifier: None,
fingerprint_changed: false,
};
peerstate.apply_header(&header, 100);
+133 -1
View File
@@ -222,6 +222,99 @@ static P_BUZON_UY: Provider = Provider {
oauth2_authorizer: None,
};
// c1.testrun.org.md: c1.testrun.org
static P_C1_TESTRUN_ORG: Provider = Provider {
id: "c1.testrun.org",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/c1-testrun-org",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "c1.testrun.org",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "c1.testrun.org",
port: 465,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
config_defaults: Some(&[ConfigDefault {
key: Config::MvboxMove,
value: "0",
}]),
oauth2_authorizer: None,
};
// c2.testrun.org.md: c2.testrun.org
static P_C2_TESTRUN_ORG: Provider = Provider {
id: "c2.testrun.org",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/c2-testrun-org",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "c2.testrun.org",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "c2.testrun.org",
port: 465,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
config_defaults: Some(&[ConfigDefault {
key: Config::MvboxMove,
value: "0",
}]),
oauth2_authorizer: None,
};
// c3.testrun.org.md: c3.testrun.org
static P_C3_TESTRUN_ORG: Provider = Provider {
id: "c3.testrun.org",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/c3-testrun-org",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "c3.testrun.org",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "c3.testrun.org",
port: 465,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
config_defaults: Some(&[ConfigDefault {
key: Config::MvboxMove,
value: "0",
}]),
oauth2_authorizer: None,
};
// chello.at.md: chello.at
static P_CHELLO_AT: Provider = Provider {
id: "chello.at",
@@ -867,6 +960,37 @@ static P_NAVER: Provider = Provider {
oauth2_authorizer: None,
};
// nine.testrun.org.md: nine.testrun.org
static P_NINE_TESTRUN_ORG: Provider = Provider {
id: "nine.testrun.org",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/nine-testrun-org",
server: &[
Server {
protocol: Imap,
socket: Ssl,
hostname: "nine.testrun.org",
port: 993,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "nine.testrun.org",
port: 465,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
config_defaults: Some(&[ConfigDefault {
key: Config::MvboxMove,
value: "0",
}]),
oauth2_authorizer: None,
};
// nubo.coop.md: nubo.coop
static P_NUBO_COOP: Provider = Provider {
id: "nubo.coop",
@@ -1495,6 +1619,9 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("delta.blindzeln.org", &P_BLINDZELN_ORG),
("bluewin.ch", &P_BLUEWIN_CH),
("buzon.uy", &P_BUZON_UY),
("c1.testrun.org", &P_C1_TESTRUN_ORG),
("c2.testrun.org", &P_C2_TESTRUN_ORG),
("c3.testrun.org", &P_C3_TESTRUN_ORG),
("chello.at", &P_CHELLO_AT),
("xfinity.com", &P_COMCAST),
("comcast.net", &P_COMCAST),
@@ -1708,6 +1835,7 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
("mailo.com", &P_MAILO_COM),
("nauta.cu", &P_NAUTA_CU),
("naver.com", &P_NAVER),
("nine.testrun.org", &P_NINE_TESTRUN_ORG),
("nubo.coop", &P_NUBO_COOP),
("hotmail.com", &P_OUTLOOK_COM),
("outlook.com", &P_OUTLOOK_COM),
@@ -1860,6 +1988,9 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
("blindzeln.org", &P_BLINDZELN_ORG),
("bluewin.ch", &P_BLUEWIN_CH),
("buzon.uy", &P_BUZON_UY),
("c1.testrun.org", &P_C1_TESTRUN_ORG),
("c2.testrun.org", &P_C2_TESTRUN_ORG),
("c3.testrun.org", &P_C3_TESTRUN_ORG),
("chello.at", &P_CHELLO_AT),
("comcast", &P_COMCAST),
("dismail.de", &P_DISMAIL_DE),
@@ -1888,6 +2019,7 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
("mailo.com", &P_MAILO_COM),
("nauta.cu", &P_NAUTA_CU),
("naver", &P_NAVER),
("nine.testrun.org", &P_NINE_TESTRUN_ORG),
("nubo.coop", &P_NUBO_COOP),
("outlook.com", &P_OUTLOOK_COM),
("ouvaton.coop", &P_OUVATON_COOP),
@@ -1918,4 +2050,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
});
pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2023, 2, 20).unwrap());
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2023, 11, 5).unwrap());
+4 -1
View File
@@ -1052,8 +1052,11 @@ mod tests {
gossip_key_fingerprint: None,
verified_key: None,
verified_key_fingerprint: None,
fingerprint_changed: false,
verifier: None,
secondary_verified_key: None,
secondary_verified_key_fingerprint: None,
secondary_verifier: None,
fingerprint_changed: false,
};
assert!(
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
+197 -71
View File
@@ -227,6 +227,9 @@ pub(crate) async fn receive_imf_inner(
.and_then(|value| mailparse::dateparse(value).ok())
.map_or(rcvd_timestamp, |value| min(value, rcvd_timestamp + 60));
let updated_verified_key_addr =
update_verified_keys(context, &mut mime_parser, from_id).await?;
// Add parts
let received_msg = add_parts(
context,
@@ -278,14 +281,17 @@ pub(crate) async fn receive_imf_inner(
MsgId::new_unset()
};
if let Some(addr) = updated_verified_key_addr {
let msg = stock_str::contact_setup_changed(context, &addr).await;
chat::add_info_msg(context, chat_id, &msg, received_msg.sort_timestamp).await?;
}
save_locations(context, &mime_parser, chat_id, from_id, insert_msg_id).await?;
if let Some(ref sync_items) = mime_parser.sync_items {
if from_id == ContactId::SELF {
if mime_parser.was_encrypted() {
if let Err(err) = context.execute_sync_items(sync_items).await {
warn!(context, "receive_imf cannot execute sync items: {err:#}.");
}
context.execute_sync_items(sync_items).await;
} else {
warn!(context, "Sync items are not encrypted.");
}
@@ -457,6 +463,7 @@ async fn add_parts(
let mut chat_id_blocked = Blocked::Not;
let mut better_msg = None;
let mut group_changes_msgs = Vec::new();
if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled {
better_msg = Some(stock_str::msg_location_enabled_by(context, from_id).await);
}
@@ -649,7 +656,7 @@ async fn add_parts(
}
}
better_msg = better_msg.or(apply_group_changes(
group_changes_msgs = apply_group_changes(
context,
mime_parser,
sent_timestamp,
@@ -658,7 +665,7 @@ async fn add_parts(
to_ids,
is_partial_download.is_some(),
)
.await?);
.await?;
}
if chat_id.is_none() {
@@ -892,7 +899,7 @@ async fn add_parts(
}
if let Some(chat_id) = chat_id {
better_msg = better_msg.or(apply_group_changes(
group_changes_msgs = apply_group_changes(
context,
mime_parser,
sent_timestamp,
@@ -901,7 +908,7 @@ async fn add_parts(
to_ids,
is_partial_download.is_some(),
)
.await?);
.await?;
}
if chat_id.is_none() && self_sent {
@@ -923,6 +930,22 @@ async fn add_parts(
}
}
}
if chat_id.is_none() {
// Check if the message belongs to a broadcast list.
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
let listid = mailinglist_header_listid(mailinglist_header)?;
chat_id = Some(
if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? {
id
} else {
let name =
compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
chat::create_broadcast_list_ex(context, Nosync, listid, name).await?
},
);
}
}
}
if fetching_existing_messages && mime_parser.decrypting_failed {
@@ -1115,7 +1138,9 @@ async fn add_parts(
let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
for part in &mut mime_parser.parts {
group_changes_msgs = group_changes_msgs.into_iter().rev().collect();
let mut parts = mime_parser.parts.iter_mut().peekable();
while let Some(part) = parts.peek() {
if part.is_reaction {
let reaction_str = simplify::remove_footers(part.msg.as_str());
set_msg_reaction(
@@ -1148,7 +1173,10 @@ async fn add_parts(
}
let mut txt_raw = "".to_string();
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
let group_changes_msg = group_changes_msgs.pop();
let (msg, typ): (&str, Viewtype) = if let Some(msg) = &group_changes_msg {
(msg, Viewtype::Text)
} else if let Some(better_msg) = &better_msg {
(better_msg, Viewtype::Text)
} else {
(&part.msg, part.typ)
@@ -1276,6 +1304,10 @@ RETURNING id
debug_assert!(!row_id.is_special());
created_db_entries.push(row_id);
if group_changes_msg.is_none() || (group_changes_msgs.is_empty() && better_msg.is_none()) {
parts.next();
}
}
// check all parts whether they contain a new logging webxdc
@@ -1660,17 +1692,6 @@ async fn create_or_lookup_group(
members.dedup();
chat::add_to_chat_contacts_table(context, new_chat_id, &members).await?;
// once, we have protected-chats explained in UI, we can uncomment the following lines.
// ("verified groups" did not add a message anyway)
//
//if create_protected == ProtectionStatus::Protected {
// set from_id=0 as it is not clear that the sender of this random group message
// actually really has enabled chat-protection at some point.
//new_chat_id
// .add_protection_msg(context, ProtectionStatus::Protected, false, 0)
// .await?;
//}
context.emit_event(EventType::ChatModified(new_chat_id));
}
@@ -1704,15 +1725,19 @@ async fn apply_group_changes(
from_id: ContactId,
to_ids: &[ContactId],
is_partial_download: bool,
) -> Result<Option<String>> {
) -> Result<Vec<String>> {
if chat_id.is_special() {
// Do not apply group changes to the trash chat.
return Ok(Vec::new());
}
let mut chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ != Chattype::Group {
return Ok(None);
return Ok(Vec::new());
}
let mut send_event_chat_modified = false;
let (mut removed_id, mut added_id) = (None, None);
let mut better_msg = None;
let mut group_changes_msgs = Vec::new();
// True if a Delta Chat client has explicitly added our current primary address.
let self_added =
@@ -1769,7 +1794,12 @@ async fn apply_group_changes(
if !chat.is_protected() {
chat_id
.inner_set_protection(context, ProtectionStatus::Protected)
.set_protection(
context,
ProtectionStatus::Protected,
smeared_time(context),
Some(from_id),
)
.await?;
}
}
@@ -1777,11 +1807,11 @@ async fn apply_group_changes(
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
better_msg = if removed_id == Some(from_id) {
Some(stock_str::msg_group_left_local(context, from_id).await)
group_changes_msgs.push(if removed_id == Some(from_id) {
stock_str::msg_group_left_local(context, from_id).await
} else {
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
};
stock_str::msg_del_member_local(context, removed_addr, from_id).await
});
if removed_id.is_some() {
if !allow_member_list_changes {
@@ -1794,7 +1824,8 @@ async fn apply_group_changes(
warn!(context, "Removed {removed_addr:?} has no contact id.")
}
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
group_changes_msgs
.push(stock_str::msg_add_member_local(context, added_addr, from_id).await);
if allow_member_list_changes {
if !recreate_member_list {
@@ -1835,21 +1866,20 @@ async fn apply_group_changes(
send_event_chat_modified = true;
}
better_msg = Some(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
group_changes_msgs
.push(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
}
} else if let Some(value) = mime_parser.get_header(HeaderDef::ChatContent) {
if value == "group-avatar-changed" {
if let Some(avatar_action) = &mime_parser.group_avatar {
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
better_msg = match avatar_action {
AvatarAction::Delete => {
Some(stock_str::msg_grp_img_deleted(context, from_id).await)
}
group_changes_msgs.push(match avatar_action {
AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
AvatarAction::Change(_) => {
Some(stock_str::msg_grp_img_changed(context, from_id).await)
stock_str::msg_grp_img_changed(context, from_id).await
}
};
});
}
}
}
@@ -1883,6 +1913,18 @@ async fn apply_group_changes(
if !diff.is_empty() {
warn!(context, "Implicit addition of {diff:?} to chat {chat_id}.");
}
group_changes_msgs.reserve(diff.len());
for contact_id in diff {
let contact = Contact::get_by_id(context, contact_id).await?;
group_changes_msgs.push(
stock_str::msg_add_member_local(
context,
contact.get_addr(),
ContactId::UNDEFINED,
)
.await,
);
}
}
if let Some(removed_id) = removed_id {
new_members.remove(&removed_id);
@@ -1895,21 +1937,7 @@ async fn apply_group_changes(
}
if new_members != chat_contacts {
let new_members_ref = &new_members;
context
.sql
.transaction(move |transaction| {
transaction
.execute("DELETE FROM chats_contacts WHERE chat_id=?", (chat_id,))?;
for contact_id in new_members_ref {
transaction.execute(
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
(chat_id, contact_id),
)?;
}
Ok(())
})
.await?;
chat::update_chat_contacts_table(context, chat_id, &new_members).await?;
chat_contacts = new_members;
send_event_chat_modified = true;
}
@@ -1949,11 +1977,22 @@ async fn apply_group_changes(
if send_event_chat_modified {
context.emit_event(EventType::ChatModified(chat_id));
}
Ok(better_msg)
Ok(group_changes_msgs)
}
static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
Ok(match LIST_ID_REGEX.captures(list_id_header) {
Some(cap) => cap.get(2).context("no match??")?.as_str().trim(),
None => list_id_header
.trim()
.trim_start_matches('<')
.trim_end_matches('>'),
}
.to_string())
}
/// Create or lookup a mailing list chat.
///
/// `list_id_header` contains the Id that must be used for the mailing list
@@ -1963,21 +2002,13 @@ static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unw
///
/// `mime_parser` is the corresponding message
/// and is used to figure out the mailing list name from different header fields.
#[allow(clippy::indexing_slicing)]
async fn create_or_lookup_mailinglist(
context: &Context,
allow_creation: bool,
list_id_header: &str,
mime_parser: &MimeMessage,
) -> Result<Option<(ChatId, Blocked)>> {
let listid = match LIST_ID_REGEX.captures(list_id_header) {
Some(cap) => cap[2].trim().to_string(),
None => list_id_header
.trim()
.trim_start_matches('<')
.trim_end_matches('>')
.to_string(),
};
let listid = mailinglist_header_listid(list_id_header)?;
if let Some((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
return Ok(Some((chat_id, blocked)));
@@ -2249,10 +2280,74 @@ enum VerifiedEncryption {
NotVerified(String), // The string contains the reason why it's not verified
}
/// Moves secondary verified key to primary verified key
/// if the message is signed with a secondary verified key.
/// Removes secondary verified key if the message is signed with primary key.
///
/// Returns address of the peerstate if the primary verified key was updated,
/// the caller then needs to add "Setup changed" notification somewhere.
async fn update_verified_keys(
context: &Context,
mimeparser: &mut MimeMessage,
from_id: ContactId,
) -> Result<Option<String>> {
if from_id == ContactId::SELF {
return Ok(None);
}
if !mimeparser.was_encrypted() {
return Ok(None);
}
let Some(peerstate) = &mut mimeparser.decryption_info.peerstate else {
// No peerstate means no verified keys.
return Ok(None);
};
let signed_with_primary_verified_key = peerstate
.verified_key_fingerprint
.as_ref()
.filter(|fp| mimeparser.signatures.contains(fp))
.is_some();
let signed_with_secondary_verified_key = peerstate
.secondary_verified_key_fingerprint
.as_ref()
.filter(|fp| mimeparser.signatures.contains(fp))
.is_some();
if signed_with_primary_verified_key {
// Remove secondary key if it exists.
if peerstate.secondary_verified_key.is_some()
|| peerstate.secondary_verified_key_fingerprint.is_some()
|| peerstate.secondary_verifier.is_some()
{
peerstate.secondary_verified_key = None;
peerstate.secondary_verified_key_fingerprint = None;
peerstate.secondary_verifier = None;
peerstate.save_to_db(&context.sql).await?;
}
// No need to notify about secondary key removal.
Ok(None)
} else if signed_with_secondary_verified_key {
peerstate.verified_key = peerstate.secondary_verified_key.take();
peerstate.verified_key_fingerprint = peerstate.secondary_verified_key_fingerprint.take();
peerstate.verifier = peerstate.secondary_verifier.take();
peerstate.save_to_db(&context.sql).await?;
// Primary verified key changed.
Ok(Some(peerstate.addr.clone()))
} else {
Ok(None)
}
}
/// Checks whether the message is allowed to appear in a protected chat.
///
/// This means that it is encrypted, signed with a verified key,
/// and if it's a group, all the recipients are verified.
///
/// Also propagates gossiped keys to verified if needed.
async fn has_verified_encryption(
context: &Context,
mimeparser: &MimeMessage,
@@ -2289,7 +2384,13 @@ async fn has_verified_encryption(
));
};
if !peerstate.has_verified_key(&mimeparser.signatures) {
let signed_with_verified_key = peerstate
.verified_key_fingerprint
.as_ref()
.filter(|fp| mimeparser.signatures.contains(fp))
.is_some();
if !signed_with_verified_key {
return Ok(NotVerified(
"The message was sent with non-verified encryption".to_string(),
));
@@ -2307,6 +2408,16 @@ async fn has_verified_encryption(
return Ok(Verified);
}
mark_recipients_as_verified(context, from_id, to_ids, mimeparser).await?;
Ok(Verified)
}
async fn mark_recipients_as_verified(
context: &Context,
from_id: ContactId,
to_ids: Vec<ContactId>,
mimeparser: &MimeMessage,
) -> Result<(), anyhow::Error> {
let rows = context
.sql
.query_map(
@@ -2335,28 +2446,43 @@ async fn has_verified_encryption(
if mimeparser.gossiped_addr.contains(&to_addr.to_lowercase()) {
if let Some(mut peerstate) = Peerstate::from_addr(context, &to_addr).await? {
// If we're here, we know the gossip key is verified.
// Use the gossip-key as verified-key if there is no verified-key
// or a member is reintroduced to a verified group.
//
// Use the gossip-key as verified-key if there is no verified-key.
//
// Store gossip key as secondary verified key if there is a verified key and
// gossiped key is different.
//
// See <https://github.com/nextleap-project/countermitm/issues/46>
// and <https://github.com/deltachat/deltachat-core-rust/issues/4541> for discussion.
let verifier_addr = contact.get_addr().to_owned();
if !is_verified
|| mimeparser
.get_header(HeaderDef::ChatGroupMemberAdded)
.filter(|s| s.as_str() == to_addr)
.is_some()
{
if !is_verified {
info!(context, "{verifier_addr} has verified {to_addr}.");
if let Some(fp) = peerstate.gossip_key_fingerprint.clone() {
peerstate.set_verified(PeerstateKeyType::GossipKey, fp, verifier_addr)?;
peerstate.save_to_db(&context.sql).await?;
if !is_verified {
let (to_contact_id, _) = Contact::add_or_lookup(
context,
"",
ContactAddress::new(&to_addr)?,
Origin::Hidden,
)
.await?;
ChatId::set_protection_for_contact(context, to_contact_id).await?;
}
}
} else {
// The contact already has a verified key.
// Store gossiped key as the secondary verified key.
peerstate.set_secondary_verified_key_from_gossip(verifier_addr);
peerstate.save_to_db(&context.sql).await?;
}
}
}
}
Ok(Verified)
Ok(())
}
/// Returns the last message referenced from `References` header if it is in the database.
+33 -2
View File
@@ -3686,6 +3686,34 @@ async fn test_mua_can_readd() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_member_left_does_not_create_chat() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
add_contact_to_chat(
&alice,
alice_chat_id,
Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?,
)
.await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
alice.pop_sent_msg().await;
// Bob only received a message of Alice leaving the group.
// This should not create the group.
//
// The reason is to avoid recreating deleted chats,
// especially the chats that were created due to "split group" bugs
// which some members simply deleted and some members left,
// recreating the chat for others.
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
assert!(bob_chat_id.is_trash());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -3699,11 +3727,14 @@ async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
.await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
alice.pop_sent_msg().await;
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
send_text_msg(&alice, alice_chat_id, "second message".to_string()).await?;
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
assert!(!bob_chat_id.is_special());
// Bob missed the message adding them, but must recreate the member list.
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 1);
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
Ok(())
}
+92 -98
View File
@@ -1,3 +1,4 @@
use std::cmp;
use std::iter::{self, once};
use std::num::NonZeroUsize;
use std::sync::atomic::Ordering;
@@ -6,6 +7,7 @@ use anyhow::{bail, Context as _, Error, Result};
use async_channel::{self as channel, Receiver, Sender};
use futures::future::try_join_all;
use futures_lite::FutureExt;
use rand::Rng;
use tokio::sync::{oneshot, RwLock, RwLockWriteGuard};
use tokio::task;
@@ -233,17 +235,17 @@ impl SchedulerState {
connectivity::maybe_network_lost(context, stores).await;
}
pub(crate) async fn interrupt_inbox(&self, info: InterruptInfo) {
pub(crate) async fn interrupt_inbox(&self) {
let inner = self.inner.read().await;
if let InnerSchedulerState::Started(ref scheduler) = *inner {
scheduler.interrupt_inbox(info);
scheduler.interrupt_inbox();
}
}
pub(crate) async fn interrupt_smtp(&self, info: InterruptInfo) {
pub(crate) async fn interrupt_smtp(&self) {
let inner = self.inner.read().await;
if let InnerSchedulerState::Started(ref scheduler) = *inner {
scheduler.interrupt_smtp(info);
scheduler.interrupt_smtp();
}
}
@@ -463,18 +465,15 @@ async fn inbox_loop(
/// handling all the errors. In case of an error, it is logged, but not propagated upwards. If
/// critical operation fails such as fetching new messages fails, connection is reset via
/// `trigger_reconnect`, so a fresh one can be opened.
async fn fetch_idle(
ctx: &Context,
connection: &mut Imap,
folder_meaning: FolderMeaning,
) -> InterruptInfo {
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: FolderMeaning) {
let folder_config = match folder_meaning.to_config() {
Some(c) => c,
None => {
error!(ctx, "Bad folder meaning: {}", folder_meaning);
return connection
connection
.fake_idle(ctx, None, FolderMeaning::Unknown)
.await;
return;
}
};
let folder = match ctx.get_config(folder_config).await {
@@ -484,9 +483,10 @@ async fn fetch_idle(
ctx,
"Can not watch {} folder, failed to retrieve config: {:#}", folder_config, err
);
return connection
connection
.fake_idle(ctx, None, FolderMeaning::Unknown)
.await;
return;
}
};
@@ -495,9 +495,10 @@ async fn fetch_idle(
} else {
connection.connectivity.set_not_configured(ctx).await;
info!(ctx, "Can not watch {} folder, not set", folder_config);
return connection
connection
.fake_idle(ctx, None, FolderMeaning::Unknown)
.await;
return;
};
// connect and fake idle if unable to connect
@@ -508,9 +509,10 @@ async fn fetch_idle(
{
warn!(ctx, "{:#}", err);
connection.trigger_reconnect(ctx);
return connection
connection
.fake_idle(ctx, Some(watch_folder), folder_meaning)
.await;
return;
}
if folder_config == Config::ConfiguredInboxFolder {
@@ -534,7 +536,7 @@ async fn fetch_idle(
{
connection.trigger_reconnect(ctx);
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
return;
}
// Mark expired messages for deletion. Marked messages will be deleted from the server
@@ -573,7 +575,7 @@ async fn fetch_idle(
{
connection.trigger_reconnect(ctx);
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
return;
}
}
Ok(false) => {}
@@ -591,55 +593,56 @@ async fn fetch_idle(
connection.connectivity.set_connected(ctx).await;
ctx.emit_event(EventType::ImapInboxIdle);
if let Some(session) = connection.session.take() {
if !session.can_idle() {
info!(
ctx,
"IMAP session does not support IDLE, going to fake idle."
);
return connection
.fake_idle(ctx, Some(watch_folder), folder_meaning)
.await;
}
if ctx
.get_config_bool(Config::DisableIdle)
.await
.context("Failed to get disable_idle config")
.log_err(ctx)
.unwrap_or_default()
{
info!(ctx, "IMAP IDLE is disabled, going to fake idle.");
return connection
.fake_idle(ctx, Some(watch_folder), folder_meaning)
.await;
}
info!(ctx, "IMAP session supports IDLE, using it.");
match session
.idle(
ctx,
connection.idle_interrupt_receiver.clone(),
&watch_folder,
)
.await
.context("idle")
{
Ok((session, info)) => {
connection.session = Some(session);
info
}
Err(err) => {
connection.trigger_reconnect(ctx);
warn!(ctx, "{:#}", err);
InterruptInfo::new(false)
}
}
} else {
let Some(session) = connection.session.take() else {
warn!(ctx, "No IMAP session, going to fake idle.");
connection
.fake_idle(ctx, Some(watch_folder), folder_meaning)
.await
.await;
return;
};
if !session.can_idle() {
info!(
ctx,
"IMAP session does not support IDLE, going to fake idle."
);
connection
.fake_idle(ctx, Some(watch_folder), folder_meaning)
.await;
return;
}
if ctx
.get_config_bool(Config::DisableIdle)
.await
.context("Failed to get disable_idle config")
.log_err(ctx)
.unwrap_or_default()
{
info!(ctx, "IMAP IDLE is disabled, going to fake idle.");
connection
.fake_idle(ctx, Some(watch_folder), folder_meaning)
.await;
return;
}
info!(ctx, "IMAP session supports IDLE, using it.");
match session
.idle(
ctx,
connection.idle_interrupt_receiver.clone(),
&watch_folder,
)
.await
.context("idle")
{
Ok(session) => {
connection.session = Some(session);
}
Err(err) => {
connection.trigger_reconnect(ctx);
warn!(ctx, "{:#}", err);
}
}
}
@@ -706,8 +709,9 @@ async fn smtp_loop(
loop {
if let Err(err) = send_smtp_messages(&ctx, &mut connection).await {
warn!(ctx, "send_smtp_messages failed: {:#}", err);
timeout = Some(timeout.map_or(30, |timeout: u64| timeout.saturating_mul(3)))
timeout = Some(timeout.unwrap_or(30));
} else {
timeout = None;
let duration_until_can_send = ctx.ratelimit.read().await.until_can_send();
if !duration_until_can_send.is_zero() {
info!(
@@ -715,14 +719,9 @@ async fn smtp_loop(
"smtp got rate limited, waiting for {} until can send again",
duration_to_str(duration_until_can_send)
);
tokio::time::timeout(duration_until_can_send, async {
idle_interrupt_receiver.recv().await.unwrap_or_default()
})
.await
.unwrap_or_default();
tokio::time::sleep(duration_until_can_send).await;
continue;
}
timeout = None;
}
// Fake Idle
@@ -736,17 +735,23 @@ async fn smtp_loop(
// sending is retried (at the latest) after the timeout. If sending fails
// again, we increase the timeout exponentially, in order not to do lots of
// unnecessary retries.
if let Some(timeout) = timeout {
if let Some(t) = timeout {
let now = tokio::time::Instant::now();
info!(
ctx,
"smtp has messages to retry, planning to retry {} seconds later", timeout
"smtp has messages to retry, planning to retry {} seconds later", t,
);
let duration = std::time::Duration::from_secs(timeout);
let duration = std::time::Duration::from_secs(t);
tokio::time::timeout(duration, async {
idle_interrupt_receiver.recv().await.unwrap_or_default()
})
.await
.unwrap_or_default();
let slept = (tokio::time::Instant::now() - now).as_secs();
timeout = Some(cmp::max(
t,
slept.saturating_add(rand::thread_rng().gen_range((slept / 2)..=slept)),
));
} else {
info!(ctx, "smtp has no messages to retry, waiting for interrupt");
idle_interrupt_receiver.recv().await.unwrap_or_default();
@@ -860,24 +865,24 @@ impl Scheduler {
fn maybe_network(&self) {
for b in self.boxes() {
b.conn_state.interrupt(InterruptInfo::new(true));
b.conn_state.interrupt();
}
self.interrupt_smtp(InterruptInfo::new(true));
self.interrupt_smtp();
}
fn maybe_network_lost(&self) {
for b in self.boxes() {
b.conn_state.interrupt(InterruptInfo::new(false));
b.conn_state.interrupt();
}
self.interrupt_smtp(InterruptInfo::new(false));
self.interrupt_smtp();
}
fn interrupt_inbox(&self, info: InterruptInfo) {
self.inbox.conn_state.interrupt(info);
fn interrupt_inbox(&self) {
self.inbox.conn_state.interrupt();
}
fn interrupt_smtp(&self, info: InterruptInfo) {
self.smtp.interrupt(info);
fn interrupt_smtp(&self) {
self.smtp.interrupt();
}
fn interrupt_ephemeral_task(&self) {
@@ -927,7 +932,7 @@ struct ConnectionState {
/// Channel to interrupt the whole connection.
stop_sender: Sender<()>,
/// Channel to interrupt idle.
idle_interrupt_sender: Sender<InterruptInfo>,
idle_interrupt_sender: Sender<()>,
/// Mutex to pass connectivity info between IMAP/SMTP threads and the API
connectivity: ConnectivityStore,
}
@@ -943,9 +948,9 @@ impl ConnectionState {
Ok(())
}
fn interrupt(&self, info: InterruptInfo) {
fn interrupt(&self) {
// Use try_send to avoid blocking on interrupts.
self.idle_interrupt_sender.try_send(info).ok();
self.idle_interrupt_sender.try_send(()).ok();
}
}
@@ -977,8 +982,8 @@ impl SmtpConnectionState {
}
/// Interrupt any form of idle.
fn interrupt(&self, info: InterruptInfo) {
self.state.interrupt(info);
fn interrupt(&self) {
self.state.interrupt();
}
/// Shutdown this connection completely.
@@ -991,7 +996,7 @@ impl SmtpConnectionState {
struct SmtpConnectionHandlers {
connection: Smtp,
stop_receiver: Receiver<()>,
idle_interrupt_receiver: Receiver<InterruptInfo>,
idle_interrupt_receiver: Receiver<()>,
}
#[derive(Debug)]
@@ -1022,8 +1027,8 @@ impl ImapConnectionState {
}
/// Interrupt any form of idle.
fn interrupt(&self, info: InterruptInfo) {
self.state.interrupt(info);
fn interrupt(&self) {
self.state.interrupt();
}
/// Shutdown this connection completely.
@@ -1038,14 +1043,3 @@ struct ImapConnectionHandlers {
connection: Imap,
stop_receiver: Receiver<()>,
}
#[derive(Default, Debug)]
pub struct InterruptInfo {
pub probe_network: bool,
}
impl InterruptInfo {
pub fn new(probe_network: bool) -> Self {
Self { probe_network }
}
}
+44 -33
View File
@@ -8,7 +8,7 @@ use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use crate::aheader::EncryptPreference;
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::{Blocked, Chattype};
use crate::constants::Blocked;
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::e2ee::ensure_secret_key_exists;
@@ -21,6 +21,7 @@ use crate::param::Param;
use crate::peerstate::{Peerstate, PeerstateKeyType};
use crate::qr::check_qr;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::token;
use crate::tools::time;
@@ -36,17 +37,15 @@ use crate::token::Namespace;
/// Set of characters to percent-encode in email addresses and names.
pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
macro_rules! inviter_progress {
($context:tt, $contact_id:expr, $progress:expr) => {
assert!(
$progress >= 0 && $progress <= 1000,
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
);
$context.emit_event($crate::events::EventType::SecurejoinInviterProgress {
contact_id: $contact_id,
progress: $progress,
});
};
fn inviter_progress(context: &Context, contact_id: ContactId, progress: usize) {
debug_assert!(
progress <= 1000,
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
);
context.emit_event(EventType::SecurejoinInviterProgress {
contact_id,
progress,
});
}
/// Generates a Secure Join QR code.
@@ -318,7 +317,7 @@ pub(crate) async fn handle_securejoin_handshake(
}
info!(context, "Secure-join requested.",);
inviter_progress!(context, contact_id, 300);
inviter_progress(context, contact_id, 300);
// for setup-contact, make Alice's one-to-one chat with Bob visible
// (secure-join-information are shown in the group chat)
@@ -430,7 +429,7 @@ pub(crate) async fn handle_securejoin_handshake(
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await?;
info!(context, "Auth verified.",);
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
inviter_progress!(context, contact_id, 600);
inviter_progress(context, contact_id, 600);
if join_vg {
// the vg-member-added message is special:
// this is a normal Chat-Group-Member-Added message
@@ -445,13 +444,19 @@ pub(crate) async fn handle_securejoin_handshake(
match chat::get_chat_id_by_grpid(context, field_grpid).await? {
Some((group_chat_id, _, _)) => {
secure_connection_established(context, contact_id, group_chat_id).await?;
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
.await?;
chat::add_contact_to_chat_ex(
context,
Nosync,
group_chat_id,
contact_id,
true,
)
.await?;
}
None => bail!("Chat {} not found", &field_grpid),
}
inviter_progress!(context, contact_id, 800);
inviter_progress!(context, contact_id, 1000);
inviter_progress(context, contact_id, 800);
inviter_progress(context, contact_id, 1000);
} else {
// Alice -> Bob
secure_connection_established(
@@ -469,7 +474,7 @@ pub(crate) async fn handle_securejoin_handshake(
.await
.context("failed sending vc-contact-confirm message")?;
inviter_progress!(context, contact_id, 1000);
inviter_progress(context, contact_id, 1000);
}
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
}
@@ -611,6 +616,8 @@ pub(crate) async fn observe_securejoin_on_other_device(
peerstate.set_verified(PeerstateKeyType::GossipKey, fingerprint, addr)?;
peerstate.prefer_encrypt = EncryptPreference::Mutual;
peerstate.save_to_db(&context.sql).await.unwrap_or_default();
ChatId::set_protection_for_contact(context, contact_id).await?;
} else if let Some(fingerprint) =
mime_message.get_header(HeaderDef::SecureJoinFingerprint)
{
@@ -651,10 +658,10 @@ pub(crate) async fn observe_securejoin_on_other_device(
return Ok(HandshakeMessage::Ignore);
}
if step.as_str() == "vg-member-added" {
inviter_progress!(context, contact_id, 800);
inviter_progress(context, contact_id, 800);
}
if step.as_str() == "vg-member-added" || step.as_str() == "vc-contact-confirm" {
inviter_progress!(context, contact_id, 1000);
inviter_progress(context, contact_id, 1000);
}
if step.as_str() == "vg-request-with-auth" || step.as_str() == "vc-request-with-auth" {
// This actually reflects what happens on the first device (which does the secure
@@ -684,17 +691,17 @@ async fn secure_connection_established(
.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
{
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ == Chattype::Single {
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
time(),
Some(contact_id),
)
.await?;
}
let private_chat_id = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes)
.await?
.id;
private_chat_id
.set_protection(
context,
ProtectionStatus::Protected,
time(),
Some(contact_id),
)
.await?;
}
context.emit_event(EventType::ChatModified(chat_id));
Ok(())
@@ -767,6 +774,7 @@ mod tests {
use crate::chat;
use crate::chat::ProtectionStatus;
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::ContactAddress;
use crate::contact::VerifiedStatus;
use crate::peerstate::Peerstate;
@@ -1027,8 +1035,11 @@ mod tests {
gossip_key_fingerprint: Some(alice_pubkey.fingerprint()),
verified_key: None,
verified_key_fingerprint: None,
fingerprint_changed: false,
verifier: None,
secondary_verified_key: None,
secondary_verified_key_fingerprint: None,
secondary_verifier: None,
fingerprint_changed: false,
};
peerstate.save_to_db(&bob.ctx.sql).await?;
+5 -5
View File
@@ -132,6 +132,7 @@ pub(super) async fn handle_contact_confirm(
// verify both contacts (this could be a bug/security issue, see
// e.g. https://github.com/deltachat/deltachat-core-rust/issues/1177).
bobstate.notify_peer_verified(context).await?;
bobstate.emit_progress(context, JoinerProgress::Succeeded);
Ok(retval)
}
Some(_) => {
@@ -228,9 +229,8 @@ impl BobState {
if context
.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
&& chat_id == self.alice_chat()
{
chat_id
self.alice_chat()
.set_protection(
context,
ProtectionStatus::Protected,
@@ -256,8 +256,8 @@ enum JoinerProgress {
///
/// Typically shows as "alice@addr verified, introducing myself."
RequestWithAuthSent,
// /// Completed securejoin.
// Succeeded,
/// Completed securejoin.
Succeeded,
}
impl From<JoinerProgress> for usize {
@@ -265,7 +265,7 @@ impl From<JoinerProgress> for usize {
match progress {
JoinerProgress::Error => 0,
JoinerProgress::RequestWithAuthSent => 400,
// JoinerProgress::Succeeded => 1000,
JoinerProgress::Succeeded => 1000,
}
}
}
+13
View File
@@ -750,6 +750,19 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
.await?;
}
if dbversion < 104 {
sql.execute_migration(
"ALTER TABLE acpeerstates
ADD COLUMN secondary_verified_key;
ALTER TABLE acpeerstates
ADD COLUMN secondary_verified_key_fingerprint TEXT DEFAULT '';
ALTER TABLE acpeerstates
ADD COLUMN secondary_verifier TEXT DEFAULT ''",
104,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?
+9 -1
View File
@@ -416,6 +416,9 @@ pub enum StockMessage {
#[strum(props(fallback = "Others will only see this group after you sent a first message."))]
NewGroupSendFirstMessage = 172,
#[strum(props(fallback = "Member %1$s added."))]
MsgAddMember = 173,
}
impl StockMessage {
@@ -603,6 +606,7 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
}
/// Stock string: `I added member %1$s.`.
/// This one is for sending in group chats.
///
/// The `added_member_addr` parameter should be an email address and is looked up in the
/// contacts to combine with the authorized display name.
@@ -637,7 +641,11 @@ pub(crate) async fn msg_add_member_local(
.unwrap_or_else(|_| addr.to_string()),
_ => addr.to_string(),
};
if by_contact == ContactId::SELF {
if by_contact == ContactId::UNDEFINED {
translated(context, StockMessage::MsgAddMember)
.await
.replace1(whom)
} else if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouAddMember)
.await
.replace1(whom)
+54 -182
View File
@@ -5,11 +5,12 @@ use lettre_email::mime::{self};
use lettre_email::PartBuilder;
use serde::{Deserialize, Serialize};
use crate::chat::{self, Chat, ChatVisibility};
use crate::chat::{self, Chat, ChatId};
use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::ContactId;
use crate::context::Context;
use crate::log::LogExt;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
@@ -41,28 +42,14 @@ pub(crate) struct QrTokenData {
pub(crate) grpid: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub(crate) enum ChatId {
ContactAddr(String),
Grpid(String),
// NOTE: Ad-hoc groups lack an identifier that can be used across devices so
// block/mute/etc. actions on them are not synchronized to other devices.
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub(crate) enum ChatAction {
Block,
Unblock,
Accept,
SetVisibility(ChatVisibility),
SetMuted(chat::MuteDuration),
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) enum SyncData {
AddQrToken(QrTokenData),
DeleteQrToken(QrTokenData),
AlterChat { id: ChatId, action: ChatAction },
AlterChat {
id: chat::SyncId,
action: chat::SyncAction,
},
}
#[derive(Debug, Serialize, Deserialize)]
@@ -104,7 +91,7 @@ impl Context {
/// Adds most recent qr-code tokens for a given chat to the list of items to be synced.
/// If device synchronization is disabled,
/// no tokens exist or the chat is unpromoted, the function does nothing.
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<chat::ChatId>) -> Result<()> {
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<ChatId>) -> Result<()> {
if !self.get_config_bool(Config::SyncMsgs).await? {
return Ok(());
}
@@ -155,7 +142,7 @@ impl Context {
pub async fn send_sync_msg(&self) -> Result<Option<MsgId>> {
if let Some((json, ids)) = self.build_sync_json().await? {
let chat_id =
chat::ChatId::create_for_contact_with_blocked(self, ContactId::SELF, Blocked::Yes)
ChatId::create_for_contact_with_blocked(self, ContactId::SELF, Blocked::Yes)
.await?;
let mut msg = Message {
chat_id,
@@ -248,41 +235,44 @@ impl Context {
/// as otherwise we would add in a dead-loop between two devices
/// sending message back and forth.
///
/// If an error is returned, the caller shall not try over.
/// Therefore, errors should only be returned on database errors or so.
/// If eg. just an item cannot be deleted,
/// that should not hold off the other items to be executed.
pub(crate) async fn execute_sync_items(&self, items: &SyncItems) -> Result<()> {
/// If an error is returned, the caller shall not try over because some sync items could be
/// already executed. Sync items are considered independent and executed in the given order but
/// regardless of whether executing of the previous items succeeded.
pub(crate) async fn execute_sync_items(&self, items: &SyncItems) {
info!(self, "executing {} sync item(s)", items.items.len());
for item in &items.items {
match &item.data {
AddQrToken(token) => {
let chat_id = if let Some(grpid) = &token.grpid {
if let Some((chat_id, _, _)) =
chat::get_chat_id_by_grpid(self, grpid).await?
{
Some(chat_id)
} else {
warn!(
self,
"Ignoring token for nonexistent/deleted group '{}'.", grpid
);
continue;
}
} else {
None
};
token::save(self, Namespace::InviteNumber, chat_id, &token.invitenumber)
.await?;
token::save(self, Namespace::Auth, chat_id, &token.auth).await?;
}
DeleteQrToken(token) => {
token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?;
token::delete(self, Namespace::Auth, &token.auth).await?;
}
AlterChat { id, action } => self.sync_alter_chat(id, action).await?,
AddQrToken(token) => self.add_qr_token(token).await,
DeleteQrToken(token) => self.delete_qr_token(token).await,
AlterChat { id, action } => self.sync_alter_chat(id, action).await,
}
.log_err(self)
.ok();
}
}
async fn add_qr_token(&self, token: &QrTokenData) -> Result<()> {
let chat_id = if let Some(grpid) = &token.grpid {
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(self, grpid).await? {
Some(chat_id)
} else {
warn!(
self,
"Ignoring token for nonexistent/deleted group '{}'.", grpid
);
return Ok(());
}
} else {
None
};
token::save(self, Namespace::InviteNumber, chat_id, &token.invitenumber).await?;
token::save(self, Namespace::Auth, chat_id, &token.auth).await?;
Ok(())
}
async fn delete_qr_token(&self, token: &QrTokenData) -> Result<()> {
token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?;
token::delete(self, Namespace::Auth, &token.auth).await?;
Ok(())
}
}
@@ -292,10 +282,9 @@ mod tests {
use std::time::{Duration, SystemTime};
use anyhow::bail;
use strum::IntoEnumIterator;
use super::*;
use crate::chat::{Chat, ProtectionStatus};
use crate::chat::Chat;
use crate::chatlist::Chatlist;
use crate::contact::{Contact, Origin};
use crate::test_utils::TestContext;
@@ -319,13 +308,13 @@ mod tests {
assert!(t.build_sync_json().await?.is_none());
// Having one test on `SyncData::AlterChat` is sufficient here as `ChatAction::SetMuted`
// introduces enums inside items and `SystemTime`. Let's avoid in-depth testing of the
// serialiser here which is an external crate.
// Having one test on `SyncData::AlterChat` is sufficient here as
// `chat::SyncAction::SetMuted` introduces enums inside items and `SystemTime`. Let's avoid
// in-depth testing of the serialiser here which is an external crate.
t.add_sync_item_with_timestamp(
SyncData::AlterChat {
id: ChatId::ContactAddr("bob@example.net".to_string()),
action: ChatAction::SetMuted(chat::MuteDuration::Until(
id: chat::SyncId::ContactAddr("bob@example.net".to_string()),
action: chat::SyncAction::SetMuted(chat::MuteDuration::Until(
SystemTime::UNIX_EPOCH + Duration::from_millis(42999),
)),
},
@@ -438,10 +427,13 @@ mod tests {
let AlterChat { id, action } = &sync_items.items.get(0).unwrap().data else {
bail!("bad item");
};
assert_eq!(*id, ChatId::ContactAddr("bob@example.net".to_string()));
assert_eq!(
*id,
chat::SyncId::ContactAddr("bob@example.net".to_string())
);
assert_eq!(
*action,
ChatAction::SetMuted(chat::MuteDuration::Until(
chat::SyncAction::SetMuted(chat::MuteDuration::Until(
SystemTime::UNIX_EPOCH + Duration::from_millis(42999)
))
);
@@ -512,7 +504,7 @@ mod tests {
.to_string(),
)
?;
t.execute_sync_items(&sync_items).await?;
t.execute_sync_items(&sync_items).await;
assert!(
Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Unknown)
@@ -546,7 +538,7 @@ mod tests {
// check that the used self-talk is not visible to the user
// but that creation will still work (in this case, the chat is empty)
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
let chat_id = chat::ChatId::create_for_contact(&alice, ContactId::SELF).await?;
let chat_id = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
let chat = Chat::load_from_db(&alice, chat_id).await?;
assert!(chat.is_self_talk());
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
@@ -569,124 +561,4 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_alter_chat() -> Result<()> {
let alices = [
TestContext::new_alice().await,
TestContext::new_alice().await,
];
for a in &alices {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let bob = TestContext::new_bob().await;
let ba_chat = bob.create_chat(&alices[0]).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
let a0b_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
alices[1].recv_msg(&sent_msg).await;
async fn sync(alices: &[TestContext]) -> Result<()> {
let sync_msg = alices.get(0).unwrap().pop_sent_msg().await;
alices.get(1).unwrap().recv_msg(&sync_msg).await;
Ok(())
}
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Request);
a0b_chat_id.accept(&alices[0]).await?;
sync(&alices).await?;
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
a0b_chat_id.block(&alices[0]).await?;
sync(&alices).await?;
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Yes);
a0b_chat_id.unblock(&alices[0]).await?;
sync(&alices).await?;
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
// Unblocking a 1:1 chat doesn't unblock the contact currently.
let a0b_contact_id = alices[0].add_or_lookup_contact(&bob).await.id;
Contact::unblock(&alices[0], a0b_contact_id).await?;
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
Contact::block(&alices[0], a0b_contact_id).await?;
sync(&alices).await?;
assert!(alices[1].add_or_lookup_contact(&bob).await.is_blocked());
Contact::unblock(&alices[0], a0b_contact_id).await?;
sync(&alices).await?;
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
// Test accepting and blocking groups. This way we test:
// - Group chats synchronisation.
// - That blocking a group deletes it on other devices.
let fiona = TestContext::new_fiona().await;
let fiona_grp_chat_id = fiona
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&alices[0]])
.await;
let sent_msg = fiona.send_text(fiona_grp_chat_id, "hi").await;
let a0_grp_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
let a1_grp_chat_id = alices[1].recv_msg(&sent_msg).await.chat_id;
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
assert_eq!(a1_grp_chat.blocked, Blocked::Request);
a0_grp_chat_id.accept(&alices[0]).await?;
sync(&alices).await?;
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
assert_eq!(a1_grp_chat.blocked, Blocked::Not);
a0_grp_chat_id.block(&alices[0]).await?;
sync(&alices).await?;
assert!(Chat::load_from_db(&alices[1], a1_grp_chat_id)
.await
.is_err());
assert!(
!alices[1]
.sql
.exists("SELECT COUNT(*) FROM chats WHERE id=?", (a1_grp_chat_id,))
.await?
);
// Test syncing of chat visibility on a self-chat. This way we test:
// - Self-chat synchronisation.
// - That sync messages don't unarchive the self-chat.
let a0self_chat_id = alices[0].get_self_chat().await.id;
assert_eq!(
alices[1].get_self_chat().await.get_visibility(),
ChatVisibility::Normal
);
let mut visibilities =
ChatVisibility::iter().chain(std::iter::once(ChatVisibility::Normal));
visibilities.next();
for v in visibilities {
a0self_chat_id.set_visibility(&alices[0], v).await?;
sync(&alices).await?;
for a in &alices {
assert_eq!(a.get_self_chat().await.get_visibility(), v);
}
}
use chat::MuteDuration;
assert_eq!(
alices[1].get_chat(&bob).await.mute_duration,
MuteDuration::NotMuted
);
let mute_durations = [
MuteDuration::Forever,
MuteDuration::Until(SystemTime::now() + Duration::from_secs(42)),
MuteDuration::NotMuted,
];
for m in mute_durations {
chat::set_muted(&alices[0], a0b_chat_id, m).await?;
sync(&alices).await?;
let m = match m {
MuteDuration::Until(time) => MuteDuration::Until(
SystemTime::UNIX_EPOCH
+ Duration::from_secs(
time.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
),
),
_ => m,
};
assert_eq!(alices[1].get_chat(&bob).await.mute_duration, m);
}
Ok(())
}
}
+10 -10
View File
@@ -22,8 +22,8 @@ use tokio::sync::RwLock;
use tokio::{fs, task};
use crate::chat::{
self, add_to_chat_contacts_table, create_group_chat, Chat, ChatId, MessageListOptions,
ProtectionStatus,
self, add_to_chat_contacts_table, create_group_chat, Chat, ChatId, ChatIdBlocked,
MessageListOptions, ProtectionStatus,
};
use crate::chatlist::Chatlist;
use crate::config::Config;
@@ -514,12 +514,7 @@ impl TestContext {
.await
.expect("receive_imf() seems not to have added a new message to the db");
assert_eq!(
received.msg_ids.len(),
1,
"recv_msg() can currently only receive messages with exactly one part"
);
let msg = Message::load_from_db(self, received.msg_ids[0])
let msg = Message::load_from_db(self, *received.msg_ids.last().unwrap())
.await
.unwrap();
@@ -597,14 +592,17 @@ impl TestContext {
}
/// Returns 1:1 [`Chat`] with another account. Panics if it doesn't exist.
/// May return a blocked chat.
///
/// This first creates a contact using the configured details on the other account, then
/// gets the 1:1 chat with this contact.
pub async fn get_chat(&self, other: &TestContext) -> Chat {
let contact = self.add_or_lookup_contact(other).await;
let chat_id = ChatId::lookup_by_contact(&self.ctx, contact.id)
let chat_id = ChatIdBlocked::lookup_by_contact(&self.ctx, contact.id)
.await
.unwrap()
.map(|chat_id_blocked| chat_id_blocked.id)
.expect(
"There is no chat with this contact. \
Hint: Use create_chat() instead of get_chat() if this is expected.",
@@ -716,7 +714,9 @@ impl TestContext {
})
.collect();
let sel_chat = Chat::load_from_db(self, chat_id).await.unwrap();
let Ok(sel_chat) = Chat::load_from_db(self, chat_id).await else {
return String::from("Can't load chat\n");
};
let members = chat::get_chat_contacts(self, sel_chat.id).await.unwrap();
let subtitle = if sel_chat.is_device_talk() {
"device-talk".to_string()
+49 -9
View File
@@ -1,7 +1,7 @@
use anyhow::Result;
use pretty_assertions::assert_eq;
use crate::chat::{Chat, ProtectionStatus};
use crate::chat::ProtectionStatus;
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::DC_GCL_FOR_FORWARDING;
@@ -126,24 +126,22 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
VerifiedStatus::BidirectVerified
);
// As soon as Alice creates a chat with Fiona, it should directly be protected
// Alice should have a hidden protected chat with Fiona
{
let chat = alice.create_chat(&fiona).await;
let chat = alice.get_chat(&fiona).await;
assert!(chat.is_protected());
let msg = alice.get_last_msg().await;
let msg = get_chat_msg(&alice, chat.id, 0, 1).await;
let expected_text = stock_str::chat_protection_enabled(&alice).await;
assert_eq!(msg.text, expected_text);
}
// Fiona should also see the chat as protected
// Fiona should have a hidden protected chat with Alice
{
let rcvd = tcm.send_recv(&alice, &fiona, "Hi Fiona").await;
let alice_fiona_id = rcvd.chat_id;
let chat = Chat::load_from_db(&fiona, alice_fiona_id).await?;
let chat = fiona.get_chat(&alice).await;
assert!(chat.is_protected());
let msg0 = get_chat_msg(&fiona, chat.id, 0, 2).await;
let msg0 = get_chat_msg(&fiona, chat.id, 0, 1).await;
let expected_text = stock_str::chat_protection_enabled(&fiona).await;
assert_eq!(msg0.text, expected_text);
}
@@ -165,6 +163,15 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
assert!(!chat.is_protected());
assert!(chat.is_protection_broken());
let msg1 = get_chat_msg(&alice, chat.id, 0, 3).await;
assert_eq!(msg1.get_info_type(), SystemMessage::ChatProtectionEnabled);
let msg2 = get_chat_msg(&alice, chat.id, 1, 3).await;
assert_eq!(msg2.get_info_type(), SystemMessage::ChatProtectionDisabled);
let msg2 = get_chat_msg(&alice, chat.id, 2, 3).await;
assert_eq!(msg2.text, "I have a new device");
// After recreating the chat, it should still be unprotected
chat.id.delete(&alice).await?;
@@ -705,6 +712,39 @@ async fn test_break_protection_then_verify_again() -> Result<()> {
Ok(())
}
/// Regression test for the following bug:
///
/// - Scan your chat partner's QR Code
/// - They change devices
/// - Scan their QR code again
///
/// -> The re-verification fails.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verify_then_verify_again() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
alice.create_chat(&bob).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
tcm.section("Bob reinstalls DC");
drop(bob);
let bob_new = tcm.unconfigured().await;
enable_verified_oneonone_chats(&[&bob_new]).await;
bob_new.configure_addr("bob@example.net").await;
e2ee::ensure_secret_key_exists(&bob_new).await?;
tcm.execute_securejoin(&bob_new, &alice).await;
assert_verified(&alice, &bob_new, ProtectionStatus::Protected).await;
Ok(())
}
/// Regression test:
/// - Verify a contact
/// - The contact stops using DC and sends a message from a classical MUA instead
+1 -4
View File
@@ -37,7 +37,6 @@ use crate::mimefactory::wrapped_base64_encode;
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::param::Params;
use crate::scheduler::InterruptInfo;
use crate::tools::strip_rtlo_characters;
use crate::tools::{create_smeared_timestamp, get_abs_path};
@@ -485,9 +484,7 @@ impl Context {
DO UPDATE SET last_serial=excluded.last_serial, descr=excluded.descr",
(instance.id, status_update_serial, status_update_serial, descr),
).await?;
self.scheduler
.interrupt_smtp(InterruptInfo::new(false))
.await;
self.scheduler.interrupt_smtp().await;
}
Ok(())
}
@@ -0,0 +1,7 @@
Group#Chat#10: Group chat [4 member(s)]
--------------------------------------------------------------------------------
Msg#10: (Contact#Contact#10): Hi! I created a group. [FRESH]
Msg#11: Me (Contact#Contact#Self): You left the group. [INFO] o
Msg#12: (Contact#Contact#10): Member claire@example.net added by alice@example.org. [FRESH][INFO]
Msg#13: (Contact#Contact#10): Member Me (bob@example.net) added. [FRESH][INFO]
--------------------------------------------------------------------------------