mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 05:26:42 +03:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d80b749dec | ||
|
|
039a8b7c36 | ||
|
|
779f58ab16 | ||
|
|
b9183fe5eb | ||
|
|
9d342671d5 | ||
|
|
4e47ebd5fc | ||
|
|
d5c418e909 | ||
|
|
85414558c5 | ||
|
|
d6af8d2526 | ||
|
|
1209e95e34 | ||
|
|
51f9279e67 | ||
|
|
f27d54f7fa | ||
|
|
7f3648f8ae | ||
|
|
49fc258578 | ||
|
|
0c51b4fe41 | ||
|
|
dbad714539 | ||
|
|
edd8008650 | ||
|
|
615a1b3f4e | ||
|
|
fe6044e1aa | ||
|
|
46b275bfab | ||
|
|
25f44c517a | ||
|
|
cac04f8ee4 | ||
|
|
45d8566ec0 | ||
|
|
29a98ba13b |
66
CHANGELOG.md
66
CHANGELOG.md
@@ -1,5 +1,66 @@
|
||||
# Changelog
|
||||
|
||||
## [2.8.0] - 2025-07-28
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Remove ProtectionBroken, make such chats Unprotected ([#7041](https://github.com/chatmail/core/pull/7041)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Lookup self by address if there is no fingerprint or gossip.
|
||||
|
||||
## [2.7.0] - 2025-07-26
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Mimefactory: Order message recipients by time of addition ([#6872](https://github.com/chatmail/core/pull/6872)).
|
||||
- Put the debug/release build version into the info ([#7034](https://github.com/chatmail/core/pull/7034)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Realtime late join ([#6869](https://github.com/chatmail/core/pull/6869)).
|
||||
- Do not fail to upgrade if the verifier of a contact doesn't exist anymore ([#7044](https://github.com/chatmail/core/pull/7044)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Add regression test for verification-gossiping crash ([#7033](https://github.com/chatmail/core/pull/7033)).
|
||||
|
||||
## [2.6.0] - 2025-07-23
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix crash when receiving a verification-gossiping message which a contact also sends to itself ([#7032](https://github.com/chatmail/core/pull/7032)).
|
||||
|
||||
## [2.5.0] - 2025-07-22
|
||||
|
||||
### Fixes
|
||||
|
||||
- Correctly migrate "verified by me".
|
||||
- Mark all email chats as unprotected in the migration ([#7026](https://github.com/chatmail/core/pull/7026)).
|
||||
- Do not ignore errors in add_flag_finalized_with_set.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Deprecate protection-broken and related stuff ([#7018](https://github.com/chatmail/core/pull/7018)).
|
||||
- Clarify the meaning of is_verified() vs verifier_id() ([#7027](https://github.com/chatmail/core/pull/7027)).
|
||||
- STYLE.md: Prefer `try_next()` over `next()`.
|
||||
|
||||
## [2.4.0] - 2025-07-21
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not ignore errors when draining FETCH responses. This avoids IMAP loop getting stuck in an infinite loop retrying reading from the connection.
|
||||
- Update `tokio-io-timeout` to 1.2.1. This release includes a fix to reset timeout after every error, so timeout error is returned at most once a minute if read is attempted after a timeout.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update async-imap to 0.11.0.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use `try_next()` when processing FETCH responses.
|
||||
|
||||
## [2.3.0] - 2025-07-19
|
||||
|
||||
### Features / Changes
|
||||
@@ -6495,3 +6556,8 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.1.0]: https://github.com/chatmail/core/compare/v2.0.0..v2.1.0
|
||||
[2.2.0]: https://github.com/chatmail/core/compare/v2.1.0..v2.2.0
|
||||
[2.3.0]: https://github.com/chatmail/core/compare/v2.2.0..v2.3.0
|
||||
[2.4.0]: https://github.com/chatmail/core/compare/v2.3.0..v2.4.0
|
||||
[2.5.0]: https://github.com/chatmail/core/compare/v2.4.0..v2.5.0
|
||||
[2.6.0]: https://github.com/chatmail/core/compare/v2.5.0..v2.6.0
|
||||
[2.7.0]: https://github.com/chatmail/core/compare/v2.6.0..v2.7.0
|
||||
[2.8.0]: https://github.com/chatmail/core/compare/v2.7.0..v2.8.0
|
||||
|
||||
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -268,9 +268,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.10.4"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca726c61b73c471f531b65e83e161776ba62c2b6ba4ec73d51fad357009ed00a"
|
||||
checksum = "8e9f9a9c94a403cf46aa2b4cecbceefc6e4284441ebbeca79b80f3bab4394458"
|
||||
dependencies = [
|
||||
"async-channel 2.3.1",
|
||||
"async-compression",
|
||||
@@ -1285,7 +1285,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.3.0"
|
||||
version = "2.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -1395,7 +1395,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.3.0"
|
||||
version = "2.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.3.1",
|
||||
@@ -1417,7 +1417,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.3.0"
|
||||
version = "2.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1433,7 +1433,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.3.0"
|
||||
version = "2.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1462,7 +1462,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.3.0"
|
||||
version = "2.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -3826,7 +3826,7 @@ version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
|
||||
dependencies = [
|
||||
"proc-macro-crate 2.0.0",
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
@@ -6073,9 +6073,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-io-timeout"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
|
||||
checksum = "0bd86198d9ee903fedd2f9a2e72014287c0d9167e4ae43b5853007205dda1b76"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.3.0"
|
||||
version = "2.8.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.85"
|
||||
@@ -44,7 +44,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.10.4", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-imap = { version = "0.11.0", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
@@ -101,7 +101,7 @@ strum_macros = "0.27"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.2"
|
||||
thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-io-timeout = "1.2.1"
|
||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
|
||||
21
STYLE.md
21
STYLE.md
@@ -78,6 +78,27 @@ All errors should be handled in one of these ways:
|
||||
- With `.log_err().ok()`.
|
||||
- Bubbled up with `?`.
|
||||
|
||||
When working with [async streams](https://docs.rs/futures/0.3.31/futures/stream/index.html),
|
||||
prefer [`try_next`](https://docs.rs/futures/0.3.31/futures/stream/trait.TryStreamExt.html#method.try_next) over `next()`, e.g. do
|
||||
```
|
||||
while let Some(event) = stream.try_next().await? {
|
||||
todo!();
|
||||
}
|
||||
```
|
||||
instead of
|
||||
```
|
||||
while let Some(event_res) = stream.next().await {
|
||||
todo!();
|
||||
}
|
||||
```
|
||||
as it allows bubbling up the error early with `?`
|
||||
with no way to accidentally skip error processing
|
||||
with early `continue` or `break`.
|
||||
Some streams reading from a connection
|
||||
return infinite number of `Some(Err(_))`
|
||||
items when connection breaks and not processing
|
||||
errors may result in infinite loop.
|
||||
|
||||
`backtrace` feature is enabled for `anyhow` crate
|
||||
and `debug = 1` option is set in the test profile.
|
||||
This allows to run `RUST_BACKTRACE=1 cargo test`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.3.0"
|
||||
version = "2.8.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -503,13 +503,6 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
|
||||
* seconds. 2 days by default.
|
||||
* This is not supposed to be changed by UIs and only used for testing.
|
||||
* - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it
|
||||
* to 1 if it supports verified 1:1 chats.
|
||||
* Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified,
|
||||
* and when the key changes, an info message is posted into the chat.
|
||||
* 0=Nothing else happens when the key changes.
|
||||
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
|
||||
* until `dc_accept_chat()` is called.
|
||||
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
|
||||
* - `is_muted` = Whether a context is muted by the user.
|
||||
* Muted contexts should not sound, vibrate or show notifications.
|
||||
@@ -3818,21 +3811,12 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
||||
/**
|
||||
* Check if a chat is protected.
|
||||
*
|
||||
* End-to-end encryption is guaranteed in protected chats
|
||||
* and only verified contacts
|
||||
* Only verified contacts
|
||||
* as determined by dc_contact_is_verified()
|
||||
* can be added to protected chats.
|
||||
*
|
||||
* Protected chats are created using dc_create_group_chat()
|
||||
* by setting the 'protect' parameter to 1.
|
||||
* 1:1 chats become protected or unprotected automatically
|
||||
* if `verified_one_on_one_chats` setting is enabled.
|
||||
*
|
||||
* UI should display a green checkmark
|
||||
* in the chat title,
|
||||
* in the chatlist item
|
||||
* and in the chat profile
|
||||
* if chat protection is enabled.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
@@ -3869,6 +3853,8 @@ int dc_chat_is_encrypted (const dc_chat_t *chat);
|
||||
*
|
||||
* The UI should let the user confirm that this is OK with a message like
|
||||
* `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
|
||||
*
|
||||
* @deprecated 2025-07 chats protection cannot break any longer
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return 1=chat protection broken, 0=otherwise.
|
||||
@@ -5267,20 +5253,14 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Check if the contact
|
||||
* can be added to verified chats,
|
||||
* i.e. has a verified key
|
||||
* and Autocrypt key matches the verified key.
|
||||
* can be added to protected chats.
|
||||
*
|
||||
* If contact is verified
|
||||
* UI should display green checkmark after the contact name
|
||||
* in contact list items,
|
||||
* in chat member list items
|
||||
* and in profiles if no chat with the contact exist (otherwise, use dc_chat_is_protected()).
|
||||
* See dc_contact_get_verifier_id() for a guidance how to display these information.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return 0: contact is not verified.
|
||||
* 2: SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown.
|
||||
* 2: SELF and contact have verified their fingerprints in both directions.
|
||||
*/
|
||||
int dc_contact_is_verified (dc_contact_t* contact);
|
||||
|
||||
@@ -5311,16 +5291,22 @@ int dc_contact_is_key_contact (dc_contact_t* contact);
|
||||
/**
|
||||
* Return the contact ID that verified a contact.
|
||||
*
|
||||
* If the function returns non-zero result,
|
||||
* display green checkmark in the profile and "Introduced by ..." line
|
||||
* with the name and address of the contact
|
||||
* formatted by dc_contact_get_name_n_addr.
|
||||
* As verifier may be unknown,
|
||||
* use dc_contact_is_verified() to check if a contact can be added to a protected chat.
|
||||
*
|
||||
* If this function returns a verifier,
|
||||
* this does not necessarily mean
|
||||
* you can add the contact to verified chats.
|
||||
* Use dc_contact_is_verified() to check
|
||||
* if a contact can be added to a verified chat instead.
|
||||
* UI should display the information in the contact's profile as follows:
|
||||
*
|
||||
* - If dc_contact_get_verifier_id() != 0,
|
||||
* display text "Introduced by ..."
|
||||
* with the name and address of the contact
|
||||
* formatted by dc_contact_get_name_n_addr().
|
||||
* Prefix the text by a green checkmark.
|
||||
*
|
||||
* - If dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() != 0,
|
||||
* display "Introduced" prefixed by a green checkmark.
|
||||
*
|
||||
* - if dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() == 0,
|
||||
* display nothing
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
@@ -6386,7 +6372,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/**
|
||||
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
* Or the verify state of a chat has changed.
|
||||
* See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
|
||||
* and dc_remove_contact_from_chat().
|
||||
*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.3.0"
|
||||
version = "2.8.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -21,15 +21,16 @@ pub struct FullChat {
|
||||
|
||||
/// True if the chat is protected.
|
||||
///
|
||||
/// UI should display a green checkmark
|
||||
/// in the chat title,
|
||||
/// in the chat profile title and
|
||||
/// in the chatlist item
|
||||
/// if chat protection is enabled.
|
||||
/// UI should also display a green checkmark
|
||||
/// in the contact profile
|
||||
/// if 1:1 chat with this contact exists and is protected.
|
||||
/// Only verified contacts
|
||||
/// as determined by [`ContactObject::is_verified`] / `Contact.isVerified`
|
||||
/// can be added to protected chats.
|
||||
///
|
||||
/// Protected chats are created using [`create_group_chat`] / `createGroupChat()`
|
||||
/// by setting the 'protect' parameter to true.
|
||||
///
|
||||
/// [`create_group_chat`]: crate::api::CommandApi::create_group_chat
|
||||
is_protected: bool,
|
||||
|
||||
/// True if the chat is encrypted.
|
||||
/// This means that all messages in the chat are encrypted,
|
||||
/// and all contacts in the chat are "key-contacts",
|
||||
@@ -70,7 +71,9 @@ pub struct FullChat {
|
||||
fresh_message_counter: usize,
|
||||
// is_group - please check over chat.type in frontend instead
|
||||
is_contact_request: bool,
|
||||
/// Deprecated 2025-07. Chats protection cannot break any longer.
|
||||
is_protection_broken: bool,
|
||||
|
||||
is_device_chat: bool,
|
||||
self_in_group: bool,
|
||||
is_muted: bool,
|
||||
@@ -215,7 +218,9 @@ pub struct BasicChat {
|
||||
is_self_talk: bool,
|
||||
color: String,
|
||||
is_contact_request: bool,
|
||||
/// Deprecated 2025-07. Chats protection cannot break any longer.
|
||||
is_protection_broken: bool,
|
||||
|
||||
is_device_chat: bool,
|
||||
is_muted: bool,
|
||||
}
|
||||
|
||||
@@ -31,13 +31,11 @@ pub struct ContactObject {
|
||||
/// e.g. if we just scanned the fingerprint from a QR code.
|
||||
e2ee_avail: bool,
|
||||
|
||||
/// True if the contact can be added to verified groups.
|
||||
/// True if the contact
|
||||
/// can be added to protected chats
|
||||
/// because SELF and contact have verified their fingerprints in both directions.
|
||||
///
|
||||
/// If this is true
|
||||
/// UI should display green checkmark after the contact name
|
||||
/// in contact list items,
|
||||
/// in chat member list items
|
||||
/// and in profiles if no chat with the contact exist.
|
||||
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
|
||||
is_verified: bool,
|
||||
|
||||
/// True if the contact profile title should have a green checkmark.
|
||||
@@ -46,12 +44,29 @@ pub struct ContactObject {
|
||||
/// or will have a green checkmark if created.
|
||||
is_profile_verified: bool,
|
||||
|
||||
/// The ID of the contact that verified this contact.
|
||||
/// The contact ID that verified a contact.
|
||||
///
|
||||
/// If this is present,
|
||||
/// display a green checkmark and "Introduced by ..."
|
||||
/// string followed by the verifier contact name and address
|
||||
/// in the contact profile.
|
||||
/// As verifier may be unknown,
|
||||
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
|
||||
///
|
||||
/// UI should display the information in the contact's profile as follows:
|
||||
///
|
||||
/// - If `verifierId` != 0,
|
||||
/// display text "Introduced by ..."
|
||||
/// with the name and address of the contact
|
||||
/// formatted by `name_and_addr`/`nameAndAddr`.
|
||||
/// Prefix the text by a green checkmark.
|
||||
///
|
||||
/// - If `verifierId` == 0 and `isVerified` != 0,
|
||||
/// display "Introduced" prefixed by a green checkmark.
|
||||
///
|
||||
/// - if `verifierId` == 0 and `isVerified` == 0,
|
||||
/// display nothing
|
||||
///
|
||||
/// This contains the contact ID of the verifier.
|
||||
/// If it is `DC_CONTACT_ID_SELF`, we verified the contact ourself.
|
||||
/// If it is None/Null, we don't have verifier information or
|
||||
/// the contact is not verified.
|
||||
verifier_id: Option<u32>,
|
||||
|
||||
/// the contact's last seen timestamp
|
||||
|
||||
@@ -224,7 +224,6 @@ pub enum EventType {
|
||||
},
|
||||
|
||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
/// Or the verify state of a chat has changed.
|
||||
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
||||
/// and removeContactFromChat().
|
||||
///
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.3.0"
|
||||
"version": "2.8.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.3.0"
|
||||
version = "2.8.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.3.0"
|
||||
version = "2.8.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.3.0"
|
||||
version = "2.8.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.3.0"
|
||||
"version": "2.8.0"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ skip = [
|
||||
{ name = "lru", version = "0.12.3" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
{ name = "nom", version = "7.1.3" },
|
||||
{ name = "proc-macro-crate", version = "2.0.0" },
|
||||
{ name = "rand_chacha", version = "0.3.1" },
|
||||
{ name = "rand_core", version = "0.6.4" },
|
||||
{ name = "rand", version = "0.8.5" },
|
||||
@@ -49,7 +48,6 @@ skip = [
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "thiserror-impl", version = "1.0.69" },
|
||||
{ name = "thiserror", version = "1.0.69" },
|
||||
{ name = "toml_edit", version = "0.20.7" },
|
||||
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
|
||||
{ name = "windows" },
|
||||
{ name = "windows_aarch64_gnullvm" },
|
||||
@@ -67,7 +65,6 @@ skip = [
|
||||
{ name = "windows_x86_64_gnu" },
|
||||
{ name = "windows_x86_64_gnullvm" },
|
||||
{ name = "windows_x86_64_msvc" },
|
||||
{ name = "winnow", version = "0.5.40" },
|
||||
{ name = "zerocopy", version = "0.7.32" },
|
||||
]
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.3.0"
|
||||
version = "2.8.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-07-19
|
||||
2025-07-28
|
||||
61
src/chat.rs
61
src/chat.rs
@@ -94,14 +94,12 @@ pub enum ProtectionStatus {
|
||||
///
|
||||
/// All members of the chat must be verified.
|
||||
Protected = 1,
|
||||
// `2` was never used as a value.
|
||||
|
||||
/// The chat was protected, but now a new message came in
|
||||
/// which was not encrypted / signed correctly.
|
||||
/// The user has to confirm that this is OK.
|
||||
///
|
||||
/// We only do this in 1:1 chats; in group chats, the chat just
|
||||
/// stays protected.
|
||||
ProtectionBroken = 3, // `2` was never used as a value.
|
||||
// Chats don't break in Core v2 anymore. Chats with broken protection existing before the
|
||||
// key-contacts migration are treated as `Unprotected`.
|
||||
//
|
||||
// ProtectionBroken = 3,
|
||||
}
|
||||
|
||||
/// The reason why messages cannot be sent to the chat.
|
||||
@@ -118,10 +116,6 @@ pub(crate) enum CantSendReason {
|
||||
/// The chat is a contact request, it needs to be accepted before sending a message.
|
||||
ContactRequest,
|
||||
|
||||
/// The chat was protected, but now a new message came in
|
||||
/// which was not encrypted / signed correctly.
|
||||
ProtectionBroken,
|
||||
|
||||
/// Mailing list without known List-Post header.
|
||||
ReadOnlyMailingList,
|
||||
|
||||
@@ -144,10 +138,6 @@ impl fmt::Display for CantSendReason {
|
||||
f,
|
||||
"contact request chat should be accepted before sending messages"
|
||||
),
|
||||
Self::ProtectionBroken => write!(
|
||||
f,
|
||||
"accept that the encryption isn't verified anymore before sending messages"
|
||||
),
|
||||
Self::ReadOnlyMailingList => {
|
||||
write!(f, "mailing list does not have a know post address")
|
||||
}
|
||||
@@ -479,16 +469,6 @@ impl ChatId {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
|
||||
match chat.typ {
|
||||
Chattype::Single
|
||||
if chat.blocked == Blocked::Not
|
||||
&& chat.protected == ProtectionStatus::ProtectionBroken =>
|
||||
{
|
||||
// The protection was broken, then the user clicked 'Accept'/'OK',
|
||||
// so, now we want to set the status to Unprotected again:
|
||||
chat.id
|
||||
.inner_set_protection(context, ProtectionStatus::Unprotected)
|
||||
.await?;
|
||||
}
|
||||
Chattype::Single | Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast => {
|
||||
// User has "created a chat" with all these contacts.
|
||||
//
|
||||
@@ -545,7 +525,7 @@ impl ChatId {
|
||||
| Chattype::InBroadcast => {}
|
||||
Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
|
||||
},
|
||||
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
|
||||
ProtectionStatus::Unprotected => {}
|
||||
};
|
||||
|
||||
context
|
||||
@@ -588,7 +568,6 @@ impl ChatId {
|
||||
let cmd = match protect {
|
||||
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
|
||||
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
|
||||
ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
|
||||
};
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
@@ -1700,12 +1679,6 @@ impl Chat {
|
||||
return Ok(Some(reason));
|
||||
}
|
||||
}
|
||||
if self.is_protection_broken() {
|
||||
let reason = ProtectionBroken;
|
||||
if !skip_fn(&reason) {
|
||||
return Ok(Some(reason));
|
||||
}
|
||||
}
|
||||
if self.is_mailing_list() && self.get_mailinglist_addr().is_none_or_empty() {
|
||||
let reason = ReadOnlyMailingList;
|
||||
if !skip_fn(&reason) {
|
||||
@@ -1935,25 +1908,9 @@ impl Chat {
|
||||
Ok(is_encrypted)
|
||||
}
|
||||
|
||||
/// Returns true if the chat was protected, and then an incoming message broke this protection.
|
||||
///
|
||||
/// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
|
||||
/// otherwise it will return false for all chats.
|
||||
///
|
||||
/// 1:1 chats are automatically set as protected when a contact is verified.
|
||||
/// When a message comes in that is not encrypted / signed correctly,
|
||||
/// the chat is automatically set as unprotected again.
|
||||
/// `is_protection_broken()` will return true until `chat_id.accept()` is called.
|
||||
///
|
||||
/// The UI should let the user confirm that this is OK with a message like
|
||||
/// `Bob sent a message from another device. Tap to learn more`
|
||||
/// and then call `chat_id.accept()`.
|
||||
/// Deprecated 2025-07. Returns false.
|
||||
pub fn is_protection_broken(&self) -> bool {
|
||||
match self.protected {
|
||||
ProtectionStatus::Protected => false,
|
||||
ProtectionStatus::Unprotected => false,
|
||||
ProtectionStatus::ProtectionBroken => true,
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns true if location streaming is enabled in the chat.
|
||||
@@ -2947,7 +2904,7 @@ async fn prepare_send_msg(
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
|
||||
let skip_fn = |reason: &CantSendReason| match reason {
|
||||
CantSendReason::ProtectionBroken | CantSendReason::ContactRequest => {
|
||||
CantSendReason::ContactRequest => {
|
||||
// Allow securejoin messages, they are supposed to repair the verification.
|
||||
// If the chat is a contact request, let the user accept it later.
|
||||
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
|
||||
@@ -245,9 +245,6 @@ impl Chatlist {
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
};
|
||||
// Return ProtectionBroken chats also, as that may happen to a verified chat at any
|
||||
// time. It may be confusing if a chat that is normally in the list disappears
|
||||
// suddenly. The UI need to deal with that case anyway.
|
||||
context.sql.query_map(
|
||||
"SELECT c.id, c.type, c.param, m.id
|
||||
FROM chats c
|
||||
|
||||
@@ -417,7 +417,7 @@ pub enum Config {
|
||||
#[strum(props(default = "172800"))]
|
||||
GossipPeriod,
|
||||
|
||||
/// Feature flag for verified 1:1 chats; the UI should set it
|
||||
/// Deprecated 2025-07. Feature flag for verified 1:1 chats; the UI should set it
|
||||
/// to 1 if it supports verified 1:1 chats.
|
||||
/// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
|
||||
/// and when the key changes, an info message is posted into the chat.
|
||||
|
||||
@@ -333,6 +333,15 @@ impl Default for RunningState {
|
||||
/// about the context on top of the information here.
|
||||
pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
let mut res = BTreeMap::new();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
res.insert(
|
||||
"debug_assertions",
|
||||
"On - DO NOT RELEASE THIS BUILD".to_string(),
|
||||
);
|
||||
#[cfg(not(debug_assertions))]
|
||||
res.insert("debug_assertions", "Off".to_string());
|
||||
|
||||
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
|
||||
res.insert("sqlite_version", rusqlite::version().to_string());
|
||||
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
|
||||
@@ -1043,7 +1052,7 @@ impl Context {
|
||||
self.get_config_int(Config::GossipPeriod).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"verified_one_on_one_chats",
|
||||
"verified_one_on_one_chats", // deprecated 2025-07
|
||||
self.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
.await?
|
||||
.to_string(),
|
||||
@@ -1078,7 +1087,6 @@ impl Context {
|
||||
#[derive(Default)]
|
||||
struct ChatNumbers {
|
||||
protected: u32,
|
||||
protection_broken: u32,
|
||||
opportunistic_dc: u32,
|
||||
opportunistic_mua: u32,
|
||||
unencrypted_dc: u32,
|
||||
@@ -1114,7 +1122,6 @@ impl Context {
|
||||
|
||||
// how many of the chats active in the last months are:
|
||||
// - protected
|
||||
// - protection-broken
|
||||
// - opportunistic-encrypted and the contact uses Delta Chat
|
||||
// - opportunistic-encrypted and the contact uses a classical MUA
|
||||
// - unencrypted and the contact uses Delta Chat
|
||||
@@ -1157,8 +1164,6 @@ impl Context {
|
||||
|
||||
if protected == ProtectionStatus::Protected {
|
||||
chats.protected += 1;
|
||||
} else if protected == ProtectionStatus::ProtectionBroken {
|
||||
chats.protection_broken += 1;
|
||||
} else if encrypted {
|
||||
if is_dc_message {
|
||||
chats.opportunistic_dc += 1;
|
||||
@@ -1176,7 +1181,6 @@ impl Context {
|
||||
)
|
||||
.await?;
|
||||
res += &format!("chats_protected {}\n", chats.protected);
|
||||
res += &format!("chats_protection_broken {}\n", chats.protection_broken);
|
||||
res += &format!("chats_opportunistic_dc {}\n", chats.opportunistic_dc);
|
||||
res += &format!("chats_opportunistic_mua {}\n", chats.opportunistic_mua);
|
||||
res += &format!("chats_unencrypted_dc {}\n", chats.unencrypted_dc);
|
||||
|
||||
24
src/imap.rs
24
src/imap.rs
@@ -17,7 +17,7 @@ use anyhow::{Context as _, Result, bail, ensure, format_err};
|
||||
use async_channel::{self, Receiver, Sender};
|
||||
use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
|
||||
use deltachat_contact_tools::ContactAddress;
|
||||
use futures::{FutureExt as _, StreamExt, TryStreamExt};
|
||||
use futures::{FutureExt as _, TryStreamExt};
|
||||
use futures_lite::FutureExt;
|
||||
use num_traits::FromPrimitive;
|
||||
use rand::Rng;
|
||||
@@ -1384,14 +1384,15 @@ impl Session {
|
||||
|
||||
// Try to find a requested UID in returned FETCH responses.
|
||||
while fetch_response.is_none() {
|
||||
let Some(next_fetch_response) = fetch_responses.next().await else {
|
||||
let Some(next_fetch_response) = fetch_responses
|
||||
.try_next()
|
||||
.await
|
||||
.context("Failed to process IMAP FETCH result")?
|
||||
else {
|
||||
// No more FETCH responses received from the server.
|
||||
break;
|
||||
};
|
||||
|
||||
let next_fetch_response =
|
||||
next_fetch_response.context("Failed to process IMAP FETCH result")?;
|
||||
|
||||
if let Some(next_uid) = next_fetch_response.uid {
|
||||
if next_uid == request_uid {
|
||||
fetch_response = Some(next_fetch_response);
|
||||
@@ -1491,7 +1492,16 @@ impl Session {
|
||||
|
||||
// If we don't process the whole response, IMAP client is left in a broken state where
|
||||
// it will try to process the rest of response as the next response.
|
||||
while fetch_responses.next().await.is_some() {}
|
||||
//
|
||||
// Make sure to not ignore the errors, because
|
||||
// if connection times out, it will return
|
||||
// infinite stream of `Some(Err(_))` results.
|
||||
while fetch_responses
|
||||
.try_next()
|
||||
.await
|
||||
.context("Failed to drain FETCH responses")?
|
||||
.is_some()
|
||||
{}
|
||||
|
||||
if count != request_uids.len() {
|
||||
warn!(
|
||||
@@ -1688,7 +1698,7 @@ impl Session {
|
||||
.uid_store(uid_set, &query)
|
||||
.await
|
||||
.with_context(|| format!("IMAP failed to store: ({uid_set}, {query})"))?;
|
||||
while let Some(_response) = responses.next().await {
|
||||
while let Some(_response) = responses.try_next().await? {
|
||||
// Read all the responses
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -216,8 +216,8 @@ impl Client {
|
||||
let mut client = Client::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
@@ -231,8 +231,8 @@ impl Client {
|
||||
let mut client = Client::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
@@ -253,8 +253,8 @@ impl Client {
|
||||
let mut client = async_imap::Client::new(buffered_tcp_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
client
|
||||
.run_command_and_check_ok("STARTTLS", None)
|
||||
.await
|
||||
@@ -287,8 +287,8 @@ impl Client {
|
||||
let mut client = Client::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
@@ -304,8 +304,8 @@ impl Client {
|
||||
let mut client = Client::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
@@ -325,8 +325,8 @@ impl Client {
|
||||
let mut client = ImapClient::new(buffered_proxy_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
client
|
||||
.run_command_and_check_ok("STARTTLS", None)
|
||||
.await
|
||||
|
||||
@@ -804,7 +804,7 @@ async fn export_database(
|
||||
"UPDATE backup.config SET value='0' WHERE keyname='verified_one_on_one_chats';",
|
||||
[],
|
||||
)
|
||||
.ok(); // If verified_one_on_one_chats was not set, this errors, which we ignore
|
||||
.ok(); // Deprecated 2025-07. If verified_one_on_one_chats was not set, this errors, which we ignore
|
||||
conn.execute("DETACH DATABASE backup", [])
|
||||
.context("failed to detach backup database")?;
|
||||
res?;
|
||||
|
||||
@@ -5,7 +5,9 @@ use std::io::Cursor;
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use base64::Engine as _;
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
use deltachat_contact_tools::sanitize_bidi_characters;
|
||||
use iroh_gossip::proto::TopicId;
|
||||
use mail_builder::headers::HeaderType;
|
||||
use mail_builder::headers::address::{Address, EmailAddress};
|
||||
use mail_builder::mime::MimePart;
|
||||
@@ -22,14 +24,14 @@ use crate::context::Context;
|
||||
use crate::e2ee::EncryptHelper;
|
||||
use crate::ensure_and_debug_assert;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::key::{DcKey, SignedPublicKey, self_fingerprint};
|
||||
use crate::location;
|
||||
use crate::log::{info, warn};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{SystemMessage, is_hidden};
|
||||
use crate::param::Param;
|
||||
use crate::peer_channels::create_iroh_header;
|
||||
use crate::peer_channels::{create_iroh_header, get_iroh_topic_for_msg};
|
||||
use crate::simplify::escape_message_footer_marks;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{
|
||||
@@ -140,6 +142,9 @@ pub struct MimeFactory {
|
||||
|
||||
/// True if the avatar should be attached.
|
||||
pub attach_selfavatar: bool,
|
||||
|
||||
/// This field is used to sustain the topic id of webxdcs needed for peer channels.
|
||||
webxdc_topic: Option<TopicId>,
|
||||
}
|
||||
|
||||
/// Result of rendering a message, ready to be submitted to a send job.
|
||||
@@ -248,6 +253,10 @@ impl MimeFactory {
|
||||
let mut missing_key_addresses = BTreeSet::new();
|
||||
context
|
||||
.sql
|
||||
// Sort recipients by `add_timestamp DESC` so that if the group is large and there
|
||||
// are multiple SMTP messages, a newly added member receives the member addition
|
||||
// message earlier and has gossiped keys of other members (otherwise the new member
|
||||
// may receive messages from other members earlier and fail to verify them).
|
||||
.query_map(
|
||||
"SELECT
|
||||
c.authname,
|
||||
@@ -261,7 +270,8 @@ impl MimeFactory {
|
||||
LEFT JOIN contacts c ON cc.contact_id=c.id
|
||||
LEFT JOIN public_keys k ON k.fingerprint=c.fingerprint
|
||||
WHERE cc.chat_id=?
|
||||
AND (cc.contact_id>9 OR (cc.contact_id=1 AND ?))",
|
||||
AND (cc.contact_id>9 OR (cc.contact_id=1 AND ?))
|
||||
ORDER BY cc.add_timestamp DESC",
|
||||
(msg.chat_id, chat.typ == Chattype::Group),
|
||||
|row| {
|
||||
let authname: String = row.get(0)?;
|
||||
@@ -460,7 +470,7 @@ impl MimeFactory {
|
||||
past_members.len(),
|
||||
member_timestamps.len(),
|
||||
);
|
||||
|
||||
let webxdc_topic = get_iroh_topic_for_msg(context, msg.id).await?;
|
||||
let factory = MimeFactory {
|
||||
from_addr,
|
||||
from_displayname,
|
||||
@@ -480,6 +490,7 @@ impl MimeFactory {
|
||||
last_added_location_id: None,
|
||||
sync_ids_to_delete: None,
|
||||
attach_selfavatar,
|
||||
webxdc_topic,
|
||||
};
|
||||
Ok(factory)
|
||||
}
|
||||
@@ -527,6 +538,7 @@ impl MimeFactory {
|
||||
last_added_location_id: None,
|
||||
sync_ids_to_delete: None,
|
||||
attach_selfavatar: false,
|
||||
webxdc_topic: None,
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
@@ -1510,7 +1522,7 @@ impl MimeFactory {
|
||||
}
|
||||
SystemMessage::IrohNodeAddr => {
|
||||
headers.push((
|
||||
"Iroh-Node-Addr",
|
||||
HeaderDef::IrohNodeAddr.into(),
|
||||
mail_builder::headers::text::Text::new(serde_json::to_string(
|
||||
&context
|
||||
.get_or_try_init_peer_channel()
|
||||
@@ -1691,10 +1703,13 @@ impl MimeFactory {
|
||||
let json = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
parts.push(context.build_status_update_part(json));
|
||||
} else if msg.viewtype == Viewtype::Webxdc {
|
||||
let topic = self
|
||||
.webxdc_topic
|
||||
.map(|top| BASE32_NOPAD.encode(top.as_bytes()).to_ascii_lowercase())
|
||||
.unwrap_or(create_iroh_header(context, msg.id).await?);
|
||||
headers.push((
|
||||
"Iroh-Gossip-Topic",
|
||||
mail_builder::headers::raw::Raw::new(create_iroh_header(context, msg.id).await?)
|
||||
.into(),
|
||||
HeaderDef::IrohGossipTopic.get_headername(),
|
||||
mail_builder::headers::raw::Raw::new(topic).into(),
|
||||
));
|
||||
if let (Some(json), _) = context
|
||||
.render_webxdc_status_update_object(
|
||||
|
||||
@@ -2,6 +2,7 @@ use deltachat_contact_tools::ContactAddress;
|
||||
use mail_builder::headers::Header;
|
||||
use mailparse::{MailHeaderMap, addrparse_header};
|
||||
use std::str;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{
|
||||
@@ -16,6 +17,7 @@ use crate::message;
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager, get_chat_msg};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
fn render_email_address(display_name: &str, addr: &str) -> String {
|
||||
let mut output = Vec::<u8>::new();
|
||||
@@ -867,6 +869,43 @@ async fn test_dont_remove_self() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_new_member_is_first_recipient() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let charlie_id = alice.add_or_lookup_contact_id(charlie).await;
|
||||
|
||||
let group = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "Group", &[bob])
|
||||
.await;
|
||||
alice.send_text(group, "Hi! I created a group.").await;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(60));
|
||||
add_contact_to_chat(alice, group, charlie_id).await?;
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
assert!(
|
||||
sent_msg
|
||||
.recipients
|
||||
.starts_with(&charlie.get_config(Config::Addr).await?.unwrap())
|
||||
);
|
||||
|
||||
remove_contact_from_chat(alice, group, bob_id).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
SystemTime::shift(Duration::from_secs(60));
|
||||
add_contact_to_chat(alice, group, bob_id).await?;
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
assert!(
|
||||
sent_msg
|
||||
.recipients
|
||||
.starts_with(&bob.get_config(Config::Addr).await?.unwrap())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test: mimefactory should never create an empty to header,
|
||||
/// also not if the Selftalk parameter is missing
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -48,13 +48,13 @@ use crate::mimeparser::SystemMessage;
|
||||
const PUBLIC_KEY_LENGTH: usize = 32;
|
||||
const PUBLIC_KEY_STUB: &[u8] = "static_string".as_bytes();
|
||||
|
||||
/// Store iroh peer channels for the context.
|
||||
/// Store Iroh peer channels for the context.
|
||||
#[derive(Debug)]
|
||||
pub struct Iroh {
|
||||
/// iroh router needed for iroh peer channels.
|
||||
/// Iroh router needed for Iroh peer channels.
|
||||
pub(crate) router: iroh::protocol::Router,
|
||||
|
||||
/// [Gossip] needed for iroh peer channels.
|
||||
/// [Gossip] needed for Iroh peer channels.
|
||||
pub(crate) gossip: Gossip,
|
||||
|
||||
/// Sequence numbers for gossip channels.
|
||||
@@ -109,7 +109,7 @@ impl Iroh {
|
||||
|
||||
info!(
|
||||
ctx,
|
||||
"IROH_REALTIME: Joining gossip with peers: {:?}", node_ids,
|
||||
"IROH_REALTIME: Joining gossip {topic} with peers: {:?}.", node_ids,
|
||||
);
|
||||
|
||||
// Inform iroh of potentially new node addresses
|
||||
@@ -138,17 +138,11 @@ impl Iroh {
|
||||
Ok(Some(join_rx))
|
||||
}
|
||||
|
||||
/// Add gossip peers to realtime channel if it is already active.
|
||||
pub async fn maybe_add_gossip_peers(&self, topic: TopicId, peers: Vec<NodeAddr>) -> Result<()> {
|
||||
/// Add gossip peer to realtime channel if it is already active.
|
||||
pub async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: NodeAddr) -> Result<()> {
|
||||
if self.iroh_channels.read().await.get(&topic).is_some() {
|
||||
for peer in &peers {
|
||||
self.router.endpoint().add_node_addr(peer.clone())?;
|
||||
}
|
||||
|
||||
self.gossip.subscribe_with_opts(
|
||||
topic,
|
||||
JoinOptions::with_bootstrap(peers.into_iter().map(|peer| peer.node_id)),
|
||||
);
|
||||
self.router.endpoint().add_node_addr(peer.clone())?;
|
||||
self.gossip.subscribe(topic, vec![peer.node_id])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -316,6 +310,17 @@ impl Context {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: NodeAddr) -> Result<()> {
|
||||
if let Some(iroh) = &*self.iroh.read().await {
|
||||
info!(
|
||||
self,
|
||||
"Adding (maybe existing) peer with id {} to {topic}.", peer.node_id
|
||||
);
|
||||
iroh.maybe_add_gossip_peer(topic, peer).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache a peers [NodeId] for one topic.
|
||||
@@ -348,13 +353,14 @@ pub async fn add_gossip_peer_from_header(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Adding iroh peer with address {node_addr:?} to the topic of {instance_id}."
|
||||
);
|
||||
let node_addr =
|
||||
serde_json::from_str::<NodeAddr>(node_addr).context("Failed to parse node address")?;
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Adding iroh peer with node id {} to the topic of {instance_id}.", node_addr.node_id
|
||||
);
|
||||
|
||||
context.emit_event(EventType::WebxdcRealtimeAdvertisementReceived {
|
||||
msg_id: instance_id,
|
||||
});
|
||||
@@ -371,8 +377,7 @@ pub async fn add_gossip_peer_from_header(
|
||||
let relay_server = node_addr.relay_url().map(|relay| relay.as_str());
|
||||
iroh_add_peer_for_topic(context, instance_id, topic, node_id, relay_server).await?;
|
||||
|
||||
let iroh = context.get_or_try_init_peer_channel().await?;
|
||||
iroh.maybe_add_gossip_peers(topic, vec![node_addr]).await?;
|
||||
context.maybe_add_gossip_peer(topic, node_addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -555,9 +560,9 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
EventType,
|
||||
chat::send_msg,
|
||||
chat::{self, ChatId, ProtectionStatus, add_contact_to_chat, resend_msgs, send_msg},
|
||||
message::{Message, Viewtype},
|
||||
test_utils::TestContextManager,
|
||||
test_utils::{TestContext, TestContextManager},
|
||||
};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -924,8 +929,8 @@ mod tests {
|
||||
let alice = &mut tcm.alice().await;
|
||||
let bob = &mut tcm.bob().await;
|
||||
|
||||
// Alice sends webxdc to bob
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
let chat = alice.create_chat(bob).await.id;
|
||||
|
||||
let mut instance = Message::new(Viewtype::File);
|
||||
instance
|
||||
.set_file_from_bytes(
|
||||
@@ -935,7 +940,100 @@ mod tests {
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
send_msg(alice, alice_chat.id, &mut instance).await.unwrap();
|
||||
connect_alice_bob(alice, chat, &mut instance, bob).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_webxdc_resend() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &mut tcm.alice().await;
|
||||
let bob = &mut tcm.bob().await;
|
||||
let group = chat::create_group_chat(alice, ProtectionStatus::Unprotected, "group chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Alice sends webxdc to bob
|
||||
let mut instance = Message::new(Viewtype::File);
|
||||
instance
|
||||
.set_file_from_bytes(
|
||||
alice,
|
||||
"minimal.xdc",
|
||||
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
add_contact_to_chat(alice, group, alice.add_or_lookup_contact_id(bob).await)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
connect_alice_bob(alice, group, &mut instance, bob).await;
|
||||
|
||||
// fiona joins late
|
||||
let fiona = &mut tcm.fiona().await;
|
||||
|
||||
add_contact_to_chat(alice, group, alice.add_or_lookup_contact_id(fiona).await)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
resend_msgs(alice, &[instance.id]).await.unwrap();
|
||||
let msg = alice.pop_sent_msg().await;
|
||||
let fiona_instance = fiona.recv_msg(&msg).await;
|
||||
fiona_instance.chat_id.accept(fiona).await.unwrap();
|
||||
assert!(fiona.ctx.iroh.read().await.is_none());
|
||||
|
||||
let fiona_connect_future = send_webxdc_realtime_advertisement(fiona, fiona_instance.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let fiona_advert = fiona.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&fiona_advert).await;
|
||||
|
||||
fiona_connect_future.await.unwrap();
|
||||
|
||||
let realtime_send_loop = async {
|
||||
// Keep sending in a loop because right after joining
|
||||
// Fiona may miss messages.
|
||||
loop {
|
||||
send_webxdc_realtime_data(alice, instance.id, b"alice -> bob & fiona".into())
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
};
|
||||
|
||||
let realtime_receive_loop = async {
|
||||
loop {
|
||||
let event = fiona.evtracker.recv().await.unwrap();
|
||||
if let EventType::WebxdcRealtimeData { data, .. } = event.typ {
|
||||
if data == b"alice -> bob & fiona" {
|
||||
break;
|
||||
} else {
|
||||
panic!(
|
||||
"Unexpected status update: {}",
|
||||
String::from_utf8_lossy(&data)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
tokio::select!(
|
||||
_ = realtime_send_loop => {
|
||||
panic!("Send loop should never finish");
|
||||
},
|
||||
_ = realtime_receive_loop => {
|
||||
return;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async fn connect_alice_bob(
|
||||
alice: &mut TestContext,
|
||||
alice_chat_id: ChatId,
|
||||
instance: &mut Message,
|
||||
bob: &mut TestContext,
|
||||
) {
|
||||
send_msg(alice, alice_chat_id, instance).await.unwrap();
|
||||
let alice_webxdc = alice.get_last_msg().await;
|
||||
|
||||
let webxdc = alice.pop_sent_msg().await;
|
||||
@@ -952,8 +1050,9 @@ mod tests {
|
||||
.unwrap();
|
||||
let alice_advertisement = alice.pop_sent_msg().await;
|
||||
|
||||
send_webxdc_realtime_advertisement(bob, bob_webxdc.id)
|
||||
let bob_advertisement_future = send_webxdc_realtime_advertisement(bob, bob_webxdc.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let bob_advertisement = bob.pop_sent_msg().await;
|
||||
|
||||
@@ -961,8 +1060,9 @@ mod tests {
|
||||
bob.recv_msg_trash(&alice_advertisement).await;
|
||||
alice.recv_msg_trash(&bob_advertisement).await;
|
||||
|
||||
eprintln!("Alice waits for connection");
|
||||
eprintln!("Alice and Bob wait for connection");
|
||||
alice_advertisement_future.await.unwrap();
|
||||
bob_advertisement_future.await.unwrap();
|
||||
|
||||
// Alice sends ephemeral message
|
||||
eprintln!("Sending ephemeral message");
|
||||
|
||||
@@ -1461,19 +1461,16 @@ async fn do_chat_assignment(
|
||||
chat.typ == Chattype::Single,
|
||||
"Chat {chat_id} is not Single",
|
||||
);
|
||||
let mut new_protection = match verified_encryption {
|
||||
let new_protection = match verified_encryption {
|
||||
VerifiedEncryption::Verified => ProtectionStatus::Protected,
|
||||
VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
|
||||
};
|
||||
|
||||
if chat.protected != ProtectionStatus::Unprotected
|
||||
&& new_protection == ProtectionStatus::Unprotected
|
||||
// `chat.protected` must be maintained regardless of the `Config::VerifiedOneOnOneChats`.
|
||||
// That's why the config is checked here, and not above.
|
||||
&& context.get_config_bool(Config::VerifiedOneOnOneChats).await?
|
||||
{
|
||||
new_protection = ProtectionStatus::ProtectionBroken;
|
||||
}
|
||||
ensure_and_debug_assert!(
|
||||
chat.protected == ProtectionStatus::Unprotected
|
||||
|| new_protection == ProtectionStatus::Protected,
|
||||
"Chat {chat_id} can't downgrade to Unprotected",
|
||||
);
|
||||
if chat.protected != new_protection {
|
||||
// The message itself will be sorted under the device message since the device
|
||||
// message is `MessageState::InNoticed`, which means that all following
|
||||
@@ -2149,7 +2146,7 @@ RETURNING id
|
||||
created_db_entries.push(row_id);
|
||||
}
|
||||
|
||||
// check all parts whether they contain a new logging webxdc
|
||||
// Maybe set logging xdc and add gossip topics for webxdcs.
|
||||
for (part, msg_id) in mime_parser.parts.iter().zip(&created_db_entries) {
|
||||
// check if any part contains a webxdc topic id
|
||||
if part.typ == Viewtype::Webxdc {
|
||||
@@ -3642,7 +3639,7 @@ async fn mark_recipients_as_verified(
|
||||
return Ok(());
|
||||
}
|
||||
for to_id in to_ids.iter().filter_map(|&x| x) {
|
||||
if to_id == ContactId::SELF {
|
||||
if to_id == ContactId::SELF || to_id == from_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3750,6 +3747,9 @@ async fn add_or_lookup_key_contacts_by_address_list(
|
||||
fp.hex()
|
||||
} else if let Some(key) = gossiped_keys.get(addr) {
|
||||
key.dc_fingerprint().hex()
|
||||
} else if context.is_self_addr(addr).await? {
|
||||
contact_ids.push(Some(ContactId::SELF));
|
||||
continue;
|
||||
} else {
|
||||
contact_ids.push(None);
|
||||
continue;
|
||||
|
||||
@@ -3050,6 +3050,39 @@ async fn test_auto_accept_protected_group_for_bots() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for a bug where receive_imf() failed
|
||||
/// if the sender of a verification-gossiping message
|
||||
/// also put itself into the To header.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_verification_gossip() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
mark_as_verified(alice, bob).await;
|
||||
mark_as_verified(bob, alice).await;
|
||||
|
||||
// This is message sent by Alice with verified encryption
|
||||
// that gossips Fiona's verification,
|
||||
// and for some reason, Alice also put herself into the To: header.
|
||||
let imf_raw =
|
||||
include_bytes!("../../test-data/message/verification-gossip-also-sent-to-from.eml");
|
||||
|
||||
// The regression test is that receive_imf() doesn't panic:
|
||||
let msg = receive_imf(bob, imf_raw, false).await?.unwrap();
|
||||
let msg = Message::load_from_db(bob, msg.msg_ids[0]).await?;
|
||||
assert_eq!(msg.text, "Hello!");
|
||||
assert!(
|
||||
bob.add_or_lookup_contact(fiona)
|
||||
.await
|
||||
.is_verified(bob)
|
||||
.await?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_bot_accepts_another_group_after_qr_scan() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -5269,3 +5302,48 @@ async fn test_outgoing_unencrypted_chat_assignment() {
|
||||
let chat = alice.create_email_chat(bob).await;
|
||||
assert_eq!(received.chat_id, chat.id);
|
||||
}
|
||||
|
||||
/// Tests Bob receiving a message from Alice
|
||||
/// in a new group she just created
|
||||
/// with only Alice and Bob.
|
||||
///
|
||||
/// The message has no Autocrypt-Gossip
|
||||
/// headers and no Chat-Group-Member-Fpr header.
|
||||
/// Such messages were created by core 1.159.5
|
||||
/// when Alice has bcc_self disabled
|
||||
/// as Chat-Group-Member-Fpr header did not exist
|
||||
/// yet and Autocrypt-Gossip is not sent
|
||||
/// as there is only one recipient
|
||||
/// (Bob, and no additional Alice devices).
|
||||
///
|
||||
/// Bob should recognize self as being
|
||||
/// a member of the group by just the e-mail address.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_group_introduction_no_gossip() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let received = receive_imf(
|
||||
bob,
|
||||
include_bytes!("../../test-data/message/group-introduction-no-gossip.eml"),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let msg = Message::load_from_db(bob, *received.msg_ids.last().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.text, "I created a group");
|
||||
let chat = Chat::load_from_db(bob, msg.chat_id).await.unwrap();
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
assert_eq!(chat.blocked, Blocked::Request);
|
||||
assert_eq!(chat.name, "Group!");
|
||||
assert!(chat.is_encrypted(bob).await.unwrap());
|
||||
|
||||
let contacts = get_chat_contacts(bob, chat.id).await?;
|
||||
assert_eq!(contacts.len(), 2);
|
||||
assert!(chat.is_self_in_chat(bob).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1251,6 +1251,16 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 133)?;
|
||||
if dbversion < migration_version {
|
||||
// Make `ProtectionBroken` chats `Unprotected`. Chats can't break anymore.
|
||||
sql.execute_migration(
|
||||
"UPDATE chats SET protected=0 WHERE protected!=1",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
@@ -1480,9 +1490,11 @@ fn migrate_key_contacts(
|
||||
} else if addr.is_empty() {
|
||||
Ok(default)
|
||||
} else {
|
||||
original_contact_id_from_addr_stmt
|
||||
Ok(original_contact_id_from_addr_stmt
|
||||
.query_row((addr,), |row| row.get(0))
|
||||
.with_context(|| format!("Original contact '{addr}' not found"))
|
||||
.optional()
|
||||
.with_context(|| format!("Original contact '{addr}' not found"))?
|
||||
.unwrap_or(default))
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1505,19 +1517,28 @@ fn migrate_key_contacts(
|
||||
};
|
||||
let new_id = insert_contact(verified_key).context("Step 13")?;
|
||||
verified_key_contacts.insert(original_id.try_into().context("Step 14")?, new_id);
|
||||
// If the original verifier is unknown, we represent this in the database
|
||||
// by putting `new_id` into the place of the verifier,
|
||||
// i.e. we say that this contact verified itself.
|
||||
let verifier_id =
|
||||
original_contact_id_from_addr(&verifier, new_id).context("Step 15")?;
|
||||
|
||||
let verifier_id = if addr_cmp(&verifier, &addr) {
|
||||
// Earlier versions of Delta Chat signalled a direct verification
|
||||
// by putting the contact's own address into the verifier column
|
||||
1 // 1=ContactId::SELF
|
||||
} else {
|
||||
// If the original verifier is unknown, we represent this in the database
|
||||
// by putting `new_id` into the place of the verifier,
|
||||
// i.e. we say that this contact verified itself.
|
||||
original_contact_id_from_addr(&verifier, new_id).context("Step 15")?
|
||||
};
|
||||
verifications.insert(new_id, verifier_id);
|
||||
|
||||
let Some(secondary_verified_key) = secondary_verified_key else {
|
||||
continue;
|
||||
};
|
||||
let new_id = insert_contact(secondary_verified_key).context("Step 16")?;
|
||||
let verifier_id: u32 =
|
||||
original_contact_id_from_addr(&secondary_verifier, new_id).context("Step 17")?;
|
||||
let verifier_id: u32 = if addr_cmp(&secondary_verifier, &addr) {
|
||||
1 // 1=ContactId::SELF
|
||||
} else {
|
||||
original_contact_id_from_addr(&secondary_verifier, new_id).context("Step 17")?
|
||||
};
|
||||
// Only use secondary verification if there is no primary verification:
|
||||
verifications.entry(new_id).or_insert(verifier_id);
|
||||
}
|
||||
@@ -1642,7 +1663,7 @@ fn migrate_key_contacts(
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.context("Step 26")?;
|
||||
|
||||
let mut keep_address_contacts = |reason: &str| {
|
||||
let mut keep_address_contacts = |reason: &str| -> Result<()> {
|
||||
info!(
|
||||
context,
|
||||
"Chat {chat_id} will be an unencrypted chat with contacts identified by email address: {reason}."
|
||||
@@ -1650,6 +1671,15 @@ fn migrate_key_contacts(
|
||||
for (m, _) in &old_members {
|
||||
orphaned_contacts.remove(m);
|
||||
}
|
||||
|
||||
// Unprotect this chat if it was protected.
|
||||
//
|
||||
// Otherwise we get protected chat with address-contact(s).
|
||||
transaction
|
||||
.execute("UPDATE chats SET protected=0 WHERE id=?", (chat_id,))
|
||||
.context("Step 26.0")?;
|
||||
|
||||
Ok(())
|
||||
};
|
||||
let old_and_new_members: Vec<(u32, bool, Option<u32>)> = match typ {
|
||||
// 1:1 chats retain:
|
||||
@@ -1669,19 +1699,13 @@ fn migrate_key_contacts(
|
||||
};
|
||||
|
||||
let Some(new_contact) = map_to_key_contact(old_member) else {
|
||||
keep_address_contacts("No peerstate, or peerstate in 'reset' state");
|
||||
keep_address_contacts("No peerstate, or peerstate in 'reset' state")?;
|
||||
continue;
|
||||
};
|
||||
if !addr_cmp_stmt
|
||||
.query_row((old_member, new_contact), |row| row.get::<_, bool>(0))?
|
||||
{
|
||||
// Unprotect this 1:1 chat if it was protected.
|
||||
//
|
||||
// Otherwise we get protected chat with address-contact.
|
||||
transaction
|
||||
.execute("UPDATE chats SET protected=0 WHERE id=?", (chat_id,))?;
|
||||
|
||||
keep_address_contacts("key contact has different email");
|
||||
keep_address_contacts("key contact has different email")?;
|
||||
continue;
|
||||
}
|
||||
vec![(*old_member, true, Some(new_contact))]
|
||||
@@ -1692,7 +1716,7 @@ fn migrate_key_contacts(
|
||||
if grpid.is_empty() {
|
||||
// Ad-hoc group that has empty Chat-Group-ID
|
||||
// because it was created in response to receiving a non-chat email.
|
||||
keep_address_contacts("Empty chat-Group-ID");
|
||||
keep_address_contacts("Empty chat-Group-ID")?;
|
||||
continue;
|
||||
} else if protected == 1 {
|
||||
old_members
|
||||
@@ -1711,7 +1735,7 @@ fn migrate_key_contacts(
|
||||
|
||||
// Mailinglist
|
||||
140 => {
|
||||
keep_address_contacts("Mailinglist");
|
||||
keep_address_contacts("Mailinglist")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1750,7 +1774,7 @@ fn migrate_key_contacts(
|
||||
transaction
|
||||
.execute("UPDATE chats SET grpid='' WHERE id=?", (chat_id,))
|
||||
.context("Step 26.1")?;
|
||||
keep_address_contacts("Group contains contact without peerstate");
|
||||
keep_address_contacts("Group contains contact without peerstate")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1292,7 +1292,7 @@ impl Context {
|
||||
contact_id: Option<ContactId>,
|
||||
) -> String {
|
||||
match protect {
|
||||
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {
|
||||
ProtectionStatus::Unprotected => {
|
||||
if let Some(contact_id) = contact_id {
|
||||
chat_protection_disabled(self, contact_id).await
|
||||
} else {
|
||||
|
||||
@@ -31,7 +31,7 @@ async fn test_verified_oneonone_chat_not_broken_by_device_change() {
|
||||
check_verified_oneonone_chat_protection_not_broken(false).await;
|
||||
}
|
||||
|
||||
async fn check_verified_oneonone_chat_protection_not_broken(broken_by_classical_email: bool) {
|
||||
async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email: bool) {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
@@ -42,7 +42,7 @@ async fn check_verified_oneonone_chat_protection_not_broken(broken_by_classical_
|
||||
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
|
||||
assert_verified(&bob, &alice, ProtectionStatus::Protected).await;
|
||||
|
||||
if broken_by_classical_email {
|
||||
if by_classical_email {
|
||||
tcm.section("Bob uses a classical MUA to send a message to Alice");
|
||||
receive_imf(
|
||||
&alice,
|
||||
@@ -58,7 +58,6 @@ async fn check_verified_oneonone_chat_protection_not_broken(broken_by_classical_
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
// Bob's contact is still verified, but the chat isn't marked as protected anymore
|
||||
let contact = alice.add_or_lookup_contact(&bob).await;
|
||||
assert_eq!(contact.is_verified(&alice).await.unwrap(), true);
|
||||
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
|
||||
@@ -199,7 +198,6 @@ async fn test_missing_key_reexecute_securejoin() -> Result<()> {
|
||||
let chat_id = tcm.execute_securejoin(bob, alice).await;
|
||||
let chat = Chat::load_from_db(bob, chat_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
assert!(!chat.is_protection_broken());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -213,7 +211,6 @@ async fn test_create_unverified_oneonone_chat() -> Result<()> {
|
||||
// A chat with an unknown contact should be created unprotected
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(!chat.is_protection_broken());
|
||||
|
||||
receive_imf(
|
||||
&alice,
|
||||
@@ -230,14 +227,12 @@ async fn test_create_unverified_oneonone_chat() -> Result<()> {
|
||||
// Now Bob is a known contact, new chats should still be created unprotected
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(!chat.is_protection_broken());
|
||||
|
||||
tcm.send_recv(&bob, &alice, "hi").await;
|
||||
chat.id.delete(&alice).await.unwrap();
|
||||
// Now we have a public key, new chats should still be created unprotected
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(!chat.is_protection_broken());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -525,7 +520,6 @@ async fn test_message_from_old_dc_setup() -> Result<()> {
|
||||
assert!(contact.is_verified(alice).await.unwrap());
|
||||
let chat = alice.get_chat(bob).await;
|
||||
assert!(chat.is_protected());
|
||||
assert_eq!(chat.is_protection_broken(), false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -812,19 +806,15 @@ async fn test_verified_chat_editor_reordering() -> Result<()> {
|
||||
// ============== Helper Functions ==============
|
||||
|
||||
async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) {
|
||||
if protected != ProtectionStatus::ProtectionBroken {
|
||||
let contact = this.add_or_lookup_contact(other).await;
|
||||
assert_eq!(contact.is_verified(this).await.unwrap(), true);
|
||||
}
|
||||
let contact = this.add_or_lookup_contact(other).await;
|
||||
assert_eq!(contact.is_verified(this).await.unwrap(), true);
|
||||
|
||||
let chat = this.get_chat(other).await;
|
||||
let (expect_protected, expect_broken) = match protected {
|
||||
ProtectionStatus::Unprotected => (false, false),
|
||||
ProtectionStatus::Protected => (true, false),
|
||||
ProtectionStatus::ProtectionBroken => (false, true),
|
||||
};
|
||||
assert_eq!(chat.is_protected(), expect_protected);
|
||||
assert_eq!(chat.is_protection_broken(), expect_broken);
|
||||
assert_eq!(
|
||||
chat.is_protected(),
|
||||
protected == ProtectionStatus::Protected
|
||||
);
|
||||
assert_eq!(chat.is_protection_broken(), false);
|
||||
}
|
||||
|
||||
async fn enable_verified_oneonone_chats(test_contexts: &[&TestContext]) {
|
||||
|
||||
65
test-data/message/group-introduction-no-gossip.eml
Normal file
65
test-data/message/group-introduction-no-gossip.eml
Normal file
@@ -0,0 +1,65 @@
|
||||
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
|
||||
boundary="18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17"
|
||||
MIME-Version: 1.0
|
||||
From: <alice@example.org>
|
||||
To: <bob@example.net>
|
||||
Subject: [...]
|
||||
Date: Mon, 28 Jul 2025 14:15:14 +0000
|
||||
Message-ID: <48b9e9cc-2bae-4d41-89b4-a409e2c60c28@localhost>
|
||||
References: <48b9e9cc-2bae-4d41-89b4-a409e2c60c28@localhost>
|
||||
Chat-Version: 1.0
|
||||
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
|
||||
C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz7CkgQQFggAOgUCaIeF8RYhBC5vossjtTLXKGNLWGSw
|
||||
j2Gp7ZRDAhsDAh4BBQsJCAcCBhUKCQgLAgQWAgMBAScCGQEACgkQZLCPYantlEM66gD/b9qi1/H1Cr
|
||||
UwwlW2akVX86Q0gX6isyKfuNu/CdTdzaQBAIHRxvwlBNZr56qMGL7CyVy6LmBslLlbQwAdclM9t9UE
|
||||
uDgEXlh13RIKKwYBBAGXVQEFAQEHQAbtyNbLZIUBTwqeW2W5tVbrusWLJ+nTUmtF7perLbYdAwEIB8
|
||||
J4BBgWCAAgBQJoh4XxAhsMFiEELm+iyyO1MtcoY0tYZLCPYantlEMACgkQZLCPYantlEPG2QD8DthL
|
||||
48j1wnjw+Kby7CmAm/M+Me82izk8dGNPn442jJ4A/2r+YmqfUPK2XDXPRwvVBAIz5bL44fe7gNkUUu
|
||||
XMnzkP
|
||||
|
||||
|
||||
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17
|
||||
Content-Type: application/pgp-encrypted; charset="utf-8"
|
||||
Content-Description: PGP/MIME version identification
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Version: 1
|
||||
|
||||
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17
|
||||
Content-Type: application/octet-stream; name="encrypted.asc";
|
||||
charset="utf-8"
|
||||
Content-Description: OpenPGP encrypted message
|
||||
Content-Disposition: inline; filename="encrypted.asc";
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
wV4D5tq63hTeebASAQdAg7f2cyYQy+7Xrrlq4j3ik2Ba7L2sbh7Tt398Kke65Rkw
|
||||
sUscOFFConkBj7T5D8XS3e9dX5Bnf1z5jTj15OUZx/2iPTRQFtoVoRB6k6vt/qwq
|
||||
wcBMA+PY3JvEjuMiAQf/U9yB7kWhDMmFI7pMoktqqIvO8woKQG0dx2v1tzjIdtY/
|
||||
KlqMW8qpAqMHHalInGjj0LDDv4iWnNf8yTh71FyGQgipqH4FnYmbRoFW8iicMixN
|
||||
0ps6c6tBiZmyWDq2Ub9SIs9L/W2+vDlbyvFnow08MitOniZLC79KXB2ZRFwp7kOm
|
||||
1gqsZVvsy9fSM7oxXrxAtu2VNyp18emd6jAFWYAz2ISl2KuWYlmSJVexSvQWMRz/
|
||||
IkKWf9kOVAEuIeUnNo45S/DVb9uWQN3M1TrIdJ1BC+nEDBJokCWUGES6kchJc823
|
||||
EkLWpn6JDyRYV/7S9tmYrXodO3x4bSL02OnFbyUjutLCRwEduCtRzg5yrw4IAvJO
|
||||
1ujWf7CaAYE+49oh530HZ/gnBJb03nJhj1SOV0qO9ZquczaW0lhSEtfQF1lVWAVc
|
||||
BCE44YoR9sBqiJEJ0Msj/WLlso5RZHvHa64JrNJ7Jvisgn5vCMSfInzQ4zIZ7LfD
|
||||
sR444bJS9V6MNDSuhKmvPvu4wCFZgNQPs4V51yBX8Rjpn/3xws+NpUtisTt5J+ji
|
||||
KOQg3Thy/9NaNmuXHRbPBxzJKdHzL0bctzVxxDyZPcg6Z2Iteea4gQLEwp5HHw2R
|
||||
VMX97vtamsjp++tMihXRnrwX/a7x9MCAFuzZted4fB87VjHIdhf+CN2KshWsX+X5
|
||||
rPR3+oB6EBVXt8IroGMYLTtmMBS4SzEyiGmNFe/Z4tQSU6pEH+Aeo/FmUhUaMhln
|
||||
BAgRRmhw1Mt9nnuRzLwstpN4W5+mnmccNVg0T6kZz9D7Rbjd7FdzgF8d5K1cJiY/
|
||||
Nv5aajaFKSEwAO9TNHNoP3LD5KxMPiCkRh888V3YhCOwTUfwJG8riWgeyFCN6Xor
|
||||
7k6qHhd3T+1u8QTQkooLWSR7UYu9upQzExvmRPNyAXFyLrZUYjlymC1vn9PfH3Pd
|
||||
31aCGYaYPMdyenoAWTwy7VVSR3wpJuzwHHMeowzCA4TklD/tr2mZSpUrgeBqvS6s
|
||||
k68Pi5WjMs/kH/3Wl5Octb8XYN++DiG7RH5JzWYRchURen8jgPjzJPIUI5t+C8w0
|
||||
vXycuP1PdJcSfKTgxkaQgLs5cUoKEAgO5fA9bUPmjEcizb89im6SoObB+6o7hfwa
|
||||
AIr0TjpOmkdL3TANYA5448gTR4Kq+FwhsxX+fHU6OxwxLBozMcBzvjReKdJko8D+
|
||||
joaTEZBFxyvQUub5/MXmuulTEDhwURgGMbIN0TukdYlhUBfvyJ/wl/U9aHWvk+dz
|
||||
3OJ6d9SqTKPPyluTPV7p3GEDy1AwAex5FrP8SxRGRHiMjVhlbwrQB89ZcUX376ge
|
||||
5MPc4wBn44baPluklYcQtk6kp62KuLpfuLT8VbiLDfKT2FoZzoAnUnw=
|
||||
=HN9M
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
|
||||
--18566fe03178296e_f40510c3b59ace3_ecf843ae3ccd2b17--
|
||||
123
test-data/message/verification-gossip-also-sent-to-from.eml
Normal file
123
test-data/message/verification-gossip-also-sent-to-from.eml
Normal file
@@ -0,0 +1,123 @@
|
||||
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
|
||||
boundary="1854e0cc03e1678e_27ea5f4f17e5cce_dadff6845d0ea716"
|
||||
MIME-Version: 1.0
|
||||
From: <alice@example.org>
|
||||
To: <bob@example.net>, <fiona@example.net>,
|
||||
<alice@example.org>
|
||||
Subject: [...]
|
||||
Date: Thu, 1 Jan 1970 00:00:00 +0000
|
||||
Message-ID: <rfc724_mid>
|
||||
Chat-Version: 1.0
|
||||
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
|
||||
C0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz7CkgQQFggAOgUCaIDT6RYhBC5vossjtTLXKGNLWGSw
|
||||
j2Gp7ZRDAhsDAh4BBQsJCAcCBhUKCQgLAgQWAgMBAScCGQEACgkQZLCPYantlEO5iQEAkKyQJYbI/n
|
||||
zh1IplGOQoJDLgObT8W0oicissPmwBskwBANT8jxTqS+y/8dXObksXNgDnByU5SAXe9o3QeeNKqKoN
|
||||
uDgEXlh13RIKKwYBBAGXVQEFAQEHQAbtyNbLZIUBTwqeW2W5tVbrusWLJ+nTUmtF7perLbYdAwEIB8
|
||||
J4BBgWCAAgBQJogNPpAhsMFiEELm+iyyO1MtcoY0tYZLCPYantlEMACgkQZLCPYantlEMrlQD+KJrE
|
||||
wIdwx6fCj6bAuqigKQMsV7If8Bctcv9Wk7QFK1kA/20HB1kPRTUSHyG+6UDyT2g40MOYwllmMMdHV6
|
||||
rCXGEG
|
||||
|
||||
|
||||
--1854e0cc03e1678e_27ea5f4f17e5cce_dadff6845d0ea716
|
||||
Content-Type: application/pgp-encrypted; charset="utf-8"
|
||||
Content-Description: PGP/MIME version identification
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
Version: 1
|
||||
|
||||
--1854e0cc03e1678e_27ea5f4f17e5cce_dadff6845d0ea716
|
||||
Content-Type: application/octet-stream; name="encrypted.asc";
|
||||
charset="utf-8"
|
||||
Content-Description: OpenPGP encrypted message
|
||||
Content-Disposition: inline; filename="encrypted.asc";
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
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
|
||||
=a+ak
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
|
||||
--1854e0cc03e1678e_27ea5f4f17e5cce_dadff6845d0ea716--
|
||||
|
||||
Reference in New Issue
Block a user