mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 05:26:42 +03:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51f9279e67 | ||
|
|
f27d54f7fa | ||
|
|
7f3648f8ae | ||
|
|
49fc258578 | ||
|
|
0c51b4fe41 | ||
|
|
dbad714539 | ||
|
|
edd8008650 | ||
|
|
615a1b3f4e | ||
|
|
fe6044e1aa | ||
|
|
46b275bfab | ||
|
|
25f44c517a | ||
|
|
cac04f8ee4 | ||
|
|
45d8566ec0 | ||
|
|
29a98ba13b | ||
|
|
e3973f6448 | ||
|
|
7b41425fe4 | ||
|
|
2c7d51f98f | ||
|
|
a2df29515a | ||
|
|
6df1d165dd | ||
|
|
e03e2d9a68 | ||
|
|
8fc6ea19b4 | ||
|
|
c5c947e175 | ||
|
|
6d8dff54a7 |
55
CHANGELOG.md
55
CHANGELOG.md
@@ -1,5 +1,56 @@
|
||||
# Changelog
|
||||
|
||||
## [2.6.0] - 2025-07-23
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix crash when receiving a verification-gossiping message which a contact also sends to itself ([#7032](https://github.com/chatmail/core/pull/7032)).
|
||||
|
||||
## [2.5.0] - 2025-07-22
|
||||
|
||||
### Fixes
|
||||
|
||||
- Correctly migrate "verified by me".
|
||||
- Mark all email chats as unprotected in the migration ([#7026](https://github.com/chatmail/core/pull/7026)).
|
||||
- Do not ignore errors in add_flag_finalized_with_set.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Deprecate protection-broken and related stuff ([#7018](https://github.com/chatmail/core/pull/7018)).
|
||||
- Clarify the meaning of is_verified() vs verifier_id() ([#7027](https://github.com/chatmail/core/pull/7027)).
|
||||
- STYLE.md: Prefer `try_next()` over `next()`.
|
||||
|
||||
## [2.4.0] - 2025-07-21
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not ignore errors when draining FETCH responses. This avoids IMAP loop getting stuck in an infinite loop retrying reading from the connection.
|
||||
- Update `tokio-io-timeout` to 1.2.1. This release includes a fix to reset timeout after every error, so timeout error is returned at most once a minute if read is attempted after a timeout.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update async-imap to 0.11.0.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use `try_next()` when processing FETCH responses.
|
||||
|
||||
## [2.3.0] - 2025-07-19
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add "e2ee encrypted" info message to all e2ee chats ([#7008](https://github.com/chatmail/core/pull/7008)).
|
||||
- repl: Print errors and debug logs to stderr.
|
||||
- `{ensure_and,logged}_debug_assert`: Don't evaluate condition twice.
|
||||
- Log when background fetch of all accounts finishes successfully.
|
||||
- Log the number of read/written bytes on IMAP stream read error ([#6924](https://github.com/chatmail/core/pull/6924)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Ignore protected headers in outer message part ([#6357](https://github.com/chatmail/core/pull/6357)).
|
||||
- List e-mail contacts in repl listcontacts command.
|
||||
- Save peer address for LoggingStream early.
|
||||
|
||||
## [2.2.0] - 2025-07-14
|
||||
|
||||
### API-Changes
|
||||
@@ -6478,3 +6529,7 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.0.0]: https://github.com/chatmail/core/compare/v1.160.0..v2.0.0
|
||||
[2.1.0]: https://github.com/chatmail/core/compare/v2.0.0..v2.1.0
|
||||
[2.2.0]: https://github.com/chatmail/core/compare/v2.1.0..v2.2.0
|
||||
[2.3.0]: https://github.com/chatmail/core/compare/v2.2.0..v2.3.0
|
||||
[2.4.0]: https://github.com/chatmail/core/compare/v2.3.0..v2.4.0
|
||||
[2.5.0]: https://github.com/chatmail/core/compare/v2.4.0..v2.5.0
|
||||
[2.6.0]: https://github.com/chatmail/core/compare/v2.5.0..v2.6.0
|
||||
|
||||
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.2.0"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -1395,7 +1395,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.2.0"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.3.1",
|
||||
@@ -1417,7 +1417,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.2.0"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1433,7 +1433,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.2.0"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1462,7 +1462,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.2.0"
|
||||
version = "2.6.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.2.0"
|
||||
version = "2.6.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
|
||||
|
||||
17
README.md
17
README.md
@@ -84,26 +84,29 @@ Create a contact:
|
||||
|
||||
```
|
||||
> addcontact yourfriends@email.org
|
||||
Command executed successfully.
|
||||
```
|
||||
|
||||
List contacts:
|
||||
|
||||
```
|
||||
> listcontacts
|
||||
Contact#10: <name unset> <yourfriends@email.org>
|
||||
Contact#1: Me √√ <your@email.org>
|
||||
Contact#Contact#Self: Me √ <your@email.org>
|
||||
1 key contacts.
|
||||
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
|
||||
1 address contacts.
|
||||
```
|
||||
|
||||
Create a chat with your friend and send a message:
|
||||
|
||||
```
|
||||
> createchat 10
|
||||
Single#10 created successfully.
|
||||
> chat 10
|
||||
Single#10: yourfriends@email.org [yourfriends@email.org]
|
||||
Single#Chat#12 created successfully.
|
||||
> chat 12
|
||||
Selecting chat Chat#12
|
||||
Single#Chat#12: yourfriends@email.org [yourfriends@email.org] Icon: profile-db-blobs/4138c52e5bc1c576cda7dd44d088c07.png
|
||||
0 messages.
|
||||
81.252µs to create this list, 123.625µs to mark all messages as noticed.
|
||||
> send hi
|
||||
Message sent.
|
||||
```
|
||||
|
||||
List messages when inside a chat:
|
||||
|
||||
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.2.0"
|
||||
version = "2.6.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.
|
||||
@@ -4535,12 +4521,12 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
||||
* - DC_INFO_MEMBER_ADDED_TO_GROUP (4) - "Member CONTACT added by OTHER_CONTACT"
|
||||
* - DC_INFO_MEMBER_REMOVED_FROM_GROUP (5) - "Member CONTACT removed by OTHER_CONTACT"
|
||||
* - DC_INFO_EPHEMERAL_TIMER_CHANGED (10) - "Disappearing messages CHANGED_TO by CONTACT"
|
||||
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
|
||||
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
|
||||
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is protected"
|
||||
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
|
||||
* the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL
|
||||
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
|
||||
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
|
||||
* - DC_INFO_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted"
|
||||
*
|
||||
* For the messages that refer to a CONTACT,
|
||||
* dc_msg_get_info_contact_id() returns the contact ID.
|
||||
@@ -4593,9 +4579,10 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
|
||||
#define DC_INFO_LOCATION_ONLY 9
|
||||
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
|
||||
#define DC_INFO_PROTECTION_ENABLED 11
|
||||
#define DC_INFO_PROTECTION_DISABLED 12
|
||||
#define DC_INFO_PROTECTION_DISABLED 12 // deprecated 2025-07
|
||||
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
|
||||
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
||||
#define DC_INFO_CHAT_E2EE 50
|
||||
|
||||
|
||||
/**
|
||||
@@ -5266,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);
|
||||
|
||||
@@ -5310,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.
|
||||
@@ -6385,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().
|
||||
*
|
||||
@@ -6898,9 +6884,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in summaries.
|
||||
#define DC_STR_GIF 23
|
||||
|
||||
/// "Encrypted message"
|
||||
///
|
||||
/// Used in subjects of outgoing messages.
|
||||
/// @deprecated 2025-07, this string is no longer needed.
|
||||
#define DC_STR_ENCRYPTEDMSG 24
|
||||
|
||||
/// "End-to-end encryption available."
|
||||
@@ -7605,7 +7589,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as a device message after a successful backup transfer.
|
||||
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
|
||||
|
||||
/// "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
/// "Messages are end-to-end encrypted."
|
||||
///
|
||||
/// Used in info messages.
|
||||
#define DC_STR_CHAT_PROTECTION_ENABLED 170
|
||||
@@ -7613,6 +7597,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "%1$s sent a message from another device."
|
||||
///
|
||||
/// Used in info messages.
|
||||
/// @deprecated 2025-07
|
||||
#define DC_STR_CHAT_PROTECTION_DISABLED 171
|
||||
|
||||
/// "Others will only see this group after you sent a first message."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.2.0"
|
||||
version = "2.6.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,7 @@ pub struct FullChat {
|
||||
fresh_message_counter: usize,
|
||||
// is_group - please check over chat.type in frontend instead
|
||||
is_contact_request: bool,
|
||||
is_protection_broken: bool,
|
||||
is_protection_broken: bool, // deprecated 2025-07
|
||||
is_device_chat: bool,
|
||||
self_in_group: 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().
|
||||
///
|
||||
|
||||
@@ -416,6 +416,9 @@ pub enum SystemMessageType {
|
||||
/// Chat ephemeral message timer is changed.
|
||||
EphemeralTimerChanged,
|
||||
|
||||
// Chat is e2ee
|
||||
ChatE2ee,
|
||||
|
||||
// Chat protection state changed
|
||||
ChatProtectionEnabled,
|
||||
ChatProtectionDisabled,
|
||||
@@ -450,6 +453,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
|
||||
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
|
||||
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
|
||||
SystemMessage::ChatE2ee => SystemMessageType::ChatE2ee,
|
||||
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
|
||||
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
|
||||
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.2.0"
|
||||
"version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -95,8 +95,10 @@ describe("online tests", function () {
|
||||
false,
|
||||
);
|
||||
|
||||
expect(messageList).have.length(1);
|
||||
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
|
||||
// There are 2 messages in the chat:
|
||||
// 'Messages are end-to-end encrypted' (info message) and 'Hello'
|
||||
expect(messageList).have.length(2);
|
||||
const message = await dc.rpc.getMessage(accountId2, messageList[1]);
|
||||
expect(message.text).equal("Hello");
|
||||
expect(message.showPadlock).equal(true);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.2.0"
|
||||
version = "2.6.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -87,7 +87,7 @@ async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
||||
let data = read_file(context, filename).await?;
|
||||
|
||||
if let Err(err) = receive_imf(context, &data, false).await {
|
||||
println!("receive_imf errored: {err:?}");
|
||||
eprintln!("receive_imf errored: {err:?}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -621,7 +621,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!("Location streaming enabled.");
|
||||
}
|
||||
println!("{cnt} chats");
|
||||
println!("{time_needed:?} to create this list");
|
||||
eprintln!("{time_needed:?} to create this list");
|
||||
}
|
||||
"start-realtime" => {
|
||||
if arg1.is_empty() {
|
||||
@@ -731,7 +731,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
|
||||
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
|
||||
|
||||
println!(
|
||||
eprintln!(
|
||||
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
|
||||
);
|
||||
}
|
||||
@@ -985,7 +985,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
},
|
||||
query,
|
||||
);
|
||||
println!("{time_needed:?} to create this list");
|
||||
eprintln!("{time_needed:?} to create this list");
|
||||
}
|
||||
"draft" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
@@ -1151,7 +1151,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"listcontacts" | "contacts" => {
|
||||
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
println!("{} contacts.", contacts.len());
|
||||
println!("{} key contacts.", contacts.len());
|
||||
let addrcontacts = Contact::get_all(&context, DC_GCL_ADDRESS, Some(arg1)).await?;
|
||||
log_contactlist(&context, &addrcontacts).await?;
|
||||
println!("{} address contacts.", addrcontacts.len());
|
||||
}
|
||||
"addcontact" => {
|
||||
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
|
||||
@@ -1224,7 +1227,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
match set_config_from_qr(&context, arg1).await {
|
||||
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
|
||||
Err(err) => println!("Cannot set config from QR code: {err:?}"),
|
||||
Err(err) => eprintln!("Cannot set config from QR code: {err:?}"),
|
||||
}
|
||||
}
|
||||
"createqrsvg" => {
|
||||
|
||||
@@ -311,7 +311,7 @@ impl Validator for DcHelper {}
|
||||
|
||||
async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
if args.len() < 2 {
|
||||
println!("Error: Bad arguments, expected [db-name].");
|
||||
eprintln!("Error: Bad arguments, expected [db-name].");
|
||||
bail!("No db-name specified");
|
||||
}
|
||||
let context = ContextBuilder::new(args[1].clone().into())
|
||||
@@ -366,7 +366,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
false
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {err:#}");
|
||||
eprintln!("Error: {err:#}");
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -381,7 +381,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {err:#}");
|
||||
eprintln!("Error: {err:#}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.2.0"
|
||||
version = "2.6.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -13,6 +13,12 @@ from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Messag
|
||||
from ._utils import futuremethod
|
||||
from .rpc import Rpc
|
||||
|
||||
E2EE_INFO_MSGS = 1
|
||||
"""
|
||||
The number of info messages added to new e2ee chats.
|
||||
Currently this is "End-to-end encryption available".
|
||||
"""
|
||||
|
||||
|
||||
class ACFactory:
|
||||
"""Test account factory."""
|
||||
|
||||
@@ -36,6 +36,9 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
|
||||
# Second client receives only second message, but not the first.
|
||||
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import pytest
|
||||
|
||||
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||
from deltachat_rpc_client.const import ChatType, DownloadState, MessageState
|
||||
from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@@ -457,8 +458,12 @@ def test_wait_next_messages(acfactory) -> None:
|
||||
alice_chat_bot.send_text("Hello!")
|
||||
|
||||
next_messages = next_messages_task.result()
|
||||
assert len(next_messages) == 1
|
||||
snapshot = next_messages[0].get_snapshot()
|
||||
|
||||
if len(next_messages) == E2EE_INFO_MSGS:
|
||||
next_messages += bot.wait_next_messages()
|
||||
|
||||
assert len(next_messages) == 1 + E2EE_INFO_MSGS
|
||||
snapshot = next_messages[0 + E2EE_INFO_MSGS].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.2.0"
|
||||
version = "2.6.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.2.0"
|
||||
"version": "2.6.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.2.0"
|
||||
version = "2.6.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
@@ -20,6 +20,12 @@ import deltachat
|
||||
from . import Account, account_hookimpl, const, get_core_info
|
||||
from .events import FFIEventLogger, FFIEventTracker
|
||||
|
||||
E2EE_INFO_MSGS = 1
|
||||
"""
|
||||
The number of info messages added to new e2ee chats.
|
||||
Currently this is "End-to-end encryption available".
|
||||
"""
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("deltachat testplugin options")
|
||||
@@ -606,7 +612,7 @@ class ACFactory:
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg is not None
|
||||
assert msg.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
assert msg.text == "Messages are end-to-end encrypted."
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg is not None
|
||||
assert "Member Me " in msg.text and " added by " in msg.text
|
||||
|
||||
@@ -133,8 +133,7 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
assert "added" in msg.text.lower()
|
||||
|
||||
assert any(
|
||||
m.is_system_message() and m.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
for m in msg.chat.get_messages()
|
||||
m.is_system_message() and m.text == "Messages are end-to-end encrypted." for m in msg.chat.get_messages()
|
||||
)
|
||||
lp.sec("ac1: send message")
|
||||
msg_out = chat1.send_text("hello")
|
||||
@@ -338,7 +337,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
assert contact.addr == ac1.get_config("addr")
|
||||
chat2 = msg_in.chat
|
||||
assert chat2.is_protected()
|
||||
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
assert chat2.get_messages()[0].text == "Messages are end-to-end encrypted."
|
||||
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
|
||||
|
||||
lp.sec("ac2_offl: sending message")
|
||||
@@ -412,7 +411,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2_offl.get_message_by_id(ev.data2)
|
||||
assert msg_in.is_system_message()
|
||||
assert msg_in.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
assert msg_in.text == "Messages are end-to-end encrypted."
|
||||
|
||||
# We need to consume one event that has data2=0
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
|
||||
@@ -10,6 +10,7 @@ from imap_tools import AND, U
|
||||
import deltachat as dc
|
||||
from deltachat import account_hookimpl, Message
|
||||
from deltachat.tracker import ImexTracker
|
||||
from deltachat.testplugin import E2EE_INFO_MSGS
|
||||
|
||||
|
||||
def test_basic_imap_api(acfactory, tmp_path):
|
||||
@@ -408,6 +409,10 @@ def test_forward_messages(acfactory, lp):
|
||||
msg_out = chat.send_text("message2")
|
||||
|
||||
lp.sec("ac2: wait for receive")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2.get_message_by_id(ev.data2)
|
||||
assert msg_in.text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
assert ev.data2 == msg_out.id
|
||||
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||
@@ -622,6 +627,11 @@ def test_moved_markseen(acfactory):
|
||||
|
||||
with ac2.direct_imap.idle() as idle2:
|
||||
ac2.start_io()
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg.text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
|
||||
@@ -738,7 +748,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
lp.sec("sending text message from ac1 to ac2")
|
||||
msg_out = chat.send_text("message1")
|
||||
|
||||
assert len(chat.get_messages()) == 1
|
||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
|
||||
lp.sec("disable ac1 MDNs")
|
||||
ac1.set_config("mdns_enabled", "0")
|
||||
@@ -746,7 +756,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
assert len(msg.chat.get_messages()) == 1
|
||||
assert len(msg.chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
|
||||
lp.sec("ac2: mark incoming message as seen")
|
||||
ac2.mark_seen_messages([msg])
|
||||
@@ -755,7 +765,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
# MDN should be moved even though MDNs are already disabled
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
assert len(chat.get_messages()) == 1
|
||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
|
||||
@@ -1123,6 +1133,11 @@ def test_send_and_receive_image(acfactory, lp, data):
|
||||
assert m == msg_out
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG")
|
||||
msg_in = ac2.get_message_by_id(ev.data2)
|
||||
assert msg_in.text == "Messages are end-to-end encrypted."
|
||||
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 == msg_out.id
|
||||
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||
@@ -1158,10 +1173,10 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
assert contact2.addr == some1_addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 3
|
||||
assert messages[0].text == "msg1"
|
||||
assert messages[1].filemime == "image/png"
|
||||
assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size
|
||||
assert len(messages) == 3 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert messages[1 + E2EE_INFO_MSGS].filemime == "image/png"
|
||||
assert os.stat(messages[1 + E2EE_INFO_MSGS].filename).st_size == os.stat(original_image_path).st_size
|
||||
ac.set_config("displayname", "new displayname")
|
||||
assert ac.get_config("displayname") == "new displayname"
|
||||
|
||||
@@ -1414,8 +1429,8 @@ def test_connectivity(acfactory, lp):
|
||||
ac1.maybe_network()
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
msgs = ac1.create_chat(ac2).get_messages()
|
||||
assert len(msgs) == 1
|
||||
assert msgs[0].text == "Hi"
|
||||
assert len(msgs) == 1 + E2EE_INFO_MSGS
|
||||
assert msgs[0 + E2EE_INFO_MSGS].text == "Hi"
|
||||
|
||||
lp.sec("Test that the connectivity changes to WORKING while new messages are fetched")
|
||||
|
||||
@@ -1425,8 +1440,8 @@ def test_connectivity(acfactory, lp):
|
||||
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
msgs = ac1.create_chat(ac2).get_messages()
|
||||
assert len(msgs) == 2
|
||||
assert msgs[1].text == "Hi 2"
|
||||
assert len(msgs) == 2 + E2EE_INFO_MSGS
|
||||
assert msgs[1 + E2EE_INFO_MSGS].text == "Hi 2"
|
||||
|
||||
|
||||
def test_fetch_deleted_msg(acfactory, lp):
|
||||
|
||||
@@ -6,6 +6,7 @@ import pytest
|
||||
import deltachat as dc
|
||||
from deltachat.tracker import ImexFailed
|
||||
from deltachat import Account, Message
|
||||
from deltachat.testplugin import E2EE_INFO_MSGS
|
||||
|
||||
|
||||
class TestOfflineAccountBasic:
|
||||
@@ -461,9 +462,9 @@ class TestOfflineChat:
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
|
||||
def test_import_export_on_encrypted_acct(self, acfactory, tmp_path):
|
||||
passphrase1 = "passphrase1"
|
||||
@@ -500,9 +501,9 @@ class TestOfflineChat:
|
||||
contact2_addr = contact2.addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
|
||||
ac2.shutdown()
|
||||
|
||||
@@ -517,9 +518,9 @@ class TestOfflineChat:
|
||||
assert contact2.addr == contact2_addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
|
||||
def test_import_export_with_passphrase(self, acfactory, tmp_path):
|
||||
passphrase = "test_passphrase"
|
||||
@@ -557,9 +558,9 @@ class TestOfflineChat:
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
|
||||
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
|
||||
"""
|
||||
@@ -603,9 +604,9 @@ class TestOfflineChat:
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
|
||||
ac2.shutdown()
|
||||
|
||||
@@ -620,9 +621,9 @@ class TestOfflineChat:
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
|
||||
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
|
||||
|
||||
def test_set_get_draft(self, chat1):
|
||||
msg1 = Message.new_empty(chat1.account, "text")
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-07-14
|
||||
2025-07-23
|
||||
@@ -353,11 +353,11 @@ impl Accounts {
|
||||
/// This is an auxiliary function and not part of public API.
|
||||
/// Use [Accounts::background_fetch] instead.
|
||||
async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
|
||||
let n_accounts = accounts.len();
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::Info(format!(
|
||||
"Starting background fetch for {} accounts.",
|
||||
accounts.len()
|
||||
"Starting background fetch for {n_accounts} accounts."
|
||||
)),
|
||||
});
|
||||
let mut set = JoinSet::new();
|
||||
@@ -369,6 +369,12 @@ impl Accounts {
|
||||
});
|
||||
}
|
||||
set.join_all().await;
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::Info(format!(
|
||||
"Finished background fetch for {n_accounts} accounts."
|
||||
)),
|
||||
});
|
||||
}
|
||||
|
||||
/// Auxiliary function for [Accounts::background_fetch].
|
||||
|
||||
46
src/chat.rs
46
src/chat.rs
@@ -118,7 +118,7 @@ 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
|
||||
/// Deprecated. The chat was protected, but now a new message came in
|
||||
/// which was not encrypted / signed correctly.
|
||||
ProtectionBroken,
|
||||
|
||||
@@ -349,6 +349,8 @@ impl ChatId {
|
||||
chat_id
|
||||
.add_protection_msg(context, ProtectionStatus::Protected, None, timestamp)
|
||||
.await?;
|
||||
} else {
|
||||
chat_id.maybe_add_encrypted_msg(context, timestamp).await?;
|
||||
}
|
||||
|
||||
info!(
|
||||
@@ -604,6 +606,42 @@ impl ChatId {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds message "Messages are end-to-end encrypted" if appropriate.
|
||||
///
|
||||
/// This function is rather slow because it does a lot of database queries,
|
||||
/// but this is fine because it is only called on chat creation.
|
||||
async fn maybe_add_encrypted_msg(self, context: &Context, timestamp_sort: i64) -> Result<()> {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
|
||||
// as secure-join adds its own message on success (after some other messasges),
|
||||
// we do not want to add "Messages are end-to-end encrypted" on chat creation.
|
||||
// we detect secure join by `can_send` (for Bob, scanner side) and by `blocked` (for Alice, inviter side) below.
|
||||
if !chat.is_encrypted(context).await?
|
||||
|| self <= DC_CHAT_ID_LAST_SPECIAL
|
||||
|| chat.is_device_talk()
|
||||
|| chat.is_self_talk()
|
||||
|| (!chat.can_send(context).await? && !chat.is_contact_request())
|
||||
|| chat.blocked == Blocked::Yes
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let text = stock_str::messages_e2e_encrypted(context).await;
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
self,
|
||||
&text,
|
||||
SystemMessage::ChatE2ee,
|
||||
timestamp_sort,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets protection and adds a message.
|
||||
///
|
||||
/// `timestamp_sort` is used as the timestamp of the added message
|
||||
@@ -1897,7 +1935,7 @@ impl Chat {
|
||||
Ok(is_encrypted)
|
||||
}
|
||||
|
||||
/// Returns true if the chat was protected, and then an incoming message broke this protection.
|
||||
/// Deprecated 2025-07. 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.
|
||||
@@ -2673,6 +2711,10 @@ impl ChatIdBlocked {
|
||||
smeared_time,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
chat_id
|
||||
.maybe_add_encrypted_msg(context, smeared_time)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::message::{MessengerMessage, delete_msgs};
|
||||
use crate::mimeparser::{self, MimeMessage};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, E2EE_INFO_MSGS, TestContext, TestContextManager,
|
||||
TimeShiftFalsePositiveNote, sync,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -2104,7 +2104,7 @@ async fn test_forward_basic() -> Result<()> {
|
||||
forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?;
|
||||
|
||||
let forwarded_msg = bob.pop_sent_msg().await;
|
||||
assert_eq!(bob_chat.id.get_msg_cnt(&bob).await?, 2);
|
||||
assert_eq!(bob_chat.id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 2);
|
||||
assert_ne!(
|
||||
forwarded_msg.load_from_db().await.rfc724_mid,
|
||||
msg.rfc724_mid,
|
||||
@@ -2132,7 +2132,7 @@ async fn test_forward_info_msg() -> Result<()> {
|
||||
assert!(msg1.get_text().contains("bob@example.net"));
|
||||
|
||||
let chat_id2 = ChatId::create_for_contact(alice, bob_id).await?;
|
||||
assert_eq!(get_chat_msgs(alice, chat_id2).await?.len(), 0);
|
||||
assert_eq!(get_chat_msgs(alice, chat_id2).await?.len(), E2EE_INFO_MSGS);
|
||||
forward_msgs(alice, &[msg1.id], chat_id2).await?;
|
||||
let msg2 = alice.get_last_msg_in(chat_id2).await;
|
||||
assert!(!msg2.is_info()); // forwarded info-messages lose their info-state
|
||||
@@ -2518,22 +2518,34 @@ async fn test_resend_own_message() -> Result<()> {
|
||||
let sent1_ts_sent = msg.timestamp_sent;
|
||||
assert_eq!(msg.get_text(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 1);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 1
|
||||
);
|
||||
bob.recv_msg(&sent2).await;
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
let received = bob.recv_msg_opt(&sent3).await;
|
||||
// No message should actually be added since we already know this message:
|
||||
assert!(received.is_none());
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
|
||||
// Fiona does not receive the first message, however, due to resending, she has a similar view as Alice and Bob
|
||||
fiona.recv_msg(&sent2).await;
|
||||
let msg = fiona.recv_msg(&sent3).await;
|
||||
assert_eq!(msg.get_text(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&fiona, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&fiona, msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&fiona, msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
let msg_from = Contact::get_by_id(&fiona, msg.get_from_id()).await?;
|
||||
assert_eq!(msg_from.get_addr(), "alice@example.org");
|
||||
assert!(sent1_ts_sent < msg.timestamp_sent);
|
||||
@@ -4454,13 +4466,13 @@ async fn test_receive_edit_request_after_removal() -> Result<()> {
|
||||
let bob_msg = bob.recv_msg(&sent1).await;
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
assert_eq!(bob_msg.text, "zext me in delra.cat");
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 1);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1);
|
||||
|
||||
delete_msgs(bob, &[bob_msg.id]).await?;
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 0);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS);
|
||||
|
||||
bob.recv_msg_trash(&sent2).await;
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 0);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4549,28 +4561,34 @@ async fn test_send_delete_request() -> Result<()> {
|
||||
// Alice sends a message, then sends a deletion request
|
||||
let sent1 = alice.send_text(alice_chat.id, "wtf").await;
|
||||
let alice_msg = sent1.load_from_db().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 2);
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 2);
|
||||
|
||||
message::delete_msgs_ex(alice, &[alice_msg.id], true).await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1);
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 1);
|
||||
|
||||
// Bob receives both messages and has nothing the end
|
||||
let bob_msg = bob.recv_msg(&sent1).await;
|
||||
assert_eq!(bob_msg.text, "wtf");
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 2);
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 2);
|
||||
|
||||
bob.recv_msg_opt(&sent2).await;
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 1);
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1);
|
||||
|
||||
// Alice has another device, and there is also nothing at the end
|
||||
let alice2 = &tcm.alice().await;
|
||||
alice2.recv_msg(&sent0).await;
|
||||
let alice2_msg = alice2.recv_msg(&sent1).await;
|
||||
assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 2);
|
||||
assert_eq!(
|
||||
alice2_msg.chat_id.get_msg_cnt(alice2).await?,
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
|
||||
alice2.recv_msg_opt(&sent2).await;
|
||||
assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 1);
|
||||
assert_eq!(
|
||||
alice2_msg.chat_id.get_msg_cnt(alice2).await?,
|
||||
E2EE_INFO_MSGS + 1
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1043,7 +1043,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(),
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, get_chat_msg};
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, get_chat_msg};
|
||||
use crate::tools::{SystemTime, create_outgoing_rfc724_mid};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -571,7 +571,7 @@ async fn test_get_next_msgs() -> Result<()> {
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
|
||||
assert!(alice.get_next_msgs().await?.is_empty());
|
||||
assert_eq!(alice.get_next_msgs().await?.len(), E2EE_INFO_MSGS);
|
||||
assert!(bob.get_next_msgs().await?.is_empty());
|
||||
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
|
||||
|
||||
@@ -178,7 +178,8 @@ mod tests {
|
||||
let bob = TestContext::new_bob().await;
|
||||
receive_imf(&bob, attachment_mime, false).await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert_eq!(msg.text, "Hello from Thunderbird!");
|
||||
// Subject should be prepended because the attachment doesn't have "Chat-Version".
|
||||
assert_eq!(msg.text, "Hello, Bob! – Hello from Thunderbird!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ mod tests {
|
||||
use crate::chat::{get_chat_msgs, send_msg};
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::receive_imf::receive_imf_from_inbox;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext};
|
||||
|
||||
#[test]
|
||||
fn test_downloadstate_values() {
|
||||
@@ -459,7 +459,10 @@ mod tests {
|
||||
.await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
let chat_id = msg.chat_id;
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 1);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 1
|
||||
);
|
||||
assert_eq!(msg.download_state(), DownloadState::Available);
|
||||
|
||||
// downloading the status update afterwards expands to nothing and moves the placeholder to trash-chat
|
||||
@@ -472,7 +475,7 @@ mod tests {
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), E2EE_INFO_MSGS);
|
||||
assert!(
|
||||
Message::load_from_db_optional(&bob, msg.id)
|
||||
.await?
|
||||
|
||||
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(())
|
||||
|
||||
@@ -8,15 +8,13 @@ use tokio::io::BufWriter;
|
||||
|
||||
use super::capabilities::Capabilities;
|
||||
use crate::context::Context;
|
||||
use crate::log::{info, warn};
|
||||
use crate::log::{LoggingStream, info, warn};
|
||||
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
|
||||
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::net::tls::wrap_tls;
|
||||
use crate::net::{
|
||||
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
|
||||
};
|
||||
use crate::net::{connect_tcp_inner, run_connection_attempts, update_connection_history};
|
||||
use crate::tools::time;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -126,12 +124,12 @@ impl Client {
|
||||
);
|
||||
let res = match security {
|
||||
ConnectionSecurity::Tls => {
|
||||
Client::connect_secure(resolved_addr, host, strict_tls).await
|
||||
Client::connect_secure(context, resolved_addr, host, strict_tls).await
|
||||
}
|
||||
ConnectionSecurity::Starttls => {
|
||||
Client::connect_starttls(resolved_addr, host, strict_tls).await
|
||||
Client::connect_starttls(context, resolved_addr, host, strict_tls).await
|
||||
}
|
||||
ConnectionSecurity::Plain => Client::connect_insecure(resolved_addr).await,
|
||||
ConnectionSecurity::Plain => Client::connect_insecure(context, resolved_addr).await,
|
||||
};
|
||||
match res {
|
||||
Ok(client) => {
|
||||
@@ -202,40 +200,61 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_secure(addr: SocketAddr, hostname: &str, strict_tls: bool) -> Result<Self> {
|
||||
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?;
|
||||
async fn connect_secure(
|
||||
context: &Context,
|
||||
addr: SocketAddr,
|
||||
hostname: &str,
|
||||
strict_tls: bool,
|
||||
) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp_inner(addr).await?;
|
||||
let account_id = context.get_id();
|
||||
let events = context.events.clone();
|
||||
let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, alpn(addr.port()), logging_stream).await?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = Client::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn connect_insecure(addr: SocketAddr) -> Result<Self> {
|
||||
async fn connect_insecure(context: &Context, addr: SocketAddr) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp_inner(addr).await?;
|
||||
let buffered_stream = BufWriter::new(tcp_stream);
|
||||
let account_id = context.get_id();
|
||||
let events = context.events.clone();
|
||||
let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
|
||||
let buffered_stream = BufWriter::new(logging_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = Client::new(session_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn connect_starttls(addr: SocketAddr, host: &str, strict_tls: bool) -> Result<Self> {
|
||||
async fn connect_starttls(
|
||||
context: &Context,
|
||||
addr: SocketAddr,
|
||||
host: &str,
|
||||
strict_tls: bool,
|
||||
) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp_inner(addr).await?;
|
||||
|
||||
let account_id = context.get_id();
|
||||
let events = context.events.clone();
|
||||
let tcp_stream = LoggingStream::new(tcp_stream, account_id, events)?;
|
||||
|
||||
// Run STARTTLS command and convert the client back into a stream.
|
||||
let buffered_tcp_stream = BufWriter::new(tcp_stream);
|
||||
let mut client = async_imap::Client::new(buffered_tcp_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
.context("failed to read greeting")??;
|
||||
.await?
|
||||
.context("Failed to read greeting")?;
|
||||
client
|
||||
.run_command_and_check_ok("STARTTLS", None)
|
||||
.await
|
||||
@@ -246,7 +265,6 @@ impl Client {
|
||||
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
|
||||
.await
|
||||
.context("STARTTLS upgrade failed")?;
|
||||
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let client = Client::new(session_stream);
|
||||
@@ -269,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)
|
||||
}
|
||||
|
||||
@@ -286,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)
|
||||
}
|
||||
|
||||
@@ -307,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?;
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
|
||||
use crate::context::Context;
|
||||
|
||||
mod stream;
|
||||
|
||||
pub(crate) use stream::LoggingStream;
|
||||
|
||||
macro_rules! info {
|
||||
($ctx:expr, $msg:expr) => {
|
||||
info!($ctx, $msg,)
|
||||
|
||||
161
src/log/stream.rs
Normal file
161
src/log/stream.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
//! Stream that logs errors as events.
|
||||
//!
|
||||
//! This stream can be used to wrap IMAP,
|
||||
//! SMTP and HTTP streams so errors
|
||||
//! that occur are logged before
|
||||
//! they are processed.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use pin_project::pin_project;
|
||||
|
||||
use crate::events::{Event, EventType, Events};
|
||||
use crate::net::session::SessionStream;
|
||||
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Metrics {
|
||||
/// Total number of bytes read.
|
||||
pub total_read: usize,
|
||||
|
||||
/// Total number of bytes written.
|
||||
pub total_written: usize,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
total_read: 0,
|
||||
total_written: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stream that logs errors to the event channel.
|
||||
#[derive(Debug)]
|
||||
#[pin_project]
|
||||
pub(crate) struct LoggingStream<S: SessionStream> {
|
||||
#[pin]
|
||||
inner: S,
|
||||
|
||||
/// Account ID for logging.
|
||||
account_id: u32,
|
||||
|
||||
/// Event channel.
|
||||
events: Events,
|
||||
|
||||
/// Metrics for this stream.
|
||||
metrics: Metrics,
|
||||
|
||||
/// Peer address at the time of creation.
|
||||
///
|
||||
/// Socket may become disconnected later,
|
||||
/// so we save it when `LoggingStream` is created.
|
||||
peer_addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl<S: SessionStream> LoggingStream<S> {
|
||||
pub fn new(inner: S, account_id: u32, events: Events) -> Result<Self> {
|
||||
let peer_addr: SocketAddr = inner
|
||||
.peer_addr()
|
||||
.context("Attempt to create LoggingStream over an unconnected stream")?;
|
||||
Ok(Self {
|
||||
inner,
|
||||
account_id,
|
||||
events,
|
||||
metrics: Metrics::new(),
|
||||
peer_addr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: SessionStream> AsyncRead for LoggingStream<S> {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
let this = self.project();
|
||||
let old_remaining = buf.remaining();
|
||||
|
||||
let res = this.inner.poll_read(cx, buf);
|
||||
|
||||
if let Poll::Ready(Err(ref err)) = res {
|
||||
let peer_addr = this.peer_addr;
|
||||
let log_message = format!(
|
||||
"Read error on stream {peer_addr:?} after reading {} and writing {} bytes: {err}.",
|
||||
this.metrics.total_read, this.metrics.total_written
|
||||
);
|
||||
this.events.emit(Event {
|
||||
id: *this.account_id,
|
||||
typ: EventType::Warning(log_message),
|
||||
});
|
||||
}
|
||||
|
||||
let n = old_remaining - buf.remaining();
|
||||
this.metrics.total_read = this.metrics.total_read.saturating_add(n);
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: SessionStream> AsyncWrite for LoggingStream<S> {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<std::io::Result<usize>> {
|
||||
let this = self.project();
|
||||
let res = this.inner.poll_write(cx, buf);
|
||||
if let Poll::Ready(Ok(n)) = res {
|
||||
this.metrics.total_written = this.metrics.total_written.saturating_add(n);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
self.project().inner.poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
self.project().inner.poll_shutdown(cx)
|
||||
}
|
||||
|
||||
fn poll_write_vectored(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
bufs: &[std::io::IoSlice<'_>],
|
||||
) -> Poll<std::io::Result<usize>> {
|
||||
let this = self.project();
|
||||
let res = this.inner.poll_write_vectored(cx, bufs);
|
||||
if let Poll::Ready(Ok(n)) = res {
|
||||
this.metrics.total_written = this.metrics.total_written.saturating_add(n);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn is_write_vectored(&self) -> bool {
|
||||
self.inner.is_write_vectored()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: SessionStream> SessionStream for LoggingStream<S> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.inner.set_read_timeout(timeout)
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.inner.peer_addr()
|
||||
}
|
||||
}
|
||||
@@ -963,6 +963,7 @@ impl Message {
|
||||
| SystemMessage::SecurejoinMessage
|
||||
| SystemMessage::LocationStreamingEnabled
|
||||
| SystemMessage::LocationOnly
|
||||
| SystemMessage::ChatE2ee
|
||||
| SystemMessage::ChatProtectionEnabled
|
||||
| SystemMessage::ChatProtectionDisabled
|
||||
| SystemMessage::InvalidUnencryptedMail
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::config::Config;
|
||||
use crate::reaction::send_reaction;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager};
|
||||
|
||||
#[test]
|
||||
fn test_guess_msgtype_from_suffix() {
|
||||
@@ -347,7 +347,7 @@ async fn test_markseen_msgs() -> Result<()> {
|
||||
let chats = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?;
|
||||
assert_eq!(msgs.len(), 2);
|
||||
assert_eq!(msgs.len(), E2EE_INFO_MSGS + 2);
|
||||
assert_eq!(bob.get_fresh_msgs().await?.len(), 0);
|
||||
|
||||
// that has no effect in contact request
|
||||
@@ -358,7 +358,7 @@ async fn test_markseen_msgs() -> Result<()> {
|
||||
assert_eq!(bob_chat.blocked, Blocked::Request);
|
||||
|
||||
let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?;
|
||||
assert_eq!(msgs.len(), 2);
|
||||
assert_eq!(msgs.len(), E2EE_INFO_MSGS + 2);
|
||||
bob_chat_id.accept(&bob).await.unwrap();
|
||||
|
||||
// bob sends to alice,
|
||||
@@ -761,19 +761,22 @@ async fn test_delete_msgs_sync() -> Result<()> {
|
||||
|
||||
// Alice sends a messsage and receives it on the other device
|
||||
let sent1 = alice.send_text(alice_chat_id, "foo").await;
|
||||
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, 1);
|
||||
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 1);
|
||||
|
||||
let msg = alice2.recv_msg(&sent1).await;
|
||||
let alice2_chat_id = msg.chat_id;
|
||||
assert_eq!(alice2.get_last_msg_in(alice2_chat_id).await.id, msg.id);
|
||||
assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, 1);
|
||||
assert_eq!(
|
||||
alice2_chat_id.get_msg_cnt(alice2).await?,
|
||||
E2EE_INFO_MSGS + 1
|
||||
);
|
||||
|
||||
// Alice deletes the message; this should happen on both devices as well
|
||||
delete_msgs(alice, &[sent1.sender_msg_id]).await?;
|
||||
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, 0);
|
||||
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS);
|
||||
|
||||
test_utils::sync(alice, alice2).await;
|
||||
assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, 0);
|
||||
assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, E2EE_INFO_MSGS);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -368,8 +368,14 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
|
||||
ensure_and_debug_assert!(member_timestamps.len() >= to.len());
|
||||
ensure_and_debug_assert!(member_fingerprints.is_empty() || member_fingerprints.len() >= to.len());
|
||||
ensure_and_debug_assert!(
|
||||
member_timestamps.len() >= to.len(),
|
||||
"member_timestamps.len() ({}) < to.len() ({})",
|
||||
member_timestamps.len(), to.len());
|
||||
ensure_and_debug_assert!(
|
||||
member_fingerprints.is_empty() || member_fingerprints.len() >= to.len(),
|
||||
"member_fingerprints.len() ({}) < to.len() ({})",
|
||||
member_fingerprints.len(), to.len());
|
||||
|
||||
if to.len() > 1 {
|
||||
if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
|
||||
@@ -448,7 +454,11 @@ impl MimeFactory {
|
||||
|
||||
ensure_and_debug_assert!(
|
||||
member_timestamps.is_empty()
|
||||
|| to.len() + past_members.len() == member_timestamps.len()
|
||||
|| to.len() + past_members.len() == member_timestamps.len(),
|
||||
"to.len() ({}) + past_members.len() ({}) != member_timestamps.len() ({})",
|
||||
to.len(),
|
||||
past_members.len(),
|
||||
member_timestamps.len(),
|
||||
);
|
||||
|
||||
let factory = MimeFactory {
|
||||
@@ -671,7 +681,11 @@ impl MimeFactory {
|
||||
|
||||
ensure_and_debug_assert!(
|
||||
self.member_timestamps.is_empty()
|
||||
|| to.len() + past_members.len() == self.member_timestamps.len()
|
||||
|| to.len() + past_members.len() == self.member_timestamps.len(),
|
||||
"to.len() ({}) + past_members.len() ({}) != self.member_timestamps.len() ({})",
|
||||
to.len(),
|
||||
past_members.len(),
|
||||
self.member_timestamps.len(),
|
||||
);
|
||||
if to.is_empty() {
|
||||
to.push(hidden_recipients());
|
||||
|
||||
@@ -181,10 +181,10 @@ pub enum SystemMessage {
|
||||
/// Chat ephemeral message timer is changed.
|
||||
EphemeralTimerChanged = 10,
|
||||
|
||||
/// "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
/// "Messages are end-to-end encrypted."
|
||||
ChatProtectionEnabled = 11,
|
||||
|
||||
/// "%1$s sent a message from another device."
|
||||
/// "%1$s sent a message from another device.", deprecated 2025-07
|
||||
ChatProtectionDisabled = 12,
|
||||
|
||||
/// Message can't be sent because of `Invalid unencrypted mail to <>`
|
||||
@@ -213,6 +213,9 @@ pub enum SystemMessage {
|
||||
|
||||
/// This message contains a users iroh node address.
|
||||
IrohNodeAddr = 40,
|
||||
|
||||
/// "Messages are end-to-end encrypted."
|
||||
ChatE2ee = 50,
|
||||
}
|
||||
|
||||
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
|
||||
@@ -246,6 +249,7 @@ impl MimeMessage {
|
||||
MimeMessage::merge_headers(
|
||||
context,
|
||||
&mut headers,
|
||||
&mut headers_removed,
|
||||
&mut recipients,
|
||||
&mut past_members,
|
||||
&mut from,
|
||||
@@ -273,6 +277,7 @@ impl MimeMessage {
|
||||
MimeMessage::merge_headers(
|
||||
context,
|
||||
&mut headers,
|
||||
&mut headers_removed,
|
||||
&mut recipients,
|
||||
&mut past_members,
|
||||
&mut from,
|
||||
@@ -446,26 +451,11 @@ impl MimeMessage {
|
||||
});
|
||||
if let (Ok(mail), true) = (mail, is_encrypted) {
|
||||
if !signatures.is_empty() {
|
||||
// Remove unsigned opportunistically protected headers from messages considered
|
||||
// Autocrypt-encrypted / displayed with padlock.
|
||||
// For "Subject" see <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
|
||||
for h in [
|
||||
HeaderDef::Subject,
|
||||
HeaderDef::ChatGroupId,
|
||||
HeaderDef::ChatGroupName,
|
||||
HeaderDef::ChatGroupNameChanged,
|
||||
HeaderDef::ChatGroupNameTimestamp,
|
||||
HeaderDef::ChatGroupAvatar,
|
||||
HeaderDef::ChatGroupMemberRemoved,
|
||||
HeaderDef::ChatGroupMemberAdded,
|
||||
HeaderDef::ChatGroupMemberTimestamps,
|
||||
HeaderDef::ChatGroupPastMembers,
|
||||
HeaderDef::ChatDelete,
|
||||
HeaderDef::ChatEdit,
|
||||
HeaderDef::ChatUserAvatar,
|
||||
] {
|
||||
remove_header(&mut headers, h.get_headername(), &mut headers_removed);
|
||||
}
|
||||
// Unsigned "Subject" mustn't be prepended to messages shown as encrypted
|
||||
// (<https://github.com/deltachat/deltachat-core-rust/issues/1790>).
|
||||
// Other headers are removed by `MimeMessage::merge_headers()` except for "List-ID".
|
||||
remove_header(&mut headers, "subject", &mut headers_removed);
|
||||
remove_header(&mut headers, "list-id", &mut headers_removed);
|
||||
}
|
||||
|
||||
// let known protected headers from the decrypted
|
||||
@@ -478,6 +468,7 @@ impl MimeMessage {
|
||||
MimeMessage::merge_headers(
|
||||
context,
|
||||
&mut headers,
|
||||
&mut headers_removed,
|
||||
&mut recipients,
|
||||
&mut past_members,
|
||||
&mut inner_from,
|
||||
@@ -1558,6 +1549,7 @@ impl MimeMessage {
|
||||
fn merge_headers(
|
||||
context: &Context,
|
||||
headers: &mut HashMap<String, String>,
|
||||
headers_removed: &mut HashSet<String>,
|
||||
recipients: &mut Vec<SingleInfo>,
|
||||
past_members: &mut Vec<SingleInfo>,
|
||||
from: &mut Option<SingleInfo>,
|
||||
@@ -1565,6 +1557,12 @@ impl MimeMessage {
|
||||
chat_disposition_notification_to: &mut Option<SingleInfo>,
|
||||
fields: &[mailparse::MailHeader<'_>],
|
||||
) {
|
||||
headers.retain(|k, _| {
|
||||
!is_protected(k) || {
|
||||
headers_removed.insert(k.to_string());
|
||||
false
|
||||
}
|
||||
});
|
||||
for field in fields {
|
||||
// lowercasing all headers is technically not correct, but makes things work better
|
||||
let key = field.get_key().to_lowercase();
|
||||
@@ -2005,6 +2003,32 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the outer header value must be ignored if the message contains a signed (and
|
||||
/// optionally encrypted) part.
|
||||
///
|
||||
/// NB: There are known cases when Subject and List-ID only appear in the outer headers of
|
||||
/// signed-only messages. Such messages are shown as unencrypted anyway.
|
||||
fn is_protected(key: &str) -> bool {
|
||||
key.starts_with("chat-")
|
||||
|| matches!(
|
||||
key,
|
||||
"return-path"
|
||||
| "auto-submitted"
|
||||
| "autocrypt-setup-message"
|
||||
| "date"
|
||||
| "from"
|
||||
| "sender"
|
||||
| "reply-to"
|
||||
| "to"
|
||||
| "cc"
|
||||
| "bcc"
|
||||
| "message-id"
|
||||
| "in-reply-to"
|
||||
| "references"
|
||||
| "secure-join"
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns if the header is hidden and must be ignored in the IMF section.
|
||||
pub(crate) fn is_hidden(key: &str) -> bool {
|
||||
matches!(
|
||||
|
||||
@@ -1402,6 +1402,26 @@ async fn test_x_microsoft_original_message_id_precedence() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_extra_imf_chat_header() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let chat_id = t.get_self_chat().await.id;
|
||||
|
||||
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
|
||||
let sent_msg = t.pop_sent_msg().await;
|
||||
// Check removal of some nonexistent "Chat-*" header to protect the code from future breakages.
|
||||
let payload = sent_msg
|
||||
.payload
|
||||
.replace("Message-ID:", "Chat-Forty-Two: 42\r\nMessage-ID:");
|
||||
let msg = MimeMessage::from_bytes(t, payload.as_bytes(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(msg.headers.contains_key("chat-version"));
|
||||
assert!(!msg.headers.contains_key("chat-forty-two"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_long_in_reply_to() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use fast_socks5::client::Socks5Stream;
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncBufRead, AsyncRead, AsyncWrite, BufStream, BufWriter};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
|
||||
pub(crate) trait SessionStream:
|
||||
@@ -9,54 +12,90 @@ pub(crate) trait SessionStream:
|
||||
{
|
||||
/// Change the read timeout on the session stream.
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>);
|
||||
|
||||
/// Returns the remote address that this stream is connected to.
|
||||
fn peer_addr(&self) -> Result<SocketAddr>;
|
||||
}
|
||||
|
||||
impl SessionStream for Box<dyn SessionStream> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.as_mut().set_read_timeout(timeout);
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.as_ref().peer_addr()
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for async_native_tls::TlsStream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().set_read_timeout(timeout);
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.get_ref().peer_addr()
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for tokio_rustls::client::TlsStream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().0.set_read_timeout(timeout);
|
||||
}
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.get_ref().0.peer_addr()
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for BufStream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().set_read_timeout(timeout);
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.get_ref().peer_addr()
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for BufWriter<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().set_read_timeout(timeout);
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.get_ref().peer_addr()
|
||||
}
|
||||
}
|
||||
impl<T: AsyncRead + AsyncWrite + Send + Sync + std::fmt::Debug> SessionStream
|
||||
for Pin<Box<TimeoutStream<T>>>
|
||||
{
|
||||
impl SessionStream for Pin<Box<TimeoutStream<TcpStream>>> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.as_mut().set_read_timeout_pinned(timeout);
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
Ok(self.get_ref().peer_addr()?)
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for Socks5Stream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_socket_mut().set_read_timeout(timeout)
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.get_socket_ref().peer_addr()
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for shadowsocks::ProxyClientStream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().set_read_timeout(timeout)
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.get_ref().peer_addr()
|
||||
}
|
||||
}
|
||||
impl<T: SessionStream> SessionStream for async_imap::DeflateStream<T> {
|
||||
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.get_mut().set_read_timeout(timeout)
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> Result<SocketAddr> {
|
||||
self.get_ref().peer_addr()
|
||||
}
|
||||
}
|
||||
|
||||
/// Session stream with a read buffer.
|
||||
|
||||
@@ -407,6 +407,7 @@ mod tests {
|
||||
use crate::message::{MessageState, delete_msgs};
|
||||
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
|
||||
use crate::sql::housekeeping;
|
||||
use crate::test_utils::E2EE_INFO_MSGS;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools::SystemTime;
|
||||
@@ -653,13 +654,25 @@ Here's my footer -- bob@example.net"
|
||||
let chat_alice = alice.create_chat(&bob).await;
|
||||
let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
|
||||
let bob_msg = bob.recv_msg(&alice_msg).await;
|
||||
assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 1);
|
||||
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 1);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&alice, chat_alice.id).await?.len(),
|
||||
E2EE_INFO_MSGS + 1
|
||||
);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 1
|
||||
);
|
||||
|
||||
let alice_msg2 = alice.send_text(chat_alice.id, "Hi again!").await;
|
||||
bob.recv_msg(&alice_msg2).await;
|
||||
assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2);
|
||||
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&alice, chat_alice.id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
|
||||
bob_msg.chat_id.accept(&bob).await?;
|
||||
|
||||
@@ -667,12 +680,18 @@ Here's my footer -- bob@example.net"
|
||||
send_reaction(&bob, bob_msg.id, "👍").await.unwrap();
|
||||
expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
|
||||
expect_no_unwanted_events(&bob).await;
|
||||
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
|
||||
let bob_reaction_msg = bob.pop_sent_msg().await;
|
||||
let alice_reaction_msg = alice.recv_msg_hidden(&bob_reaction_msg).await;
|
||||
assert_eq!(alice_reaction_msg.state, MessageState::InFresh);
|
||||
assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&alice, chat_alice.id).await?.len(),
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
|
||||
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
|
||||
assert_eq!(reactions.to_string(), "👍1");
|
||||
|
||||
@@ -1457,7 +1457,10 @@ async fn do_chat_assignment(
|
||||
false => None,
|
||||
};
|
||||
if let Some(chat) = chat {
|
||||
ensure_and_debug_assert!(chat.typ == Chattype::Single);
|
||||
ensure_and_debug_assert!(
|
||||
chat.typ == Chattype::Single,
|
||||
"Chat {chat_id} is not Single",
|
||||
);
|
||||
let mut new_protection = match verified_encryption {
|
||||
VerifiedEncryption::Verified => ProtectionStatus::Protected,
|
||||
VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
|
||||
@@ -2142,7 +2145,7 @@ RETURNING id
|
||||
// afterwards insert additional parts.
|
||||
replace_msg_id = None;
|
||||
|
||||
ensure_and_debug_assert!(!row_id.is_special());
|
||||
ensure_and_debug_assert!(!row_id.is_special(), "Rowid {row_id} is special");
|
||||
created_db_entries.push(row_id);
|
||||
}
|
||||
|
||||
@@ -2406,7 +2409,9 @@ async fn lookup_chat_by_reply(
|
||||
// as we can directly assign the message to the chat
|
||||
// by its group ID.
|
||||
ensure_and_debug_assert!(
|
||||
mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted()
|
||||
mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted(),
|
||||
"Encrypted message has group ID {}",
|
||||
mime_parser.get_chat_group_id().unwrap_or_default(),
|
||||
);
|
||||
|
||||
// Try to assign message to the same chat as the parent message.
|
||||
@@ -3637,7 +3642,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@ use crate::download::MIN_DOWNLOAD_LIMIT;
|
||||
use crate::imap::prefetch_should_download;
|
||||
use crate::imex::{ImexMode, imex};
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::mark_as_verified;
|
||||
use crate::test_utils::{TestContext, TestContextManager, get_chat_msg};
|
||||
use crate::test_utils::{
|
||||
E2EE_INFO_MSGS, TestContext, TestContextManager, get_chat_msg, mark_as_verified,
|
||||
};
|
||||
use crate::tools::{SystemTime, time};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -133,7 +134,7 @@ async fn test_adhoc_group_outgoing_show_accepted_contact_unaccepted() -> Result<
|
||||
let chats = Chatlist::try_load(bob, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chat_id = chats.get_chat_id(0)?;
|
||||
assert_eq!(chat_id.get_msg_cnt(bob).await?, 1);
|
||||
assert_eq!(chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3682,6 +3683,24 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_ignore_protected_headers_in_outer_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
|
||||
send_text_msg(bob, bob_chat_id, "hi all!".to_string()).await?;
|
||||
let mut sent_msg = bob.pop_sent_msg().await;
|
||||
sent_msg.payload = sent_msg.payload.replace(
|
||||
"Chat-Version:",
|
||||
"Auto-Submitted: auto-generated\r\nChat-Version:",
|
||||
);
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
let ab_contact = alice.add_or_lookup_contact(bob).await;
|
||||
assert!(!ab_contact.is_bot());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_member_list_on_rejoin() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -4392,7 +4411,7 @@ async fn test_create_group_with_big_msg() -> Result<()> {
|
||||
|
||||
// The big message must go away from the 1:1 chat.
|
||||
let msgs = chat::get_chat_msgs(&alice, ab_chat_id).await?;
|
||||
assert!(msgs.is_empty());
|
||||
assert_eq!(msgs.len(), E2EE_INFO_MSGS);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::{self, chat_protection_enabled};
|
||||
use crate::stock_str::{self, messages_e2e_encrypted};
|
||||
use crate::test_utils::{
|
||||
TestContext, TestContextManager, TimeShiftFalsePositiveNote, get_chat_msg,
|
||||
};
|
||||
@@ -246,7 +246,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
let chat = alice.get_chat(&bob).await;
|
||||
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
|
||||
assert!(msg.is_info());
|
||||
let expected_text = chat_protection_enabled(&alice).await;
|
||||
let expected_text = messages_e2e_encrypted(&alice).await;
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
if case == SetupContactCase::CheckProtectionTimestamp {
|
||||
assert_eq!(msg.timestamp_sort, vc_request_with_auth_ts_sent + 1);
|
||||
@@ -296,7 +296,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_text(), chat_protection_enabled(&bob).await);
|
||||
assert_eq!(msg.get_text(), messages_e2e_encrypted(&bob).await);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -540,7 +540,7 @@ async fn test_secure_join() -> Result<()> {
|
||||
// - You added member bob@example.net
|
||||
let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await;
|
||||
assert!(msg.is_info());
|
||||
let expected_text = chat_protection_enabled(&alice).await;
|
||||
let expected_text = messages_e2e_encrypted(&alice).await;
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
}
|
||||
|
||||
|
||||
@@ -1505,19 +1505,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 +1651,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 +1659,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 +1687,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 +1704,7 @@ fn migrate_key_contacts(
|
||||
if grpid.is_empty() {
|
||||
// Ad-hoc group that has empty Chat-Group-ID
|
||||
// because it was created in response to receiving a non-chat email.
|
||||
keep_address_contacts("Empty chat-Group-ID");
|
||||
keep_address_contacts("Empty chat-Group-ID")?;
|
||||
continue;
|
||||
} else if protected == 1 {
|
||||
old_members
|
||||
@@ -1711,7 +1723,7 @@ fn migrate_key_contacts(
|
||||
|
||||
// Mailinglist
|
||||
140 => {
|
||||
keep_address_contacts("Mailinglist");
|
||||
keep_address_contacts("Mailinglist")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1750,7 +1762,7 @@ fn migrate_key_contacts(
|
||||
transaction
|
||||
.execute("UPDATE chats SET grpid='' WHERE id=?", (chat_id,))
|
||||
.context("Step 26.1")?;
|
||||
keep_address_contacts("Group contains contact without peerstate");
|
||||
keep_address_contacts("Group contains contact without peerstate")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -65,9 +65,6 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "GIF"))]
|
||||
Gif = 23,
|
||||
|
||||
#[strum(props(fallback = "Encrypted message"))]
|
||||
EncryptedMsg = 24,
|
||||
|
||||
#[strum(props(fallback = "End-to-end encryption available"))]
|
||||
E2eAvailable = 25,
|
||||
|
||||
@@ -380,9 +377,10 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "I left the group."))]
|
||||
MsgILeftGroup = 166,
|
||||
|
||||
#[strum(props(fallback = "Messages are guaranteed to be end-to-end encrypted from now on."))]
|
||||
#[strum(props(fallback = "Messages are end-to-end encrypted."))]
|
||||
ChatProtectionEnabled = 170,
|
||||
|
||||
// deprecated 2025-07
|
||||
#[strum(props(fallback = "%1$s sent a message from another device."))]
|
||||
ChatProtectionDisabled = 171,
|
||||
|
||||
@@ -1031,8 +1029,8 @@ pub(crate) async fn error_no_network(context: &Context) -> String {
|
||||
translated(context, StockMessage::ErrorNoNetwork).await
|
||||
}
|
||||
|
||||
/// Stock string: `Messages are guaranteed to be end-to-end encrypted from now on.`
|
||||
pub(crate) async fn chat_protection_enabled(context: &Context) -> String {
|
||||
/// Stock string: `Messages are end-to-end encrypted.`
|
||||
pub(crate) async fn messages_e2e_encrypted(context: &Context) -> String {
|
||||
translated(context, StockMessage::ChatProtectionEnabled).await
|
||||
}
|
||||
|
||||
@@ -1303,7 +1301,7 @@ impl Context {
|
||||
"[Error] No contact_id given".to_string()
|
||||
}
|
||||
}
|
||||
ProtectionStatus::Protected => chat_protection_enabled(self).await,
|
||||
ProtectionStatus::Protected => messages_e2e_encrypted(self).await,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,10 @@ use crate::securejoin::{get_securejoin_qr, join_securejoin};
|
||||
use crate::stock_str::StockStrings;
|
||||
use crate::tools::time;
|
||||
|
||||
/// The number of info messages added to new e2ee chats.
|
||||
/// Currently this is "End-to-end encryption available", string `E2eAvailable`.
|
||||
pub const E2EE_INFO_MSGS: usize = 1;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png");
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ use crate::mimeparser::SystemMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::{get_securejoin_qr, join_securejoin};
|
||||
use crate::stock_str;
|
||||
use crate::test_utils::{TestContext, TestContextManager, get_chat_msg, mark_as_verified};
|
||||
use crate::test_utils::{
|
||||
E2EE_INFO_MSGS, TestContext, TestContextManager, get_chat_msg, mark_as_verified,
|
||||
};
|
||||
use crate::tools::SystemTime;
|
||||
use crate::{e2ee, message};
|
||||
|
||||
@@ -132,7 +134,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
|
||||
assert!(chat.is_protected());
|
||||
|
||||
let msg = get_chat_msg(&alice, chat.id, 0, 1).await;
|
||||
let expected_text = stock_str::chat_protection_enabled(&alice).await;
|
||||
let expected_text = stock_str::messages_e2e_encrypted(&alice).await;
|
||||
assert_eq!(msg.text, expected_text);
|
||||
}
|
||||
|
||||
@@ -142,7 +144,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
|
||||
assert!(chat.is_protected());
|
||||
|
||||
let msg0 = get_chat_msg(&fiona, chat.id, 0, 1).await;
|
||||
let expected_text = stock_str::chat_protection_enabled(&fiona).await;
|
||||
let expected_text = stock_str::messages_e2e_encrypted(&fiona).await;
|
||||
assert_eq!(msg0.text, expected_text);
|
||||
}
|
||||
|
||||
@@ -162,7 +164,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
|
||||
let chat = alice.get_chat(&fiona_new).await;
|
||||
assert!(!chat.is_protected());
|
||||
|
||||
let msg = get_chat_msg(&alice, chat.id, 0, 1).await;
|
||||
let msg = get_chat_msg(&alice, chat.id, 1, E2EE_INFO_MSGS + 1).await;
|
||||
assert_eq!(msg.text, "I have a new device");
|
||||
|
||||
// After recreating the chat, it should still be unprotected
|
||||
@@ -268,7 +270,7 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> {
|
||||
.await?;
|
||||
|
||||
let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 1).await;
|
||||
let enabled = stock_str::chat_protection_enabled(&alice).await;
|
||||
let enabled = stock_str::messages_e2e_encrypted(&alice).await;
|
||||
assert_eq!(msg0.text, enabled);
|
||||
assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatProtectionEnabled);
|
||||
|
||||
|
||||
12
src/tools.rs
12
src/tools.rs
@@ -767,9 +767,10 @@ pub(crate) fn inc_and_check<T: PrimInt + AddAssign + std::fmt::Debug>(
|
||||
/// In non-optimized builds, panics instead if so.
|
||||
#[macro_export]
|
||||
macro_rules! ensure_and_debug_assert {
|
||||
($($arg:tt)*) => {
|
||||
debug_assert!($($arg)*);
|
||||
anyhow::ensure!($($arg)*);
|
||||
($cond:expr, $($arg:tt)*) => {
|
||||
let cond_val = $cond;
|
||||
debug_assert!(cond_val, $($arg)*);
|
||||
anyhow::ensure!(cond_val, $($arg)*);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -806,10 +807,11 @@ macro_rules! ensure_and_debug_assert_ne {
|
||||
#[macro_export]
|
||||
macro_rules! logged_debug_assert {
|
||||
($ctx:expr, $cond:expr, $($arg:tt)*) => {
|
||||
if !$cond {
|
||||
let cond_val = $cond;
|
||||
if !cond_val {
|
||||
warn!($ctx, $($arg)*);
|
||||
}
|
||||
debug_assert!($cond, $($arg)*);
|
||||
debug_assert!(cond_val, $($arg)*);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::config::Config;
|
||||
use crate::download::DownloadState;
|
||||
use crate::ephemeral;
|
||||
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager};
|
||||
use crate::tools::{self, SystemTime};
|
||||
use crate::{message, sql};
|
||||
|
||||
@@ -250,7 +250,7 @@ async fn test_resend_webxdc_instance_and_info() -> Result<()> {
|
||||
);
|
||||
let bob_grp = bob_instance.chat_id;
|
||||
assert_eq!(bob.get_last_msg_in(bob_grp).await.id, bob_instance.id);
|
||||
assert_eq!(bob_grp.get_msg_cnt(&bob).await?, 1);
|
||||
assert_eq!(bob_grp.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -869,14 +869,14 @@ async fn test_send_big_webxdc_status_update() -> Result<()> {
|
||||
let sent2 = &alice.pop_sent_msg().await;
|
||||
let alice_update = sent2.load_from_db().await;
|
||||
assert_eq!(alice_update.text, BODY_DESCR.to_string());
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1);
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 1);
|
||||
|
||||
// Bob receives the instance.
|
||||
let bob_instance = bob.recv_msg(sent1).await;
|
||||
let bob_chat_id = bob_instance.chat_id;
|
||||
assert_eq!(bob_instance.rfc724_mid, alice_instance.rfc724_mid);
|
||||
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 1);
|
||||
|
||||
// Bob receives the status updates.
|
||||
bob.recv_msg_trash(sent2).await;
|
||||
@@ -896,7 +896,7 @@ async fn test_send_big_webxdc_status_update() -> Result<()> {
|
||||
r#"[{"payload":{"foo":"bar2"},"serial":2,"max_serial":3},
|
||||
{"payload":{"foo":"bar3"},"serial":3,"max_serial":3}]"#
|
||||
);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1485,7 +1485,7 @@ async fn test_webxdc_info_msg() -> Result<()> {
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
|
||||
let sent1 = &alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1);
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 1);
|
||||
|
||||
alice
|
||||
.send_webxdc_status_update(
|
||||
@@ -1495,7 +1495,7 @@ async fn test_webxdc_info_msg() -> Result<()> {
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
let sent2 = &alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2);
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 2);
|
||||
let info_msg = alice.get_last_msg().await;
|
||||
assert!(info_msg.is_info());
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage);
|
||||
@@ -1517,7 +1517,7 @@ async fn test_webxdc_info_msg() -> Result<()> {
|
||||
let bob_instance = bob.recv_msg(sent1).await;
|
||||
let bob_chat_id = bob_instance.chat_id;
|
||||
bob.recv_msg_trash(sent2).await;
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 2);
|
||||
let info_msg = bob.get_last_msg().await;
|
||||
assert!(info_msg.is_info());
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage);
|
||||
@@ -1536,7 +1536,10 @@ async fn test_webxdc_info_msg() -> Result<()> {
|
||||
let alice2_instance = alice2.recv_msg(sent1).await;
|
||||
let alice2_chat_id = alice2_instance.chat_id;
|
||||
alice2.recv_msg_trash(sent2).await;
|
||||
assert_eq!(alice2_chat_id.get_msg_cnt(&alice2).await?, 2);
|
||||
assert_eq!(
|
||||
alice2_chat_id.get_msg_cnt(&alice2).await?,
|
||||
E2EE_INFO_MSGS + 2
|
||||
);
|
||||
let info_msg = alice2.get_last_msg().await;
|
||||
assert!(info_msg.is_info());
|
||||
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage);
|
||||
@@ -1572,13 +1575,13 @@ async fn test_webxdc_info_msg_cleanup_series() -> Result<()> {
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
let sent2 = &alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2);
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 2);
|
||||
alice
|
||||
.send_webxdc_status_update(alice_instance.id, r#"{"info":"i2", "payload":2}"#)
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
let sent3 = &alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2);
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 2);
|
||||
let info_msg = alice.get_last_msg().await;
|
||||
assert_eq!(info_msg.get_text(), "i2");
|
||||
|
||||
@@ -1586,9 +1589,9 @@ async fn test_webxdc_info_msg_cleanup_series() -> Result<()> {
|
||||
let bob_instance = bob.recv_msg(sent1).await;
|
||||
let bob_chat_id = bob_instance.chat_id;
|
||||
bob.recv_msg_trash(sent2).await;
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 2);
|
||||
bob.recv_msg_trash(sent3).await;
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2);
|
||||
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 2);
|
||||
let info_msg = bob.get_last_msg().await;
|
||||
assert_eq!(info_msg.get_text(), "i2");
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
Group#Chat#10: Group chat [3 member(s)]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10🔒: (Contact#Contact#10): Hi! I created a group. [FRESH]
|
||||
Msg#11🔒: Me (Contact#Contact#Self): You left. [INFO] √
|
||||
Msg#12🔒: (Contact#Contact#10): Member charlie@example.net added by alice@example.org. [FRESH][INFO]
|
||||
Msg#13🔒: (Contact#Contact#10): What a silence! [FRESH]
|
||||
Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
|
||||
Msg#11🔒: (Contact#Contact#10): Hi! I created a group. [FRESH]
|
||||
Msg#12🔒: Me (Contact#Contact#Self): You left. [INFO] √
|
||||
Msg#13🔒: (Contact#Contact#10): Member charlie@example.net added by alice@example.org. [FRESH][INFO]
|
||||
Msg#14🔒: (Contact#Contact#10): What a silence! [FRESH]
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Single#Chat#10: bob@example.net [KEY bob@example.net] 🛡️
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO 🛡️]
|
||||
Msg#11🔒: Me (Contact#Contact#Self): Test – This is encrypted, signed, and has an Autocrypt Header without prefer-encrypt=mutual. √
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Single#Chat#10: bob@example.net [KEY bob@example.net] 🛡️
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO 🛡️]
|
||||
Msg#11🔒: (Contact#Contact#10): Heyho from DC [FRESH]
|
||||
Msg#13🔒: Me (Contact#Contact#Self): Sending with DC again √
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -4,6 +4,6 @@ Msg#11: info (Contact#Contact#Info): alice@example.org invited you to join this
|
||||
|
||||
Waiting for the device of alice@example.org to reply… [NOTICED][INFO]
|
||||
Msg#13: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO]
|
||||
Msg#17: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
|
||||
Msg#17: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO 🛡️]
|
||||
Msg#18🔒: (Contact#Contact#10): Member Me added by alice@example.org. [FRESH][INFO]
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user