Compare commits

..

23 Commits

Author SHA1 Message Date
Hocuri
51f9279e67 chore(release): prepare for 2.6.0 2025-07-23 11:47:05 +02:00
Hocuri
f27d54f7fa fix: Fix crash when receiving a verification-gossiping message which a contact also sends to itself (#7032)
This should fix https://github.com/chatmail/core/issues/7030; @r10s if
you could test whether it fixes your problem

The crash happened if you received a message that has the from contact
also in the "To: " and "Chat-Group-Member-Fpr: " headers. Not sure how
it happened that such a message was created.
2025-07-23 11:09:11 +02:00
link2xt
7f3648f8ae chore(release): prepare for 2.5.0 2025-07-22 14:21:07 +00:00
link2xt
49fc258578 fix: do not ignore errors in add_flag_finalized_with_set 2025-07-22 14:17:30 +00:00
link2xt
0c51b4fe41 docs(STYLE.md): prefer try_next() over next() 2025-07-22 12:33:37 +00:00
bjoern
dbad714539 docs: clarify the meaning of is_verified() vs verifier_id() (#7027)
this PR adapts the documentation UI guidance to recent "green checkmark"
discussions

cmp https://github.com/deltachat/deltachat-pages/pull/1145,
https://github.com/deltachat/deltachat-ios/pull/2781,
https://github.com/deltachat/deltachat-android/pull/3828,
https://github.com/deltachat/deltachat-desktop/pull/5318

---------

Co-authored-by: Hocuri <hocuri@gmx.de>
2025-07-22 10:40:12 +02:00
Hocuri
edd8008650 fix: Mark all email chats as unprotected in the migration (#7026)
Previously, chat protection was only removed if the chat became an
email-chat because the key-contact has a different email, but not if the
chat became an email-chat for a different reason.

Therefore, it could happen that the migration produced a protected,
unencrypted chat, where you couldn't write any messages.

I tested that applying this fix actually fixes the bug I had.
2025-07-21 20:15:59 +00:00
Hocuri
615a1b3f4e fix: Correctly migrate "verified by me" 2025-07-21 18:28:19 +00:00
bjoern
fe6044e1aa docs: deprecate protection-broken and related stuff (#7018)
came over these parts while targeting the new info message of
https://github.com/chatmail/core/pull/7008 in
https://github.com/deltachat/deltachat-ios/pull/2778 and
https://github.com/deltachat/deltachat-android/pull/3822

---------

Co-authored-by: Hocuri <hocuri@gmx.de>
2025-07-21 18:40:00 +02:00
link2xt
46b275bfab chore(release): prepare for 2.4.0 2025-07-21 15:08:00 +00:00
link2xt
25f44c517a chore: update async-imap to 0.11.0 2025-07-21 15:03:15 +00:00
link2xt
cac04f8ee4 refactor: use try_next() when processing FETCH responses 2025-07-21 13:55:02 +00:00
link2xt
45d8566ec0 fix: do not ignore errors when draining FETCH responses 2025-07-21 13:55:02 +00:00
link2xt
29a98ba13b fix: update tokio-io-timeout to 1.2.1
tokio-io-timeout 1.2.0 used previously
did not reset the timeout when returning
timeout error. This resulted
in infinite loop spamming
the log with messages that look like this and using 100% CPU:

    Read error on stream 192.168.1.20:993 after reading 9118 and writing 1036 bytes: timed out.
    Read error on stream 192.168.1.20:993 after reading 9118 and writing 1036 bytes: timed out.
    Read error on stream 192.168.1.20:993 after reading 9118 and writing 1036 bytes: timed out.
    Read error on stream 192.168.1.20:993 after reading 9118 and writing 1036 bytes: timed out.
    Read error on stream 192.168.1.20:993 after reading 9118 and writing 1036 bytes: timed out.
    Read error on stream 192.168.1.20:993 after reading 9118 and writing 1036 bytes: timed out.

Normally these messages should be separated by at least 1 minute timeout.

The reason for infinite loop is not figured out yet,
but this change should at least fix 100% CPU usage.

See <https://github.com/sfackler/tokio-io-timeout/issues/13>
for the bugreport and
<https://github.com/sfackler/tokio-io-timeout/pull/14>
for the bugfix.
2025-07-20 10:53:35 +00:00
link2xt
e3973f6448 chore(release): prepare for 2.3.0 2025-07-19 11:58:13 +00:00
link2xt
7b41425fe4 fix: save peer address for LoggingStream early
Socket may lose peer address when it is disconnected from
the server side. In this case debug_assert! failed
for me when running the core in debug mode on desktop.
To avoid the case of peer_addr not being available,
we now store it when LoggingStream is created.
2025-07-19 10:58:35 +00:00
bjoern
2c7d51f98f feat: add "e2ee encrypted" info message to all e2ee chats (#7008)
this PR adds a info message "messages are end-to-end-encrypted" also for
chats created by eg. vcards. by the removal of lock icons, this is a
good place to hint for that in addition; this is also what eg. whatsapp
and others are doing

the wording itself is tweaked at
https://github.com/deltachat/deltachat-android/pull/3817 (and there is
also the rough idea to make the message a little more outstanding, by
some more dedicated colors)

~~did not test in practise, if this leads to double "e2ee info messages"
on secure join, tests look good, however.~~ EDIT: did lots of practise
tests meanwhile :)

most of the changes in this PR are about test ...

ftr, in another PR, after 2.0 reeases, there could probably quite some
code cleanup wrt set-protection, protection-disabled etc.

---------

Co-authored-by: Hocuri <hocuri@gmx.de>
2025-07-18 22:08:33 +02:00
l
a2df29515a feat: log the number of read/written bytes on IMAP stream read error (#6924) 2025-07-17 20:01:16 +00:00
link2xt
6df1d165dd feat: log when background fetch of all accounts finishes successfully 2025-07-17 15:46:46 +00:00
cliffmccarthy
e03e2d9a68 fix: List e-mail contacts in repl listcontacts command
- After the revisions to support key contacts, the 'listcontacts'
  command in the repl only lists key-contacts.  A separate flag now
  has to be passed to Contact::get_all() to list address contacts.
  This makes it difficult to run the example in README.md because we
  need to see the new contact so we can get its ID for the next
  command in the example, that creates a chat by ID.
- Revised 'listcontacts' command to make a second call to
  Contact::get_all() with the DC_GCL_ADDRESS flag, then print the
  e-mail contacts after the key contacts.
- Revised configuration example in top-level README.md to reflect
  current command output.

fixes #7011
2025-07-17 10:27:14 +02:00
iequidoo
8fc6ea19b4 feat: {ensure_and,logged}_debug_assert: Don't evaluate condition twice 2025-07-16 11:41:45 -03:00
iequidoo
c5c947e175 feat(repl): Print errors and debug logs to stderr
Follow-up to 545007aca5.
2025-07-16 11:39:56 -03:00
iequidoo
6d8dff54a7 fix: Ignore protected headers in outer message part (#6357)
Delta Chat always adds protected headers to the inner encrypted or signed message, so if a protected
header is only present in the outer part, it should be ignored because it's probably added by the
server or somebody else. The exceptions are Subject and List-ID because there are known cases when
they are only present in the outer message part.

Also treat any Chat-* headers as protected. This fixes e.g. a case when the server injects a
"Chat-Version" IMF header tricking Delta Chat into thinking that it's a chat message.

Also handle "Auto-Submitted" and "Autocrypt-Setup-Message" as protected headers on the receiver
side, this was apparently forgotten.
2025-07-16 11:39:06 -03:00
64 changed files with 864 additions and 315 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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

View File

@@ -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:

View File

@@ -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`

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "2.2.0"
version = "2.6.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -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."

View File

@@ -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"

View File

@@ -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,

View File

@@ -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

View File

@@ -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().
///

View File

@@ -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,

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.2.0"
"version": "2.6.0"
}

View File

@@ -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);
});

View File

@@ -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"

View File

@@ -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" => {

View File

@@ -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;
}
}

View File

@@ -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",

View File

@@ -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."""

View File

@@ -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

View File

@@ -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!"

View File

@@ -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"

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.2.0"
"version": "2.6.0"
}

View File

@@ -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" },
]

View File

@@ -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"

View File

@@ -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

View File

@@ -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")

View File

@@ -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):

View File

@@ -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")

View File

@@ -1 +1 @@
2025-07-14
2025-07-23

View File

@@ -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].

View File

@@ -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 {

View File

@@ -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(())
}

View File

@@ -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.

View File

@@ -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(),

View File

@@ -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;

View File

@@ -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(())
}

View File

@@ -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?

View File

@@ -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(())

View File

@@ -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

View File

@@ -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?;

View File

@@ -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
View 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()
}
}

View File

@@ -963,6 +963,7 @@ impl Message {
| SystemMessage::SecurejoinMessage
| SystemMessage::LocationStreamingEnabled
| SystemMessage::LocationOnly
| SystemMessage::ChatE2ee
| SystemMessage::ChatProtectionEnabled
| SystemMessage::ChatProtectionDisabled
| SystemMessage::InvalidUnencryptedMail

View File

@@ -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(())
}

View File

@@ -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());

View File

@@ -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!(

View File

@@ -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;

View File

@@ -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.

View File

@@ -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");

View File

@@ -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;
}

View File

@@ -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(())
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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,
}
}

View File

@@ -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");

View File

@@ -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);

View File

@@ -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)*);
};
}

View File

@@ -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");

View File

@@ -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]
--------------------------------------------------------------------------------

View File

@@ -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. √
--------------------------------------------------------------------------------

View File

@@ -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 √
--------------------------------------------------------------------------------

View File

@@ -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]
--------------------------------------------------------------------------------