Compare commits

..

1 Commits

Author SHA1 Message Date
ivn
11b7a5c9d4 fix!: Use Viewtype::File for messages with invalid images, images of unknown size, images > 50 Mpx (#6825)
BREAKING CHANGE: messages with invalid images, images of unknown size,
huge images, will have Viewtype::File

After changing the logic of Viewtype selection, I had to fix 3 old tests
that used invalid Base64 image data.

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2025-07-10 17:54:56 -03:00
62 changed files with 426 additions and 1385 deletions

View File

@@ -1,86 +1,5 @@
# Changelog
## [2.4.0] - 2025-07-21
### Fixes
- Do not ignore errors when draining FETCH responses. This avoids IMAP loop getting stuck in an infinite loop retrying reading from the connection.
- Update `tokio-io-timeout` to 1.2.1. This release includes a fix to reset timeout after every error, so timeout error is returned at most once a minute if read is attempted after a timeout.
### Miscellaneous Tasks
- Update async-imap to 0.11.0.
### Refactor
- Use `try_next()` when processing FETCH responses.
## [2.3.0] - 2025-07-19
### Features / Changes
- Add "e2ee encrypted" info message to all e2ee chats ([#7008](https://github.com/chatmail/core/pull/7008)).
- repl: Print errors and debug logs to stderr.
- `{ensure_and,logged}_debug_assert`: Don't evaluate condition twice.
- Log when background fetch of all accounts finishes successfully.
- Log the number of read/written bytes on IMAP stream read error ([#6924](https://github.com/chatmail/core/pull/6924)).
### Fixes
- Ignore protected headers in outer message part ([#6357](https://github.com/chatmail/core/pull/6357)).
- List e-mail contacts in repl listcontacts command.
- Save peer address for LoggingStream early.
## [2.2.0] - 2025-07-14
### API-Changes
- Add chat::create_group_ex(), deprecate create_group_chat() ([#6927](https://github.com/chatmail/core/pull/6927)).
- jsonrpc: Add CommandApi::create_group_chat_unencrypted() ([#6927](https://github.com/chatmail/core/pull/6927)).
- [**breaking**] In ChatListItem, replace is_group and is_(out_)broadcast with chat_type property ([#7003](https://github.com/chatmail/core/pull/7003)).
### Features / Changes
- Log failed debug assertions in all configurations.
- Donation request device message ([#6913](https://github.com/chatmail/core/pull/6913)).
- Advance next UID even if connection fails while fetching.
### Fixes
- Always prefer the last header.
### Tests
- Tune down DELTACHAT_SAVE_TMP_DB hint ([#6998](https://github.com/chatmail/core/pull/6998)).
- Unencrypted group creation ([#6927](https://github.com/chatmail/core/pull/6927)).
## [2.1.0] - 2025-07-11
### Features / Changes
- Add account ordering functionality ([#6993](https://github.com/chatmail/core/pull/6993)).
- feat: Make it possible to leave broadcast channels ([#6984](https://github.com/chatmail/core/pull/6984))
- Migrations: Use tools::Time to measure time for logging.
- Log emitted logging events with `tracing`.
- Ensure_and_debug_assert{,_eq,_ne} macros combining `debug_assert*` and anyhow::ensure ([#6907](https://github.com/chatmail/core/pull/6907)).
### Fixes
- Use Viewtype::File for messages with invalid images, images of unknown size, images > 50 Mpx ([#6825](https://github.com/chatmail/core/pull/6825)).
- Don't apply chat name and avatar changes from non-members.
### Documentation
- Update showpadlock ffi.
### Miscellaneous Tasks
- cargo: Update cordyceps from 0.3.2 to 0.3.4.
### Tests
- Add option to save database on test failure ([#6992](https://github.com/chatmail/core/pull/6992)).
## [2.0.0] - 2025-07-09
This release changes the way the core handles contact keys.
@@ -6507,7 +6426,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[1.159.5]: https://github.com/chatmail/core/compare/v1.159.4..v1.159.5
[1.160.0]: https://github.com/chatmail/core/compare/v1.159.5..v1.160.0
[2.0.0]: https://github.com/chatmail/core/compare/v1.160.0..v2.0.0
[2.1.0]: https://github.com/chatmail/core/compare/v2.0.0..v2.1.0
[2.2.0]: https://github.com/chatmail/core/compare/v2.1.0..v2.2.0
[2.3.0]: https://github.com/chatmail/core/compare/v2.2.0..v2.3.0
[2.4.0]: https://github.com/chatmail/core/compare/v2.3.0..v2.4.0

63
Cargo.lock generated
View File

@@ -268,9 +268,9 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.11.0"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9f9a9c94a403cf46aa2b4cecbceefc6e4284441ebbeca79b80f3bab4394458"
checksum = "ca726c61b73c471f531b65e83e161776ba62c2b6ba4ec73d51fad357009ed00a"
dependencies = [
"async-channel 2.3.1",
"async-compression",
@@ -958,11 +958,11 @@ checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8"
[[package]]
name = "cordyceps"
version = "0.3.4"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a"
checksum = "ec10f0a762d93c4498d2e97a333805cb6250d60bead623f71d8034f9a4152ba3"
dependencies = [
"loom",
"loom 0.5.6",
"tracing",
]
@@ -1285,7 +1285,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.4.0"
version = "2.0.0"
dependencies = [
"anyhow",
"async-broadcast",
@@ -1395,7 +1395,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.4.0"
version = "2.0.0"
dependencies = [
"anyhow",
"async-channel 2.3.1",
@@ -1417,7 +1417,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "2.4.0"
version = "2.0.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1433,7 +1433,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.4.0"
version = "2.0.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1462,7 +1462,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.4.0"
version = "2.0.0"
dependencies = [
"anyhow",
"deltachat",
@@ -2205,6 +2205,19 @@ dependencies = [
"slab",
]
[[package]]
name = "generator"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
dependencies = [
"cc",
"libc",
"log",
"rustversion",
"windows 0.48.0",
]
[[package]]
name = "generator"
version = "0.8.4"
@@ -3327,6 +3340,19 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "loom"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
dependencies = [
"cfg-if",
"generator 0.7.5",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "loom"
version = "0.7.2"
@@ -3334,7 +3360,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [
"cfg-if",
"generator",
"generator 0.8.4",
"scoped-tls",
"tracing",
"tracing-subscriber",
@@ -3451,7 +3477,7 @@ dependencies = [
"crossbeam-channel",
"crossbeam-epoch",
"crossbeam-utils",
"loom",
"loom 0.7.2",
"parking_lot",
"portable-atomic",
"rustc_version",
@@ -6073,9 +6099,9 @@ dependencies = [
[[package]]
name = "tokio-io-timeout"
version = "1.2.1"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bd86198d9ee903fedd2f9a2e72014287c0d9167e4ae43b5853007205dda1b76"
checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
dependencies = [
"pin-project-lite",
"tokio",
@@ -6724,6 +6750,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows"
version = "0.52.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.4.0"
version = "2.0.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.11.0", default-features = false, features = ["runtime-tokio", "compress"] }
async-imap = { version = "0.10.4", default-features = false, features = ["runtime-tokio", "compress"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
@@ -101,7 +101,7 @@ strum_macros = "0.27"
tagger = "4.3.4"
textwrap = "0.16.2"
thiserror = { workspace = true }
tokio-io-timeout = "1.2.1"
tokio-io-timeout = "1.2.0"
tokio-rustls = { version = "0.26.2", default-features = false }
tokio-stream = { version = "0.1.17", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar

View File

@@ -84,29 +84,26 @@ Create a contact:
```
> addcontact yourfriends@email.org
Command executed successfully.
```
List contacts:
```
> listcontacts
Contact#Contact#Self: Me √ <your@email.org>
1 key contacts.
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
1 address contacts.
Contact#10: <name unset> <yourfriends@email.org>
Contact#1: Me √√ <your@email.org>
```
Create a chat with your friend and send a message:
```
> createchat 10
Single#Chat#12 created successfully.
> chat 12
Selecting chat Chat#12
Single#Chat#12: yourfriends@email.org [yourfriends@email.org] Icon: profile-db-blobs/4138c52e5bc1c576cda7dd44d088c07.png
0 messages.
81.252µs to create this list, 123.625µs to mark all messages as noticed.
Single#10 created successfully.
> chat 10
Single#10: yourfriends@email.org [yourfriends@email.org]
> send hi
Message sent.
```
List messages when inside a chat:

View File

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

View File

@@ -4303,16 +4303,11 @@ int dc_msg_get_duration (const dc_msg_t* msg);
/**
* Check if message was correctly encrypted and signed.
*
* Historically, UIs showed a small padlock on the message then.
* Today, the UIs should instead
* show a small email-icon on the message if the message is not encrypted or signed,
* and nothing otherwise.
* Check if a padlock should be shown beside the message.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return 1=message correctly encrypted and signed, no need to show anything; 0=show email-icon beside the message.
* @return 1=padlock should be shown beside message, 0=do not show a padlock beside the message.
*/
int dc_msg_get_showpadlock (const dc_msg_t* msg);
@@ -4535,12 +4530,12 @@ int dc_msg_is_info (const dc_msg_t* msg);
* - DC_INFO_MEMBER_ADDED_TO_GROUP (4) - "Member CONTACT added by OTHER_CONTACT"
* - DC_INFO_MEMBER_REMOVED_FROM_GROUP (5) - "Member CONTACT removed by OTHER_CONTACT"
* - DC_INFO_EPHEMERAL_TIMER_CHANGED (10) - "Disappearing messages CHANGED_TO by CONTACT"
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is protected"
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
* the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
* - DC_INFO_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted"
*
* For the messages that refer to a CONTACT,
* dc_msg_get_info_contact_id() returns the contact ID.
@@ -4593,10 +4588,9 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
#define DC_INFO_LOCATION_ONLY 9
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
#define DC_INFO_PROTECTION_ENABLED 11
#define DC_INFO_PROTECTION_DISABLED 12 // deprecated 2025-07
#define DC_INFO_PROTECTION_DISABLED 12
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
#define DC_INFO_CHAT_E2EE 50
/**
@@ -6899,7 +6893,9 @@ void dc_event_unref(dc_event_t* event);
/// Used in summaries.
#define DC_STR_GIF 23
/// @deprecated 2025-07, this string is no longer needed.
/// "Encrypted message"
///
/// Used in subjects of outgoing messages.
#define DC_STR_ENCRYPTEDMSG 24
/// "End-to-end encryption available."
@@ -7423,7 +7419,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages.
#define DC_STR_REMOVE_MEMBER_BY_OTHER 131
/// "You left."
/// "You left the group."
///
/// Used in status messages.
#define DC_STR_GROUP_LEFT_BY_YOU 132
@@ -7604,7 +7600,7 @@ void dc_event_unref(dc_event_t* event);
/// Used as a device message after a successful backup transfer.
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
/// "Messages are end-to-end encrypted."
/// "Messages are guaranteed to be end-to-end encrypted from now on."
///
/// Used in info messages.
#define DC_STR_CHAT_PROTECTION_ENABLED 170
@@ -7612,7 +7608,6 @@ void dc_event_unref(dc_event_t* event);
/// "%1$s sent a message from another device."
///
/// Used in info messages.
/// @deprecated 2025-07
#define DC_STR_CHAT_PROTECTION_DISABLED 171
/// "Others will only see this group after you sent a first message."
@@ -7667,9 +7662,6 @@ void dc_event_unref(dc_event_t* event);
/// @deprecated 2025-06-05
#define DC_STR_SECUREJOIN_TAKES_LONGER 192
/// "❤️ Seems you're enjoying Delta Chat!"… (donation request device message)
#define DC_STR_DONATION_REQUEST 193
/// "Contact". Deprecated, currently unused.
#define DC_STR_CONTACT 200

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.4.0"
version = "2.0.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"

View File

@@ -224,14 +224,6 @@ impl CommandApi {
self.accounts.read().await.get_selected_account_id()
}
/// Set the order of accounts.
/// The provided list should contain all account IDs in the desired order.
/// If an account ID is missing from the list, it will be appended at the end.
/// If the list contains non-existent account IDs, they will be ignored.
async fn set_accounts_order(&self, order: Vec<u32>) -> Result<()> {
self.accounts.write().await.set_accounts_order(order).await
}
/// Get a list of all configured accounts.
async fn get_all_accounts(&self) -> Result<Vec<Account>> {
let mut accounts = Vec::new();
@@ -953,7 +945,7 @@ impl CommandApi {
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>())
}
/// Create a new encrypted group chat (with key-contacts).
/// Create a new group chat.
///
/// After creation,
/// the group has one member with the ID DC_CONTACT_ID_SELF
@@ -971,24 +963,14 @@ impl CommandApi {
///
/// @param protect If set to 1 the function creates group with protection initially enabled.
/// Only verified members are allowed in these groups
/// and end-to-end-encryption is always enabled.
async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let protect = match protect {
true => ProtectionStatus::Protected,
false => ProtectionStatus::Unprotected,
};
chat::create_group_ex(&ctx, Some(protect), &name)
.await
.map(|id| id.to_u32())
}
/// Create a new unencrypted group chat.
///
/// Same as [`Self::create_group_chat`], but the chat is unencrypted and can only have
/// address-contacts.
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_group_ex(&ctx, None, &name)
chat::create_group_chat(&ctx, protect, &name)
.await
.map(|id| id.to_u32())
}

View File

@@ -23,7 +23,6 @@ pub enum ChatListItemFetchResult {
name: String,
avatar_path: Option<String>,
color: String,
chat_type: u32,
last_updated: Option<i64>,
summary_text1: String,
summary_text2: String,
@@ -55,7 +54,6 @@ pub enum ChatListItemFetchResult {
///
/// See also `is_key_contact` on `Contact`.
is_encrypted: bool,
/// deprecated 2025-07, use chat_type instead
is_group: bool,
fresh_message_counter: usize,
is_self_talk: bool,
@@ -66,6 +64,10 @@ pub enum ChatListItemFetchResult {
is_pinned: bool,
is_muted: bool,
is_contact_request: bool,
/// Deprecated 2025-07, alias for is_out_broadcast
is_broadcast: bool,
/// true if the chat type is OutBroadcast
is_out_broadcast: bool,
/// contact id if this is a dm chat (for view profile entry in context menu)
dm_chat_contact: Option<u32>,
was_seen_recently: bool,
@@ -155,7 +157,6 @@ pub(crate) async fn get_chat_list_item_by_id(
name: chat.get_name().to_owned(),
avatar_path,
color,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
last_updated,
summary_text1,
summary_text2,
@@ -173,6 +174,8 @@ pub(crate) async fn get_chat_list_item_by_id(
is_pinned: visibility == ChatVisibility::Pinned,
is_muted: chat.is_muted(),
is_contact_request: chat.is_contact_request(),
is_broadcast: chat.get_type() == Chattype::OutBroadcast,
is_out_broadcast: chat.get_type() == Chattype::OutBroadcast,
dm_chat_contact,
was_seen_recently,
last_message_type: message_type,

View File

@@ -416,9 +416,6 @@ pub enum SystemMessageType {
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged,
// Chat is e2ee
ChatE2ee,
// Chat protection state changed
ChatProtectionEnabled,
ChatProtectionDisabled,
@@ -453,7 +450,6 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
SystemMessage::ChatE2ee => SystemMessageType::ChatE2ee,
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,

View File

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

View File

@@ -95,10 +95,8 @@ describe("online tests", function () {
false,
);
// There are 2 messages in the chat:
// 'Messages are end-to-end encrypted' (info message) and 'Hello'
expect(messageList).have.length(2);
const message = await dc.rpc.getMessage(accountId2, messageList[1]);
expect(messageList).have.length(1);
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
expect(message.text).equal("Hello");
expect(message.showPadlock).equal(true);
});

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.4.0"
version = "2.0.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 {
eprintln!("receive_imf errored: {err:?}");
println!("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");
eprintln!("{time_needed:?} to create this list");
println!("{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();
eprintln!(
println!(
"{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,
);
eprintln!("{time_needed:?} to create this list");
println!("{time_needed:?} to create this list");
}
"draft" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -1151,10 +1151,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"listcontacts" | "contacts" => {
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
log_contactlist(&context, &contacts).await?;
println!("{} key contacts.", contacts.len());
let addrcontacts = Contact::get_all(&context, DC_GCL_ADDRESS, Some(arg1)).await?;
log_contactlist(&context, &addrcontacts).await?;
println!("{} address contacts.", addrcontacts.len());
println!("{} contacts.", contacts.len());
}
"addcontact" => {
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
@@ -1227,7 +1224,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
match set_config_from_qr(&context, arg1).await {
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
Err(err) => eprintln!("Cannot set config from QR code: {err:?}"),
Err(err) => println!("Cannot set config from QR code: {err:?}"),
}
}
"createqrsvg" => {

View File

@@ -311,7 +311,7 @@ impl Validator for DcHelper {}
async fn start(args: Vec<String>) -> Result<(), Error> {
if args.len() < 2 {
eprintln!("Error: Bad arguments, expected [db-name].");
println!("Error: Bad arguments, expected [db-name].");
bail!("No db-name specified");
}
let context = ContextBuilder::new(args[1].clone().into())
@@ -366,7 +366,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
false
}
Err(err) => {
eprintln!("Error: {err:#}");
println!("Error: {err:#}");
true
}
}
@@ -381,7 +381,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
break;
}
Err(err) => {
eprintln!("Error: {err:#}");
println!("Error: {err:#}");
break;
}
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.4.0"
version = "2.0.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",

View File

@@ -13,12 +13,6 @@ from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Messag
from ._utils import futuremethod
from .rpc import Rpc
E2EE_INFO_MSGS = 1
"""
The number of info messages added to new e2ee chats.
Currently this is "End-to-end encryption available".
"""
class ACFactory:
"""Test account factory."""

View File

@@ -36,9 +36,6 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
assert ac1.get_config("bcc_self") == "1"
# Second client receives only second message, but not the first.
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == "Messages are end-to-end encrypted."
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text

View File

@@ -12,7 +12,6 @@ import pytest
from deltachat_rpc_client import Contact, EventType, Message, events
from deltachat_rpc_client.const import ChatType, DownloadState, MessageState
from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
from deltachat_rpc_client.rpc import JsonRpcError
@@ -458,12 +457,8 @@ def test_wait_next_messages(acfactory) -> None:
alice_chat_bot.send_text("Hello!")
next_messages = next_messages_task.result()
if len(next_messages) == E2EE_INFO_MSGS:
next_messages += bot.wait_next_messages()
assert len(next_messages) == 1 + E2EE_INFO_MSGS
snapshot = next_messages[0 + E2EE_INFO_MSGS].get_snapshot()
assert len(next_messages) == 1
snapshot = next_messages[0].get_snapshot()
assert snapshot.text == "Hello!"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "2.4.0"
version = "2.0.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.4.0"
"version": "2.0.0"
}

View File

@@ -25,11 +25,13 @@ skip = [
{ name = "derive_more-impl", version = "1.0.0" },
{ name = "derive_more", version = "1.0.0" },
{ name = "event-listener", version = "2.5.3" },
{ name = "generator", version = "0.7.5" },
{ name = "getrandom", version = "0.2.12" },
{ name = "hashbrown", version = "0.14.5" },
{ name = "heck", version = "0.4.1" },
{ name = "http", version = "0.2.12" },
{ name = "linux-raw-sys", version = "0.4.14" },
{ name = "loom", version = "0.5.6" },
{ name = "lru", version = "0.12.3" },
{ name = "netlink-packet-route", version = "0.17.1" },
{ name = "nom", version = "7.1.3" },

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.4.0"
version = "2.0.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,12 +20,6 @@ import deltachat
from . import Account, account_hookimpl, const, get_core_info
from .events import FFIEventLogger, FFIEventTracker
E2EE_INFO_MSGS = 1
"""
The number of info messages added to new e2ee chats.
Currently this is "End-to-end encryption available".
"""
def pytest_addoption(parser):
group = parser.getgroup("deltachat testplugin options")
@@ -612,7 +606,7 @@ class ACFactory:
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg is not None
assert msg.text == "Messages are end-to-end encrypted."
assert msg.text == "Messages are guaranteed to be end-to-end encrypted from now on."
msg = ac2._evtracker.wait_next_incoming_message()
assert msg is not None
assert "Member Me " in msg.text and " added by " in msg.text

View File

@@ -133,7 +133,8 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert "added" in msg.text.lower()
assert any(
m.is_system_message() and m.text == "Messages are end-to-end encrypted." for m in msg.chat.get_messages()
m.is_system_message() and m.text == "Messages are guaranteed to be end-to-end encrypted from now on."
for m in msg.chat.get_messages()
)
lp.sec("ac1: send message")
msg_out = chat1.send_text("hello")
@@ -337,7 +338,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
assert contact.addr == ac1.get_config("addr")
chat2 = msg_in.chat
assert chat2.is_protected()
assert chat2.get_messages()[0].text == "Messages are end-to-end encrypted."
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
lp.sec("ac2_offl: sending message")
@@ -411,7 +412,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2_offl.get_message_by_id(ev.data2)
assert msg_in.is_system_message()
assert msg_in.text == "Messages are end-to-end encrypted."
assert msg_in.text == "Messages are guaranteed to be end-to-end encrypted from now on."
# We need to consume one event that has data2=0
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")

View File

@@ -10,7 +10,6 @@ from imap_tools import AND, U
import deltachat as dc
from deltachat import account_hookimpl, Message
from deltachat.tracker import ImexTracker
from deltachat.testplugin import E2EE_INFO_MSGS
def test_basic_imap_api(acfactory, tmp_path):
@@ -409,10 +408,6 @@ def test_forward_messages(acfactory, lp):
msg_out = chat.send_text("message2")
lp.sec("ac2: wait for receive")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2.get_message_by_id(ev.data2)
assert msg_in.text == "Messages are end-to-end encrypted."
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
assert ev.data2 == msg_out.id
msg_in = ac2.get_message_by_id(msg_out.id)
@@ -627,11 +622,6 @@ def test_moved_markseen(acfactory):
with ac2.direct_imap.idle() as idle2:
ac2.start_io()
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg.text == "Messages are end-to-end encrypted."
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
@@ -748,7 +738,7 @@ def test_mdn_asymmetric(acfactory, lp):
lp.sec("sending text message from ac1 to ac2")
msg_out = chat.send_text("message1")
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
assert len(chat.get_messages()) == 1
lp.sec("disable ac1 MDNs")
ac1.set_config("mdns_enabled", "0")
@@ -756,7 +746,7 @@ def test_mdn_asymmetric(acfactory, lp):
lp.sec("wait for ac2 to receive message")
msg = ac2._evtracker.wait_next_incoming_message()
assert len(msg.chat.get_messages()) == 1 + E2EE_INFO_MSGS
assert len(msg.chat.get_messages()) == 1
lp.sec("ac2: mark incoming message as seen")
ac2.mark_seen_messages([msg])
@@ -765,7 +755,7 @@ def test_mdn_asymmetric(acfactory, lp):
# MDN should be moved even though MDNs are already disabled
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
assert len(chat.get_messages()) == 1
# Wait for the message to be marked as seen on IMAP.
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
@@ -1133,11 +1123,6 @@ def test_send_and_receive_image(acfactory, lp, data):
assert m == msg_out
lp.sec("wait for ac2 to receive message")
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG")
msg_in = ac2.get_message_by_id(ev.data2)
assert msg_in.text == "Messages are end-to-end encrypted."
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG")
assert ev.data2 == msg_out.id
msg_in = ac2.get_message_by_id(msg_out.id)
@@ -1173,10 +1158,10 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
assert contact2.addr == some1_addr
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 3 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert messages[1 + E2EE_INFO_MSGS].filemime == "image/png"
assert os.stat(messages[1 + E2EE_INFO_MSGS].filename).st_size == os.stat(original_image_path).st_size
assert len(messages) == 3
assert messages[0].text == "msg1"
assert messages[1].filemime == "image/png"
assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size
ac.set_config("displayname", "new displayname")
assert ac.get_config("displayname") == "new displayname"
@@ -1429,8 +1414,8 @@ def test_connectivity(acfactory, lp):
ac1.maybe_network()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 1 + E2EE_INFO_MSGS
assert msgs[0 + E2EE_INFO_MSGS].text == "Hi"
assert len(msgs) == 1
assert msgs[0].text == "Hi"
lp.sec("Test that the connectivity changes to WORKING while new messages are fetched")
@@ -1440,8 +1425,8 @@ def test_connectivity(acfactory, lp):
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 2 + E2EE_INFO_MSGS
assert msgs[1 + E2EE_INFO_MSGS].text == "Hi 2"
assert len(msgs) == 2
assert msgs[1].text == "Hi 2"
def test_fetch_deleted_msg(acfactory, lp):

View File

@@ -6,7 +6,6 @@ import pytest
import deltachat as dc
from deltachat.tracker import ImexFailed
from deltachat import Account, Message
from deltachat.testplugin import E2EE_INFO_MSGS
class TestOfflineAccountBasic:
@@ -462,9 +461,9 @@ class TestOfflineChat:
assert contact2.addr == ac_contact.get_config("addr")
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
def test_import_export_on_encrypted_acct(self, acfactory, tmp_path):
passphrase1 = "passphrase1"
@@ -501,9 +500,9 @@ class TestOfflineChat:
contact2_addr = contact2.addr
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
ac2.shutdown()
@@ -518,9 +517,9 @@ class TestOfflineChat:
assert contact2.addr == contact2_addr
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
def test_import_export_with_passphrase(self, acfactory, tmp_path):
passphrase = "test_passphrase"
@@ -558,9 +557,9 @@ class TestOfflineChat:
assert contact2.addr == ac_contact.get_config("addr")
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
"""
@@ -604,9 +603,9 @@ class TestOfflineChat:
assert contact2.addr == ac_contact.get_config("addr")
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
ac2.shutdown()
@@ -621,9 +620,9 @@ class TestOfflineChat:
assert contact2.addr == ac_contact.get_config("addr")
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
def test_set_get_draft(self, chat1):
msg1 = Message.new_empty(chat1.account, "text")

View File

@@ -1 +1 @@
2025-07-21
2025-07-09

View File

@@ -1,6 +1,6 @@
//! # Account manager module.
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeMap;
use std::future::Future;
use std::path::{Path, PathBuf};
@@ -270,51 +270,9 @@ impl Accounts {
}
}
/// Gets a list of all account ids in the user-configured order.
/// Get a list of all account ids.
pub fn get_all(&self) -> Vec<u32> {
let mut ordered_ids = Vec::new();
let mut all_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
// First, add accounts in the configured order
for &id in &self.config.inner.accounts_order {
if all_ids.remove(&id) {
ordered_ids.push(id);
}
}
// Then add any accounts not in the order list (newly added accounts)
for id in all_ids {
ordered_ids.push(id);
}
ordered_ids
}
/// Sets the order of accounts.
///
/// The provided list should contain all account IDs in the desired order.
/// If an account ID is missing from the list, it will be appended at the end.
/// If the list contains non-existent account IDs, they will be ignored.
pub async fn set_accounts_order(&mut self, order: Vec<u32>) -> Result<()> {
let existing_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
// Filter out non-existent account IDs
let mut filtered_order: Vec<u32> = order
.into_iter()
.filter(|id| existing_ids.contains(id))
.collect();
// Add any missing account IDs at the end
for &id in &existing_ids {
if !filtered_order.contains(&id) {
filtered_order.push(id);
}
}
self.config.inner.accounts_order = filtered_order;
self.config.sync().await?;
self.emit_event(EventType::AccountsChanged);
Ok(())
self.accounts.keys().copied().collect()
}
/// Starts background tasks such as IMAP and SMTP loops for all accounts.
@@ -353,11 +311,11 @@ impl Accounts {
/// This is an auxiliary function and not part of public API.
/// Use [Accounts::background_fetch] instead.
async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
let n_accounts = accounts.len();
events.emit(Event {
id: 0,
typ: EventType::Info(format!(
"Starting background fetch for {n_accounts} accounts."
"Starting background fetch for {} accounts.",
accounts.len()
)),
});
let mut set = JoinSet::new();
@@ -369,12 +327,6 @@ impl Accounts {
});
}
set.join_all().await;
events.emit(Event {
id: 0,
typ: EventType::Info(format!(
"Finished background fetch for {n_accounts} accounts."
)),
});
}
/// Auxiliary function for [Accounts::background_fetch].
@@ -463,10 +415,6 @@ struct InnerConfig {
pub selected_account: u32,
pub next_id: u32,
pub accounts: Vec<AccountConfig>,
/// Ordered list of account IDs, representing the user's preferred order.
/// If an account ID is not in this list, it will be appended at the end.
#[serde(default)]
pub accounts_order: Vec<u32>,
}
impl Drop for Config {
@@ -533,7 +481,6 @@ impl Config {
accounts: Vec::new(),
selected_account: 0,
next_id: 1,
accounts_order: Vec::new(),
};
if !lock {
let cfg = Self {
@@ -666,10 +613,6 @@ impl Config {
uuid,
});
self.inner.next_id += 1;
// Add new account to the end of the order list
self.inner.accounts_order.push(id);
id
};
@@ -691,10 +634,6 @@ impl Config {
// remove account from the configs
self.inner.accounts.remove(idx);
}
// Remove from order list as well
self.inner.accounts_order.retain(|&x| x != id);
if self.inner.selected_account == id {
// reset selected account
self.inner.selected_account = self

View File

@@ -33,7 +33,6 @@ use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
use crate::events::EventType;
use crate::location;
use crate::log::{LogExt, error, info, warn};
use crate::logged_debug_assert;
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
@@ -349,8 +348,6 @@ impl ChatId {
chat_id
.add_protection_msg(context, ProtectionStatus::Protected, None, timestamp)
.await?;
} else {
chat_id.maybe_add_encrypted_msg(context, timestamp).await?;
}
info!(
@@ -606,42 +603,6 @@ impl ChatId {
Ok(())
}
/// Adds message "Messages are end-to-end encrypted" if appropriate.
///
/// This function is rather slow because it does a lot of database queries,
/// but this is fine because it is only called on chat creation.
async fn maybe_add_encrypted_msg(self, context: &Context, timestamp_sort: i64) -> Result<()> {
let chat = Chat::load_from_db(context, self).await?;
// as secure-join adds its own message on success (after some other messasges),
// we do not want to add "Messages are end-to-end encrypted" on chat creation.
// we detect secure join by `can_send` (for Bob, scanner side) and by `blocked` (for Alice, inviter side) below.
if !chat.is_encrypted(context).await?
|| self <= DC_CHAT_ID_LAST_SPECIAL
|| chat.is_device_talk()
|| chat.is_self_talk()
|| (!chat.can_send(context).await? && !chat.is_contact_request())
|| chat.blocked == Blocked::Yes
{
return Ok(());
}
let text = stock_str::messages_e2e_encrypted(context).await;
add_info_msg_with_cmd(
context,
self,
&text,
SystemMessage::ChatE2ee,
timestamp_sort,
None,
None,
None,
None,
)
.await?;
Ok(())
}
/// Sets protection and adds a message.
///
/// `timestamp_sort` is used as the timestamp of the added message
@@ -1378,18 +1339,14 @@ impl ChatId {
let mut ret = stock_str::e2e_available(context).await + "\n";
for &contact_id in get_chat_contacts(context, self)
for contact_id in get_chat_contacts(context, self)
.await?
.iter()
.filter(|&contact_id| !contact_id.is_special())
{
let contact = Contact::get_by_id(context, contact_id).await?;
let contact = Contact::get_by_id(context, *contact_id).await?;
let addr = contact.get_addr();
logged_debug_assert!(
context,
contact.is_key_contact(),
"get_encryption_info: contact {contact_id} is not a key-contact."
);
debug_assert!(contact.is_key_contact());
let fingerprint = contact
.fingerprint()
.context("Contact does not have a fingerprint in encrypted chat")?;
@@ -2711,10 +2668,6 @@ impl ChatIdBlocked {
smeared_time,
)
.await?;
} else {
chat_id
.maybe_add_encrypted_msg(context, smeared_time)
.await?;
}
Ok(Self {
@@ -2952,12 +2905,10 @@ async fn prepare_send_msg(
// If the chat is a contact request, let the user accept it later.
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
}
// Allow to send "Member removed" messages so we can leave the group/broadcast.
// Allow to send "Member removed" messages so we can leave the group.
// Necessary checks should be made anyway before removing contact
// from the chat.
CantSendReason::NotAMember | CantSendReason::InBroadcast => {
msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup
}
CantSendReason::NotAMember => msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup,
CantSendReason::MissingKey => msg
.param
.get_bool(Param::ForcePlaintext)
@@ -3009,9 +2960,6 @@ async fn prepare_send_msg(
let row_ids = create_send_msg_jobs(context, msg)
.await
.context("Failed to create send jobs")?;
if !row_ids.is_empty() {
donation_request_maybe(context).await.log_err(context).ok();
}
Ok(row_ids)
}
@@ -3256,31 +3204,6 @@ pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Re
send_msg(context, chat_id, &mut msg).await
}
async fn donation_request_maybe(context: &Context) -> Result<()> {
let secs_between_checks = 30 * 24 * 60 * 60;
let now = time();
let ts = context
.get_config_i64(Config::DonationRequestNextCheck)
.await?;
if ts > now {
return Ok(());
}
let msg_cnt = context.sql.count(
"SELECT COUNT(*) FROM msgs WHERE state>=? AND hidden=0",
(MessageState::OutDelivered,),
);
let ts = if ts == 0 || msg_cnt.await? < 100 {
now.saturating_add(secs_between_checks)
} else {
let mut msg = Message::new_text(stock_str::donation_request(context).await);
add_device_msg(context, None, Some(&mut msg)).await?;
i64::MAX
};
context
.set_config_internal(Config::DonationRequestNextCheck, Some(&ts.to_string()))
.await
}
/// Chat message list request options.
#[derive(Debug)]
pub struct MessageListOptions {
@@ -3699,31 +3622,15 @@ pub async fn get_past_chat_contacts(context: &Context, chat_id: ChatId) -> Resul
}
/// Creates a group chat with a given `name`.
/// Deprecated on 2025-06-21, use `create_group_ex()`.
pub async fn create_group_chat(
context: &Context,
protect: ProtectionStatus,
name: &str,
chat_name: &str,
) -> Result<ChatId> {
create_group_ex(context, Some(protect), name).await
}
/// Creates a group chat.
///
/// * `encryption` - If `Some`, the chat is encrypted (with key-contacts) and can be protected.
/// * `name` - Chat name.
pub async fn create_group_ex(
context: &Context,
encryption: Option<ProtectionStatus>,
name: &str,
) -> Result<ChatId> {
let chat_name = sanitize_single_line(name);
let chat_name = sanitize_single_line(chat_name);
ensure!(!chat_name.is_empty(), "Invalid chat name");
let grpid = match encryption {
Some(_) => create_id(),
None => String::new(),
};
let grpid = create_id();
let timestamp = create_smeared_timestamp(context);
let row_id = context
@@ -3743,8 +3650,7 @@ pub async fn create_group_ex(
chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, chat_id);
if encryption == Some(ProtectionStatus::Protected) {
let protect = ProtectionStatus::Protected;
if protect == ProtectionStatus::Protected {
chat_id
.set_protection_for_timestamp_sort(context, protect, timestamp, None)
.await?;
@@ -4173,6 +4079,8 @@ pub async fn remove_contact_from_chat(
"Cannot remove special contact"
);
let mut msg = Message::new(Viewtype::default());
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast {
if !chat.is_self_in_chat(context).await? {
@@ -4202,10 +4110,19 @@ pub async fn remove_contact_from_chat(
// in case of the database becoming inconsistent due to a bug.
if let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? {
if chat.typ == Chattype::Group && chat.is_promoted() {
let addr = contact.get_addr();
let res = send_member_removal_msg(context, chat_id, contact_id, addr).await;
msg.viewtype = Viewtype::Text;
if contact_id == ContactId::SELF {
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
} else {
msg.text =
stock_str::msg_del_member_local(context, contact_id, ContactId::SELF)
.await;
}
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
msg.param.set(Param::Arg, contact.get_addr().to_lowercase());
msg.param
.set(Param::ContactAddedRemoved, contact.id.to_u32() as i32);
let res = send_msg(context, chat_id, &mut msg).await;
if contact_id == ContactId::SELF {
res?;
set_group_explicitly_left(context, &chat.grpid).await?;
@@ -4224,11 +4141,6 @@ pub async fn remove_contact_from_chat(
chat.sync_contacts(context).await.log_err(context).ok();
}
}
} else if chat.typ == Chattype::InBroadcast && contact_id == ContactId::SELF {
// For incoming broadcast channels, it's not possible to remove members,
// but it's possible to leave:
let self_addr = context.get_primary_self_addr().await?;
send_member_removal_msg(context, chat_id, contact_id, &self_addr).await?;
} else {
bail!("Cannot remove members from non-group chats.");
}
@@ -4236,28 +4148,6 @@ pub async fn remove_contact_from_chat(
Ok(())
}
async fn send_member_removal_msg(
context: &Context,
chat_id: ChatId,
contact_id: ContactId,
addr: &str,
) -> Result<MsgId> {
let mut msg = Message::new(Viewtype::Text);
if contact_id == ContactId::SELF {
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
} else {
msg.text = stock_str::msg_del_member_local(context, contact_id, ContactId::SELF).await;
}
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
msg.param.set(Param::Arg, addr.to_lowercase());
msg.param
.set(Param::ContactAddedRemoved, contact_id.to_u32());
send_msg(context, chat_id, &mut msg).await
}
async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()> {
if !is_group_explicitly_left(context, grpid).await? {
context

View File

@@ -5,10 +5,10 @@ use crate::ephemeral::Timer;
use crate::headerdef::HeaderDef;
use crate::imex::{ImexMode, has_backup, imex};
use crate::message::{MessengerMessage, delete_msgs};
use crate::mimeparser::{self, MimeMessage};
use crate::mimeparser;
use crate::receive_imf::receive_imf;
use crate::test_utils::{
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, E2EE_INFO_MSGS, TestContext, TestContextManager,
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
TimeShiftFalsePositiveNote, sync,
};
use pretty_assertions::assert_eq;
@@ -374,10 +374,7 @@ async fn test_member_add_remove() -> Result<()> {
// Alice leaves the chat.
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
let sent = alice.pop_sent_msg().await;
assert_eq!(
sent.load_from_db().await.get_text(),
stock_str::msg_group_left_local(&alice, ContactId::SELF).await
);
assert_eq!(sent.load_from_db().await.get_text(), "You left the group.");
Ok(())
}
@@ -2104,7 +2101,7 @@ async fn test_forward_basic() -> Result<()> {
forward_msgs(&bob, &[msg.id], bob_chat.get_id()).await?;
let forwarded_msg = bob.pop_sent_msg().await;
assert_eq!(bob_chat.id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 2);
assert_eq!(bob_chat.id.get_msg_cnt(&bob).await?, 2);
assert_ne!(
forwarded_msg.load_from_db().await.rfc724_mid,
msg.rfc724_mid,
@@ -2132,7 +2129,7 @@ async fn test_forward_info_msg() -> Result<()> {
assert!(msg1.get_text().contains("bob@example.net"));
let chat_id2 = ChatId::create_for_contact(alice, bob_id).await?;
assert_eq!(get_chat_msgs(alice, chat_id2).await?.len(), E2EE_INFO_MSGS);
assert_eq!(get_chat_msgs(alice, chat_id2).await?.len(), 0);
forward_msgs(alice, &[msg1.id], chat_id2).await?;
let msg2 = alice.get_last_msg_in(chat_id2).await;
assert!(!msg2.is_info()); // forwarded info-messages lose their info-state
@@ -2518,34 +2515,22 @@ async fn test_resend_own_message() -> Result<()> {
let sent1_ts_sent = msg.timestamp_sent;
assert_eq!(msg.get_text(), "alice->bob");
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2);
assert_eq!(
get_chat_msgs(&bob, msg.chat_id).await?.len(),
E2EE_INFO_MSGS + 1
);
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 1);
bob.recv_msg(&sent2).await;
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
assert_eq!(
get_chat_msgs(&bob, msg.chat_id).await?.len(),
E2EE_INFO_MSGS + 2
);
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2);
let received = bob.recv_msg_opt(&sent3).await;
// No message should actually be added since we already know this message:
assert!(received.is_none());
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
assert_eq!(
get_chat_msgs(&bob, msg.chat_id).await?.len(),
E2EE_INFO_MSGS + 2
);
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2);
// Fiona does not receive the first message, however, due to resending, she has a similar view as Alice and Bob
fiona.recv_msg(&sent2).await;
let msg = fiona.recv_msg(&sent3).await;
assert_eq!(msg.get_text(), "alice->bob");
assert_eq!(get_chat_contacts(&fiona, msg.chat_id).await?.len(), 3);
assert_eq!(
get_chat_msgs(&fiona, msg.chat_id).await?.len(),
E2EE_INFO_MSGS + 2
);
assert_eq!(get_chat_msgs(&fiona, msg.chat_id).await?.len(), 2);
let msg_from = Contact::get_by_id(&fiona, msg.get_from_id()).await?;
assert_eq!(msg_from.get_addr(), "alice@example.org");
assert!(sent1_ts_sent < msg.timestamp_sent);
@@ -2946,108 +2931,6 @@ async fn test_broadcast_channel_protected_listid() -> Result<()> {
Ok(())
}
/// Test that if Bob leaves a broadcast channel,
/// Alice (the channel owner) won't see him as a member anymore,
/// but won't be notified about this in any way.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_leave_broadcast() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
tcm.section("Alice creates broadcast channel with Bob.");
let alice_chat_id = create_broadcast(alice, "foo".to_string()).await?;
let bob_contact = alice.add_or_lookup_contact(bob).await.id;
add_contact_to_chat(alice, alice_chat_id, bob_contact).await?;
tcm.section("Alice sends first message to broadcast.");
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
let bob_msg = bob.recv_msg(&sent_msg).await;
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 1);
// Clear events so that we can later check
// that the 'Broadcast channel left' message didn't trigger IncomingMsg:
alice.evtracker.clear_events();
// Shift the time so that we can later check the "Broadcast channel left" message's timestamp:
SystemTime::shift(Duration::from_secs(60));
tcm.section("Bob leaves the broadcast channel.");
let bob_chat_id = bob_msg.chat_id;
bob_chat_id.accept(bob).await?;
remove_contact_from_chat(bob, bob_chat_id, ContactId::SELF).await?;
let leave_msg = bob.pop_sent_msg().await;
alice.recv_msg_trash(&leave_msg).await;
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 0);
alice.emit_event(EventType::Test);
alice
.evtracker
.get_matching(|ev| match ev {
EventType::Test => true,
EventType::IncomingMsg { .. } => {
panic!("'Broadcast channel left' message should be silent")
}
EventType::MsgsNoticed(..) => {
panic!("'Broadcast channel left' message shouldn't clear notifications")
}
EventType::MsgsChanged { .. } => {
panic!("Broadcast channels should be left silently, without any message");
}
_ => false,
})
.await;
Ok(())
}
/// Tests that if Bob leaves a broadcast channel with one device,
/// the other device shows a correct info message "You left.".
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_leave_broadcast_multidevice() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob0 = &tcm.bob().await;
let bob1 = &tcm.bob().await;
tcm.section("Alice creates broadcast channel with Bob.");
let alice_chat_id = create_broadcast(alice, "foo".to_string()).await?;
let bob_contact = alice.add_or_lookup_contact(bob0).await.id;
add_contact_to_chat(alice, alice_chat_id, bob_contact).await?;
tcm.section("Alice sends first message to broadcast.");
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
let bob0_hello = bob0.recv_msg(&sent_msg).await;
let bob1_hello = bob1.recv_msg(&sent_msg).await;
tcm.section("Bob leaves the broadcast channel with his first device.");
let bob_chat_id = bob0_hello.chat_id;
bob_chat_id.accept(bob0).await?;
remove_contact_from_chat(bob0, bob_chat_id, ContactId::SELF).await?;
let leave_msg = bob0.pop_sent_msg().await;
let parsed = MimeMessage::from_bytes(bob1, leave_msg.payload().as_bytes(), None).await?;
assert_eq!(
parsed.parts[0].msg,
stock_str::msg_group_left_remote(bob0).await
);
let rcvd = bob1.recv_msg(&leave_msg).await;
assert_eq!(rcvd.chat_id, bob1_hello.chat_id);
assert!(rcvd.is_info());
assert_eq!(rcvd.get_info_type(), SystemMessage::MemberRemovedFromGroup);
assert_eq!(
rcvd.text,
stock_str::msg_group_left_local(bob1, ContactId::SELF).await
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_for_contact_with_blocked() -> Result<()> {
let t = TestContext::new().await;
@@ -4466,13 +4349,13 @@ async fn test_receive_edit_request_after_removal() -> Result<()> {
let bob_msg = bob.recv_msg(&sent1).await;
let bob_chat_id = bob_msg.chat_id;
assert_eq!(bob_msg.text, "zext me in delra.cat");
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1);
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 1);
delete_msgs(bob, &[bob_msg.id]).await?;
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS);
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 0);
bob.recv_msg_trash(&sent2).await;
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS);
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 0);
Ok(())
}
@@ -4561,34 +4444,28 @@ async fn test_send_delete_request() -> Result<()> {
// Alice sends a message, then sends a deletion request
let sent1 = alice.send_text(alice_chat.id, "wtf").await;
let alice_msg = sent1.load_from_db().await;
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 2);
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 2);
message::delete_msgs_ex(alice, &[alice_msg.id], true).await?;
let sent2 = alice.pop_sent_msg().await;
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 1);
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1);
// Bob receives both messages and has nothing the end
let bob_msg = bob.recv_msg(&sent1).await;
assert_eq!(bob_msg.text, "wtf");
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 2);
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 2);
bob.recv_msg_opt(&sent2).await;
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1);
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 1);
// Alice has another device, and there is also nothing at the end
let alice2 = &tcm.alice().await;
alice2.recv_msg(&sent0).await;
let alice2_msg = alice2.recv_msg(&sent1).await;
assert_eq!(
alice2_msg.chat_id.get_msg_cnt(alice2).await?,
E2EE_INFO_MSGS + 2
);
assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 2);
alice2.recv_msg_opt(&sent2).await;
assert_eq!(
alice2_msg.chat_id.get_msg_cnt(alice2).await?,
E2EE_INFO_MSGS + 1
);
assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 1);
Ok(())
}
@@ -4716,32 +4593,6 @@ async fn test_no_key_contacts_in_adhoc_chats() -> Result<()> {
Ok(())
}
/// Tests that key-contacts cannot be added to an unencrypted (ad hoc) group and the group and
/// messages report that they are unencrypted.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_unencrypted_group_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let chat_id = create_group_ex(alice, None, "Group chat").await?;
let bob_key_contact_id = alice.add_or_lookup_contact_id(bob).await;
let charlie_address_contact_id = alice.add_or_lookup_address_contact_id(charlie).await;
let res = add_contact_to_chat(alice, chat_id, bob_key_contact_id).await;
assert!(res.is_err());
add_contact_to_chat(alice, chat_id, charlie_address_contact_id).await?;
let chat = Chat::load_from_db(alice, chat_id).await?;
assert!(!chat.is_encrypted(alice).await?);
let sent_msg = alice.send_text(chat_id, "Hello").await;
let msg = Message::load_from_db(alice, sent_msg.sender_msg_id).await?;
assert!(!msg.get_showpadlock());
Ok(())
}
/// Tests that avatar cannot be set in ad hoc groups.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_avatar_in_adhoc_chats() -> Result<()> {

View File

@@ -369,9 +369,6 @@ pub enum Config {
#[strum(props(default = "0"))]
DisableIdle,
/// Timestamp of the next check for donation request need.
DonationRequestNextCheck,
/// Defines the max. size (in bytes) of messages downloaded automatically.
/// 0 = no limit.
#[strum(props(default = "0"))]

View File

@@ -37,7 +37,7 @@ use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::sync::{self, Sync::*};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time};
use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str};
use crate::{chat, chatlist_events, stock_str};
/// Time during which a contact is considered as seen recently.
const SEEN_RECENTLY_SECONDS: i64 = 600;
@@ -1922,10 +1922,9 @@ pub(crate) async fn mark_contact_id_as_verified(
contact_id: ContactId,
verifier_id: ContactId,
) -> Result<()> {
ensure_and_debug_assert_ne!(
contact_id,
verifier_id,
"Contact cannot be verified by self",
debug_assert_ne!(
contact_id, verifier_id,
"Contact cannot be verified by self"
);
context
.sql

View File

@@ -27,7 +27,6 @@ use crate::events::{Event, EventEmitter, EventType, Events};
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
use crate::key::{load_self_secret_key, self_fingerprint};
use crate::log::{info, warn};
use crate::logged_debug_assert;
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
use crate::message::{self, Message, MessageState, MsgId};
use crate::param::{Param, Params};
@@ -661,16 +660,8 @@ impl Context {
/// or [`Self::emit_msgs_changed_without_msg_id`] should be used
/// instead of this function.
pub fn emit_msgs_changed(&self, chat_id: ChatId, msg_id: MsgId) {
logged_debug_assert!(
self,
!chat_id.is_unset(),
"emit_msgs_changed: chat_id is unset."
);
logged_debug_assert!(
self,
!msg_id.is_unset(),
"emit_msgs_changed: msg_id is unset."
);
debug_assert!(!chat_id.is_unset());
debug_assert!(!msg_id.is_unset());
self.emit_event(EventType::MsgsChanged { chat_id, msg_id });
chatlist_events::emit_chatlist_changed(self);
@@ -679,11 +670,7 @@ impl Context {
/// Emits a MsgsChanged event with specified chat and without message id.
pub fn emit_msgs_changed_without_msg_id(&self, chat_id: ChatId) {
logged_debug_assert!(
self,
!chat_id.is_unset(),
"emit_msgs_changed_without_msg_id: chat_id is unset."
);
debug_assert!(!chat_id.is_unset());
self.emit_event(EventType::MsgsChanged {
chat_id,
@@ -1054,12 +1041,6 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"donation_request_next_check",
self.get_config_i64(Config::DonationRequestNextCheck)
.await?
.to_string(),
);
res.insert(
"first_key_contacts_msg_id",
self.sql

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::{E2EE_INFO_MSGS, TestContext, get_chat_msg};
use crate::test_utils::{TestContext, get_chat_msg};
use crate::tools::{SystemTime, create_outgoing_rfc724_mid};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -571,7 +571,7 @@ async fn test_get_next_msgs() -> Result<()> {
let alice_chat = alice.create_chat(&bob).await;
assert_eq!(alice.get_next_msgs().await?.len(), E2EE_INFO_MSGS);
assert!(alice.get_next_msgs().await?.is_empty());
assert!(bob.get_next_msgs().await?.is_empty());
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;

View File

@@ -178,8 +178,7 @@ mod tests {
let bob = TestContext::new_bob().await;
receive_imf(&bob, attachment_mime, false).await?;
let msg = bob.get_last_msg().await;
// Subject should be prepended because the attachment doesn't have "Chat-Version".
assert_eq!(msg.text, "Hello, Bob! Hello from Thunderbird!");
assert_eq!(msg.text, "Hello from Thunderbird!");
Ok(())
}

View File

@@ -213,18 +213,17 @@ impl Session {
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
uid_message_ids.insert(uid, rfc724_mid);
let (sender, receiver) = async_channel::unbounded();
self.fetch_many_msgs(
context,
folder,
uidvalidity,
vec![uid],
&uid_message_ids,
false,
sender,
)
.await?;
if receiver.recv().await.is_err() {
let (last_uid, _received) = self
.fetch_many_msgs(
context,
folder,
uidvalidity,
vec![uid],
&uid_message_ids,
false,
)
.await?;
if last_uid.is_none() {
bail!("Failed to fetch UID {uid}");
}
Ok(())
@@ -277,7 +276,7 @@ mod tests {
use crate::chat::{get_chat_msgs, send_msg};
use crate::ephemeral::Timer;
use crate::receive_imf::receive_imf_from_inbox;
use crate::test_utils::{E2EE_INFO_MSGS, TestContext};
use crate::test_utils::TestContext;
#[test]
fn test_downloadstate_values() {
@@ -459,10 +458,7 @@ mod tests {
.await?;
let msg = bob.get_last_msg().await;
let chat_id = msg.chat_id;
assert_eq!(
get_chat_msgs(&bob, chat_id).await?.len(),
E2EE_INFO_MSGS + 1
);
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 1);
assert_eq!(msg.download_state(), DownloadState::Available);
// downloading the status update afterwards expands to nothing and moves the placeholder to trash-chat
@@ -475,7 +471,7 @@ mod tests {
None,
)
.await?;
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), E2EE_INFO_MSGS);
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
assert!(
Message::load_from_db_optional(&bob, msg.id)
.await?

View File

@@ -14,7 +14,7 @@ use std::{
};
use anyhow::{Context as _, Result, bail, ensure, format_err};
use async_channel::{self, Receiver, Sender};
use async_channel::Receiver;
use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
use deltachat_contact_tools::ContactAddress;
use futures::{FutureExt as _, StreamExt, TryStreamExt};
@@ -562,7 +562,7 @@ impl Imap {
let read_cnt = msgs.len();
let download_limit = context.download_limit().await?;
let mut uids_fetch = Vec::<(u32, bool /* partially? */)>::with_capacity(msgs.len() + 1);
let mut uids_fetch = Vec::<(_, bool /* partially? */)>::with_capacity(msgs.len() + 1);
let mut uid_message_ids = BTreeMap::new();
let mut largest_uid_skipped = None;
let delete_target = context.get_delete_msgs_target().await?;
@@ -695,72 +695,51 @@ impl Imap {
self.connectivity.set_working(context).await;
}
let (sender, receiver) = async_channel::unbounded();
// Actually download messages.
let mut largest_uid_fetched: u32 = 0;
let mut received_msgs = Vec::with_capacity(uids_fetch.len());
let mut uids_fetch_in_batch = Vec::with_capacity(max(uids_fetch.len(), 1));
let mut fetch_partially = false;
uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1));
for (uid, fp) in uids_fetch {
if fp != fetch_partially {
let (largest_uid_fetched_in_batch, received_msgs_in_batch) = session
.fetch_many_msgs(
context,
folder,
uid_validity,
uids_fetch_in_batch.split_off(0),
&uid_message_ids,
fetch_partially,
)
.await
.context("fetch_many_msgs")?;
received_msgs.extend(received_msgs_in_batch);
largest_uid_fetched = max(
largest_uid_fetched,
largest_uid_fetched_in_batch.unwrap_or(0),
);
fetch_partially = fp;
}
uids_fetch_in_batch.push(uid);
}
// Advance uid_next to the maximum of the largest known UID plus 1
// and mailbox UIDNEXT.
// Largest known UID is normally less than UIDNEXT,
// but a message may have arrived between determining UIDNEXT
// and executing the FETCH command.
let mailbox_uid_next = session
.selected_mailbox
.as_ref()
.with_context(|| format!("Expected {folder:?} to be selected"))?
.uid_next
.unwrap_or_default();
let new_uid_next = max(
max(largest_uid_fetched, largest_uid_skipped.unwrap_or(0)) + 1,
mailbox_uid_next,
);
let update_uids_future = async {
let mut largest_uid_fetched: u32 = 0;
while let Ok((uid, received_msg_opt)) = receiver.recv().await {
largest_uid_fetched = max(largest_uid_fetched, uid);
if let Some(received_msg) = received_msg_opt {
received_msgs.push(received_msg)
}
}
largest_uid_fetched
};
let actually_download_messages_future = async move {
let mut uids_fetch_in_batch = Vec::with_capacity(max(uids_fetch.len(), 1));
let mut fetch_partially = false;
uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1));
for (uid, fp) in uids_fetch {
if fp != fetch_partially {
session
.fetch_many_msgs(
context,
folder,
uid_validity,
uids_fetch_in_batch.split_off(0),
&uid_message_ids,
fetch_partially,
sender.clone(),
)
.await
.context("fetch_many_msgs")?;
fetch_partially = fp;
}
uids_fetch_in_batch.push(uid);
}
anyhow::Ok(())
};
let (largest_uid_fetched, fetch_res) =
tokio::join!(update_uids_future, actually_download_messages_future);
// Advance uid_next to the largest fetched UID plus 1.
//
// This may be larger than `mailbox_uid_next`
// if the message has arrived after selecting mailbox
// and determining its UIDNEXT and before prefetch.
let mut new_uid_next = largest_uid_fetched + 1;
if fetch_res.is_ok() {
// If we have successfully fetched all messages we planned during prefetch,
// then we have covered at least the range between old UIDNEXT
// and UIDNEXT of the mailbox at the time of selecting it.
new_uid_next = max(new_uid_next, mailbox_uid_next);
new_uid_next = max(new_uid_next, largest_uid_skipped.unwrap_or(0) + 1);
}
if new_uid_next > old_uid_next {
set_uid_next(context, folder, new_uid_next).await?;
}
@@ -773,10 +752,6 @@ impl Imap {
chat::mark_old_messages_as_noticed(context, received_msgs).await?;
// Now fail if fetching failed, so we will
// establish a new session if this one is broken.
fetch_res?;
Ok(read_cnt > 0)
}
@@ -1325,19 +1300,9 @@ impl Session {
/// Fetches a list of messages by server UID.
///
/// Sends pairs of UID and info about each downloaded message to the provided channel.
/// Received message info is optional because UID may be ignored
/// if the message has a `\Deleted` flag.
///
/// The channel is used to return the results because the function may fail
/// due to network errors before it finishes fetching all the messages.
/// In this case caller still may want to process all the results
/// received over the channel and persist last seen UID in the database
/// before bubbling up the failure.
///
/// Returns the last UID fetched successfully and the info about each downloaded message.
/// If the message is incorrect or there is a failure to write a message to the database,
/// it is skipped and the error is logged.
#[expect(clippy::too_many_arguments)]
pub(crate) async fn fetch_many_msgs(
&mut self,
context: &Context,
@@ -1346,10 +1311,12 @@ impl Session {
request_uids: Vec<u32>,
uid_message_ids: &BTreeMap<u32, String>,
fetch_partially: bool,
received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
) -> Result<()> {
) -> Result<(Option<u32>, Vec<ReceivedMsg>)> {
let mut last_uid = None;
let mut received_msgs = Vec::new();
if request_uids.is_empty() {
return Ok(());
return Ok((last_uid, received_msgs));
}
for (request_uids, set) in build_sequence_sets(&request_uids)? {
@@ -1384,15 +1351,14 @@ impl Session {
// Try to find a requested UID in returned FETCH responses.
while fetch_response.is_none() {
let Some(next_fetch_response) = fetch_responses
.try_next()
.await
.context("Failed to process IMAP FETCH result")?
else {
let Some(next_fetch_response) = fetch_responses.next().await else {
// No more FETCH responses received from the server.
break;
};
let next_fetch_response =
next_fetch_response.context("Failed to process IMAP FETCH result")?;
if let Some(next_uid) = next_fetch_response.uid {
if next_uid == request_uid {
fetch_response = Some(next_fetch_response);
@@ -1436,7 +1402,7 @@ impl Session {
if is_deleted {
info!(context, "Not processing deleted msg {}.", request_uid);
received_msgs_channel.send((request_uid, None)).await?;
last_uid = Some(request_uid);
continue;
}
@@ -1447,7 +1413,7 @@ impl Session {
context,
"Not processing message {} without a BODY.", request_uid
);
received_msgs_channel.send((request_uid, None)).await?;
last_uid = Some(request_uid);
continue;
};
@@ -1479,29 +1445,20 @@ impl Session {
.await
{
Ok(received_msg) => {
received_msgs_channel
.send((request_uid, received_msg))
.await?;
if let Some(m) = received_msg {
received_msgs.push(m);
}
}
Err(err) => {
warn!(context, "receive_imf error: {:#}.", err);
received_msgs_channel.send((request_uid, None)).await?;
}
};
last_uid = Some(request_uid)
}
// If we don't process the whole response, IMAP client is left in a broken state where
// it will try to process the rest of response as the next response.
//
// Make sure to not ignore the errors, because
// if connection times out, it will return
// infinite stream of `Some(Err(_))` results.
while fetch_responses
.try_next()
.await
.context("Failed to drain FETCH responses")?
.is_some()
{}
while fetch_responses.next().await.is_some() {}
if count != request_uids.len() {
warn!(
@@ -1520,7 +1477,7 @@ impl Session {
}
}
Ok(())
Ok((last_uid, received_msgs))
}
/// Retrieves server metadata if it is supported.

View File

@@ -8,13 +8,15 @@ use tokio::io::BufWriter;
use super::capabilities::Capabilities;
use crate::context::Context;
use crate::log::{LoggingStream, info, warn};
use crate::log::{info, warn};
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionStream;
use crate::net::tls::wrap_tls;
use crate::net::{connect_tcp_inner, run_connection_attempts, update_connection_history};
use crate::net::{
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
};
use crate::tools::time;
#[derive(Debug)]
@@ -124,12 +126,12 @@ impl Client {
);
let res = match security {
ConnectionSecurity::Tls => {
Client::connect_secure(context, resolved_addr, host, strict_tls).await
Client::connect_secure(resolved_addr, host, strict_tls).await
}
ConnectionSecurity::Starttls => {
Client::connect_starttls(context, resolved_addr, host, strict_tls).await
Client::connect_starttls(resolved_addr, host, strict_tls).await
}
ConnectionSecurity::Plain => Client::connect_insecure(context, resolved_addr).await,
ConnectionSecurity::Plain => Client::connect_insecure(resolved_addr).await,
};
match res {
Ok(client) => {
@@ -200,61 +202,40 @@ impl Client {
}
}
async fn connect_secure(
context: &Context,
addr: SocketAddr,
hostname: &str,
strict_tls: bool,
) -> Result<Self> {
let tcp_stream = connect_tcp_inner(addr).await?;
let account_id = context.get_id();
let events = context.events.clone();
let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
let tls_stream = wrap_tls(strict_tls, hostname, alpn(addr.port()), logging_stream).await?;
async fn connect_secure(addr: SocketAddr, hostname: &str, strict_tls: bool) -> Result<Self> {
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
let _greeting = client
.read_response()
.await?
.context("Failed to read greeting")?;
.await
.context("failed to read greeting")??;
Ok(client)
}
async fn connect_insecure(context: &Context, addr: SocketAddr) -> Result<Self> {
async fn connect_insecure(addr: SocketAddr) -> Result<Self> {
let tcp_stream = connect_tcp_inner(addr).await?;
let account_id = context.get_id();
let events = context.events.clone();
let logging_stream = LoggingStream::new(tcp_stream, account_id, events)?;
let buffered_stream = BufWriter::new(logging_stream);
let buffered_stream = BufWriter::new(tcp_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
let _greeting = client
.read_response()
.await?
.context("Failed to read greeting")?;
.await
.context("failed to read greeting")??;
Ok(client)
}
async fn connect_starttls(
context: &Context,
addr: SocketAddr,
host: &str,
strict_tls: bool,
) -> Result<Self> {
async fn connect_starttls(addr: SocketAddr, host: &str, strict_tls: bool) -> Result<Self> {
let tcp_stream = connect_tcp_inner(addr).await?;
let account_id = context.get_id();
let events = context.events.clone();
let tcp_stream = LoggingStream::new(tcp_stream, account_id, events)?;
// Run STARTTLS command and convert the client back into a stream.
let buffered_tcp_stream = BufWriter::new(tcp_stream);
let mut client = async_imap::Client::new(buffered_tcp_stream);
let _greeting = client
.read_response()
.await?
.context("Failed to read greeting")?;
.await
.context("failed to read greeting")??;
client
.run_command_and_check_ok("STARTTLS", None)
.await
@@ -265,6 +246,7 @@ impl Client {
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let client = Client::new(session_stream);
@@ -287,8 +269,8 @@ impl Client {
let mut client = Client::new(session_stream);
let _greeting = client
.read_response()
.await?
.context("Failed to read greeting")?;
.await
.context("failed to read greeting")??;
Ok(client)
}
@@ -304,8 +286,8 @@ impl Client {
let mut client = Client::new(session_stream);
let _greeting = client
.read_response()
.await?
.context("Failed to read greeting")?;
.await
.context("failed to read greeting")??;
Ok(client)
}
@@ -325,8 +307,8 @@ impl Client {
let mut client = ImapClient::new(buffered_proxy_stream);
let _greeting = client
.read_response()
.await?
.context("Failed to read greeting")?;
.await
.context("failed to read greeting")??;
client
.run_command_and_check_ok("STARTTLS", None)
.await

View File

@@ -4,10 +4,6 @@
use crate::context::Context;
mod stream;
pub(crate) use stream::LoggingStream;
macro_rules! info {
($ctx:expr, $msg:expr) => {
info!($ctx, $msg,)

View File

@@ -1,161 +0,0 @@
//! Stream that logs errors as events.
//!
//! This stream can be used to wrap IMAP,
//! SMTP and HTTP streams so errors
//! that occur are logged before
//! they are processed.
use std::net::SocketAddr;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use anyhow::{Context as _, Result};
use pin_project::pin_project;
use crate::events::{Event, EventType, Events};
use crate::net::session::SessionStream;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
#[derive(Debug)]
struct Metrics {
/// Total number of bytes read.
pub total_read: usize,
/// Total number of bytes written.
pub total_written: usize,
}
impl Metrics {
fn new() -> Self {
Self {
total_read: 0,
total_written: 0,
}
}
}
/// Stream that logs errors to the event channel.
#[derive(Debug)]
#[pin_project]
pub(crate) struct LoggingStream<S: SessionStream> {
#[pin]
inner: S,
/// Account ID for logging.
account_id: u32,
/// Event channel.
events: Events,
/// Metrics for this stream.
metrics: Metrics,
/// Peer address at the time of creation.
///
/// Socket may become disconnected later,
/// so we save it when `LoggingStream` is created.
peer_addr: SocketAddr,
}
impl<S: SessionStream> LoggingStream<S> {
pub fn new(inner: S, account_id: u32, events: Events) -> Result<Self> {
let peer_addr: SocketAddr = inner
.peer_addr()
.context("Attempt to create LoggingStream over an unconnected stream")?;
Ok(Self {
inner,
account_id,
events,
metrics: Metrics::new(),
peer_addr,
})
}
}
impl<S: SessionStream> AsyncRead for LoggingStream<S> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
let this = self.project();
let old_remaining = buf.remaining();
let res = this.inner.poll_read(cx, buf);
if let Poll::Ready(Err(ref err)) = res {
let peer_addr = this.peer_addr;
let log_message = format!(
"Read error on stream {peer_addr:?} after reading {} and writing {} bytes: {err}.",
this.metrics.total_read, this.metrics.total_written
);
this.events.emit(Event {
id: *this.account_id,
typ: EventType::Warning(log_message),
});
}
let n = old_remaining - buf.remaining();
this.metrics.total_read = this.metrics.total_read.saturating_add(n);
res
}
}
impl<S: SessionStream> AsyncWrite for LoggingStream<S> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
let this = self.project();
let res = this.inner.poll_write(cx, buf);
if let Poll::Ready(Ok(n)) = res {
this.metrics.total_written = this.metrics.total_written.saturating_add(n);
}
res
}
fn poll_flush(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<std::io::Result<()>> {
self.project().inner.poll_flush(cx)
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<std::io::Result<()>> {
self.project().inner.poll_shutdown(cx)
}
fn poll_write_vectored(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[std::io::IoSlice<'_>],
) -> Poll<std::io::Result<usize>> {
let this = self.project();
let res = this.inner.poll_write_vectored(cx, bufs);
if let Poll::Ready(Ok(n)) = res {
this.metrics.total_written = this.metrics.total_written.saturating_add(n);
}
res
}
fn is_write_vectored(&self) -> bool {
self.inner.is_write_vectored()
}
}
impl<S: SessionStream> SessionStream for LoggingStream<S> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.inner.set_read_timeout(timeout)
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.inner.peer_addr()
}
}

View File

@@ -963,7 +963,6 @@ 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::{E2EE_INFO_MSGS, TestContext, TestContextManager};
use crate::test_utils::{TestContext, TestContextManager};
#[test]
fn test_guess_msgtype_from_suffix() {
@@ -347,7 +347,7 @@ async fn test_markseen_msgs() -> Result<()> {
let chats = Chatlist::try_load(&bob, 0, None, None).await?;
assert_eq!(chats.len(), 1);
let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?;
assert_eq!(msgs.len(), E2EE_INFO_MSGS + 2);
assert_eq!(msgs.len(), 2);
assert_eq!(bob.get_fresh_msgs().await?.len(), 0);
// that has no effect in contact request
@@ -358,7 +358,7 @@ async fn test_markseen_msgs() -> Result<()> {
assert_eq!(bob_chat.blocked, Blocked::Request);
let msgs = chat::get_chat_msgs(&bob, bob_chat_id).await?;
assert_eq!(msgs.len(), E2EE_INFO_MSGS + 2);
assert_eq!(msgs.len(), 2);
bob_chat_id.accept(&bob).await.unwrap();
// bob sends to alice,
@@ -761,22 +761,19 @@ async fn test_delete_msgs_sync() -> Result<()> {
// Alice sends a messsage and receives it on the other device
let sent1 = alice.send_text(alice_chat_id, "foo").await;
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS + 1);
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, 1);
let msg = alice2.recv_msg(&sent1).await;
let alice2_chat_id = msg.chat_id;
assert_eq!(alice2.get_last_msg_in(alice2_chat_id).await.id, msg.id);
assert_eq!(
alice2_chat_id.get_msg_cnt(alice2).await?,
E2EE_INFO_MSGS + 1
);
assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, 1);
// Alice deletes the message; this should happen on both devices as well
delete_msgs(alice, &[sent1.sender_msg_id]).await?;
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, E2EE_INFO_MSGS);
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, 0);
test_utils::sync(alice, alice2).await;
assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, E2EE_INFO_MSGS);
assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, 0);
Ok(())
}

View File

@@ -20,7 +20,6 @@ use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::e2ee::EncryptHelper;
use crate::ensure_and_debug_assert;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::key::self_fingerprint;
use crate::key::{DcKey, SignedPublicKey};
@@ -309,7 +308,7 @@ impl MimeFactory {
} else if id == ContactId::SELF {
member_fingerprints.push(self_fingerprint.to_string());
} else {
ensure_and_debug_assert!(member_fingerprints.is_empty(), "If some past member is a key-contact, all other past members should be key-contacts too");
debug_assert!(member_fingerprints.is_empty(), "If some past member is a key-contact, all other past members should be key-contacts too");
}
}
member_timestamps.push(add_timestamp);
@@ -360,7 +359,7 @@ impl MimeFactory {
// if we are leaving the group.
past_member_fingerprints.push(self_fingerprint.to_string());
} else {
ensure_and_debug_assert!(past_member_fingerprints.is_empty(), "If some past member is a key-contact, all other past members should be key-contacts too");
debug_assert!(past_member_fingerprints.is_empty(), "If some past member is a key-contact, all other past members should be key-contacts too");
}
}
}
@@ -368,14 +367,8 @@ impl MimeFactory {
}
}
ensure_and_debug_assert!(
member_timestamps.len() >= to.len(),
"member_timestamps.len() ({}) < to.len() ({})",
member_timestamps.len(), to.len());
ensure_and_debug_assert!(
member_fingerprints.is_empty() || member_fingerprints.len() >= to.len(),
"member_fingerprints.len() ({}) < to.len() ({})",
member_fingerprints.len(), to.len());
debug_assert!(member_timestamps.len() >= to.len());
debug_assert!(member_fingerprints.is_empty() || member_fingerprints.len() >= to.len());
if to.len() > 1 {
if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
@@ -452,13 +445,9 @@ impl MimeFactory {
};
let attach_selfavatar = Self::should_attach_selfavatar(context, &msg).await;
ensure_and_debug_assert!(
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 {
@@ -679,13 +668,9 @@ impl MimeFactory {
));
}
ensure_and_debug_assert!(
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());
@@ -804,7 +789,7 @@ impl MimeFactory {
}
if let Loaded::Message { chat, .. } = &self.loaded {
if chat.typ == Chattype::OutBroadcast || chat.typ == Chattype::InBroadcast {
if chat.typ == Chattype::OutBroadcast {
headers.push((
"List-ID",
mail_builder::headers::text::Text::new(format!(
@@ -1334,10 +1319,7 @@ impl MimeFactory {
}
}
if chat.typ == Chattype::Group
|| chat.typ == Chattype::OutBroadcast
|| chat.typ == Chattype::InBroadcast
{
if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast {
headers.push((
"Chat-Group-Name",
mail_builder::headers::text::Text::new(chat.name.to_string()).into(),

View File

@@ -181,10 +181,10 @@ pub enum SystemMessage {
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged = 10,
/// "Messages are end-to-end encrypted."
/// "Messages are guaranteed to be end-to-end encrypted from now on."
ChatProtectionEnabled = 11,
/// "%1$s sent a message from another device.", deprecated 2025-07
/// "%1$s sent a message from another device."
ChatProtectionDisabled = 12,
/// Message can't be sent because of `Invalid unencrypted mail to <>`
@@ -213,9 +213,6 @@ pub enum SystemMessage {
/// This message contains a users iroh node address.
IrohNodeAddr = 40,
/// "Messages are end-to-end encrypted."
ChatE2ee = 50,
}
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
@@ -249,7 +246,6 @@ impl MimeMessage {
MimeMessage::merge_headers(
context,
&mut headers,
&mut headers_removed,
&mut recipients,
&mut past_members,
&mut from,
@@ -277,7 +273,6 @@ impl MimeMessage {
MimeMessage::merge_headers(
context,
&mut headers,
&mut headers_removed,
&mut recipients,
&mut past_members,
&mut from,
@@ -451,11 +446,26 @@ impl MimeMessage {
});
if let (Ok(mail), true) = (mail, is_encrypted) {
if !signatures.is_empty() {
// Unsigned "Subject" mustn't be prepended to messages shown as encrypted
// (<https://github.com/deltachat/deltachat-core-rust/issues/1790>).
// Other headers are removed by `MimeMessage::merge_headers()` except for "List-ID".
remove_header(&mut headers, "subject", &mut headers_removed);
remove_header(&mut headers, "list-id", &mut headers_removed);
// Remove unsigned opportunistically protected headers from messages considered
// Autocrypt-encrypted / displayed with padlock.
// For "Subject" see <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
for h in [
HeaderDef::Subject,
HeaderDef::ChatGroupId,
HeaderDef::ChatGroupName,
HeaderDef::ChatGroupNameChanged,
HeaderDef::ChatGroupNameTimestamp,
HeaderDef::ChatGroupAvatar,
HeaderDef::ChatGroupMemberRemoved,
HeaderDef::ChatGroupMemberAdded,
HeaderDef::ChatGroupMemberTimestamps,
HeaderDef::ChatGroupPastMembers,
HeaderDef::ChatDelete,
HeaderDef::ChatEdit,
HeaderDef::ChatUserAvatar,
] {
remove_header(&mut headers, h.get_headername(), &mut headers_removed);
}
}
// let known protected headers from the decrypted
@@ -468,7 +478,6 @@ impl MimeMessage {
MimeMessage::merge_headers(
context,
&mut headers,
&mut headers_removed,
&mut recipients,
&mut past_members,
&mut inner_from,
@@ -1549,7 +1558,6 @@ impl MimeMessage {
fn merge_headers(
context: &Context,
headers: &mut HashMap<String, String>,
headers_removed: &mut HashSet<String>,
recipients: &mut Vec<SingleInfo>,
past_members: &mut Vec<SingleInfo>,
from: &mut Option<SingleInfo>,
@@ -1557,25 +1565,23 @@ impl MimeMessage {
chat_disposition_notification_to: &mut Option<SingleInfo>,
fields: &[mailparse::MailHeader<'_>],
) {
headers.retain(|k, _| {
!is_protected(k) || {
headers_removed.insert(k.to_string());
false
}
});
for field in fields {
// lowercasing all headers is technically not correct, but makes things work better
let key = field.get_key().to_lowercase();
if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
match addrparse_header(field) {
Ok(addrlist) => {
*chat_disposition_notification_to = addrlist.extract_single_info();
if !headers.contains_key(&key) || // key already exists, only overwrite known types (protected headers)
is_known(&key) || key.starts_with("chat-")
{
if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
match addrparse_header(field) {
Ok(addrlist) => {
*chat_disposition_notification_to = addrlist.extract_single_info();
}
Err(e) => warn!(context, "Could not read {} address: {}", key, e),
}
Err(e) => warn!(context, "Could not read {} address: {}", key, e),
} else {
let value = field.get_value();
headers.insert(key.to_string(), value);
}
} else {
let value = field.get_value();
headers.insert(key.to_string(), value);
}
}
let recipients_new = get_recipients(fields);
@@ -2003,30 +2009,26 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
}
}
/// Returns whether the outer header value must be ignored if the message contains a signed (and
/// optionally encrypted) part.
///
/// NB: There are known cases when Subject and List-ID only appear in the outer headers of
/// signed-only messages. Such messages are shown as unencrypted anyway.
fn is_protected(key: &str) -> bool {
key.starts_with("chat-")
|| matches!(
key,
"return-path"
| "auto-submitted"
| "autocrypt-setup-message"
| "date"
| "from"
| "sender"
| "reply-to"
| "to"
| "cc"
| "bcc"
| "message-id"
| "in-reply-to"
| "references"
| "secure-join"
)
/// Returns true if the header overwrites outer header
/// when it comes from protected headers.
fn is_known(key: &str) -> bool {
matches!(
key,
"return-path"
| "date"
| "from"
| "sender"
| "reply-to"
| "to"
| "cc"
| "bcc"
| "message-id"
| "in-reply-to"
| "references"
| "subject"
| "secure-join"
| "list-id"
)
}
/// Returns if the header is hidden and must be ignored in the IMF section.

View File

@@ -1402,26 +1402,6 @@ async fn test_x_microsoft_original_message_id_precedence() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_extra_imf_chat_header() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let chat_id = t.get_self_chat().await.id;
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
let sent_msg = t.pop_sent_msg().await;
// Check removal of some nonexistent "Chat-*" header to protect the code from future breakages.
let payload = sent_msg
.payload
.replace("Message-ID:", "Chat-Forty-Two: 42\r\nMessage-ID:");
let msg = MimeMessage::from_bytes(t, payload.as_bytes(), None)
.await
.unwrap();
assert!(msg.headers.contains_key("chat-version"));
assert!(!msg.headers.contains_key("chat-forty-two"));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_long_in_reply_to() -> Result<()> {
let t = TestContext::new_alice().await;

View File

@@ -1,10 +1,7 @@
use anyhow::Result;
use fast_socks5::client::Socks5Stream;
use std::net::SocketAddr;
use std::pin::Pin;
use std::time::Duration;
use tokio::io::{AsyncBufRead, AsyncRead, AsyncWrite, BufStream, BufWriter};
use tokio::net::TcpStream;
use tokio_io_timeout::TimeoutStream;
pub(crate) trait SessionStream:
@@ -12,90 +9,54 @@ pub(crate) trait SessionStream:
{
/// Change the read timeout on the session stream.
fn set_read_timeout(&mut self, timeout: Option<Duration>);
/// Returns the remote address that this stream is connected to.
fn peer_addr(&self) -> Result<SocketAddr>;
}
impl SessionStream for Box<dyn SessionStream> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.as_mut().set_read_timeout(timeout);
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.as_ref().peer_addr()
}
}
impl<T: SessionStream> SessionStream for async_native_tls::TlsStream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout);
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.get_ref().peer_addr()
}
}
impl<T: SessionStream> SessionStream for tokio_rustls::client::TlsStream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().0.set_read_timeout(timeout);
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.get_ref().0.peer_addr()
}
}
impl<T: SessionStream> SessionStream for BufStream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout);
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.get_ref().peer_addr()
}
}
impl<T: SessionStream> SessionStream for BufWriter<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout);
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.get_ref().peer_addr()
}
}
impl SessionStream for Pin<Box<TimeoutStream<TcpStream>>> {
impl<T: AsyncRead + AsyncWrite + Send + Sync + std::fmt::Debug> SessionStream
for Pin<Box<TimeoutStream<T>>>
{
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.as_mut().set_read_timeout_pinned(timeout);
}
fn peer_addr(&self) -> Result<SocketAddr> {
Ok(self.get_ref().peer_addr()?)
}
}
impl<T: SessionStream> SessionStream for Socks5Stream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_socket_mut().set_read_timeout(timeout)
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.get_socket_ref().peer_addr()
}
}
impl<T: SessionStream> SessionStream for shadowsocks::ProxyClientStream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout)
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.get_ref().peer_addr()
}
}
impl<T: SessionStream> SessionStream for async_imap::DeflateStream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout)
}
fn peer_addr(&self) -> Result<SocketAddr> {
self.get_ref().peer_addr()
}
}
/// Session stream with a read buffer.

View File

@@ -407,7 +407,6 @@ mod tests {
use crate::message::{MessageState, delete_msgs};
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
use crate::sql::housekeeping;
use crate::test_utils::E2EE_INFO_MSGS;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use crate::tools::SystemTime;
@@ -654,25 +653,13 @@ Here's my footer -- bob@example.net"
let chat_alice = alice.create_chat(&bob).await;
let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
let bob_msg = bob.recv_msg(&alice_msg).await;
assert_eq!(
get_chat_msgs(&alice, chat_alice.id).await?.len(),
E2EE_INFO_MSGS + 1
);
assert_eq!(
get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
E2EE_INFO_MSGS + 1
);
assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 1);
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 1);
let alice_msg2 = alice.send_text(chat_alice.id, "Hi again!").await;
bob.recv_msg(&alice_msg2).await;
assert_eq!(
get_chat_msgs(&alice, chat_alice.id).await?.len(),
E2EE_INFO_MSGS + 2
);
assert_eq!(
get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
E2EE_INFO_MSGS + 2
);
assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2);
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
bob_msg.chat_id.accept(&bob).await?;
@@ -680,18 +667,12 @@ Here's my footer -- bob@example.net"
send_reaction(&bob, bob_msg.id, "👍").await.unwrap();
expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
expect_no_unwanted_events(&bob).await;
assert_eq!(
get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
E2EE_INFO_MSGS + 2
);
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
let bob_reaction_msg = bob.pop_sent_msg().await;
let alice_reaction_msg = alice.recv_msg_hidden(&bob_reaction_msg).await;
assert_eq!(alice_reaction_msg.state, MessageState::InFresh);
assert_eq!(
get_chat_msgs(&alice, chat_alice.id).await?.len(),
E2EE_INFO_MSGS + 2
);
assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2);
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
assert_eq!(reactions.to_string(), "👍1");

View File

@@ -14,9 +14,7 @@ use mailparse::SingleInfo;
use num_traits::FromPrimitive;
use regex::Regex;
use crate::chat::{
self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, remove_from_chat_contacts_table,
};
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
use crate::contact::{Contact, ContactId, Origin, mark_contact_id_as_verified};
@@ -31,7 +29,6 @@ use crate::key::self_fingerprint_opt;
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::log::LogExt;
use crate::log::{info, warn};
use crate::logged_debug_assert;
use crate::message::{
self, Message, MessageState, MessengerMessage, MsgId, Viewtype, rfc724_mid_exists,
};
@@ -45,7 +42,7 @@ use crate::simplify;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::{self, buf_compress, remove_subject_prefix};
use crate::{chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_eq, location};
use crate::{chatlist_events, location};
use crate::{contact, imap};
/// This is the struct that is returned after receiving one email (aka MIME message).
@@ -1457,10 +1454,7 @@ async fn do_chat_assignment(
false => None,
};
if let Some(chat) = chat {
ensure_and_debug_assert!(
chat.typ == Chattype::Single,
"Chat {chat_id} is not Single",
);
debug_assert!(chat.typ == Chattype::Single);
let mut new_protection = match verified_encryption {
VerifiedEncryption::Verified => ProtectionStatus::Protected,
VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
@@ -1693,9 +1687,7 @@ async fn add_parts(
_ if chat.id.is_special() => GroupChangesInfo::default(),
Chattype::Single => GroupChangesInfo::default(),
Chattype::Mailinglist => GroupChangesInfo::default(),
Chattype::OutBroadcast => {
apply_out_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
}
Chattype::OutBroadcast => GroupChangesInfo::default(),
Chattype::Group => {
apply_group_changes(
context,
@@ -1709,7 +1701,7 @@ async fn add_parts(
.await?
}
Chattype::InBroadcast => {
apply_in_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
apply_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
}
};
@@ -2145,7 +2137,7 @@ RETURNING id
// afterwards insert additional parts.
replace_msg_id = None;
ensure_and_debug_assert!(!row_id.is_special(), "Rowid {row_id} is special");
debug_assert!(!row_id.is_special());
created_db_entries.push(row_id);
}
@@ -2408,11 +2400,7 @@ async fn lookup_chat_by_reply(
// lookup by reply should never be needed
// as we can directly assign the message to the chat
// by its group ID.
ensure_and_debug_assert!(
mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted(),
"Encrypted message has group ID {}",
mime_parser.get_chat_group_id().unwrap_or_default(),
);
debug_assert!(mime_parser.get_chat_group_id().is_none() || !mime_parser.was_encrypted());
// Try to assign message to the same chat as the parent message.
let Some(parent_chat_id) = ChatId::lookup_by_message(parent) else {
@@ -3450,30 +3438,7 @@ async fn apply_mailinglist_changes(
Ok(())
}
async fn apply_out_broadcast_changes(
context: &Context,
mime_parser: &MimeMessage,
chat: &mut Chat,
from_id: ContactId,
) -> Result<GroupChangesInfo> {
ensure!(chat.typ == Chattype::OutBroadcast);
if let Some(_removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
// The sender of the message left the broadcast channel
remove_from_chat_contacts_table(context, chat.id, from_id).await?;
return Ok(GroupChangesInfo {
better_msg: Some("".to_string()),
added_removed_id: None,
silent: true,
extra_msgs: vec![],
});
}
Ok(GroupChangesInfo::default())
}
async fn apply_in_broadcast_changes(
async fn apply_broadcast_changes(
context: &Context,
mime_parser: &MimeMessage,
chat: &mut Chat,
@@ -3494,15 +3459,6 @@ async fn apply_in_broadcast_changes(
)
.await?;
if let Some(_removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
// The only member added/removed message that is ever sent is "I left.",
// so, this is the only case we need to handle here
if from_id == ContactId::SELF {
better_msg
.get_or_insert(stock_str::msg_group_left_local(context, ContactId::SELF).await);
}
}
if send_event_chat_modified {
context.emit_event(EventType::ChatModified(chat.id));
chatlist_events::emit_chatlist_item_changed(context, chat.id);
@@ -3771,7 +3727,7 @@ async fn add_or_lookup_key_contacts_by_address_list(
}
}
ensure_and_debug_assert_eq!(contact_ids.len(), address_list.len(),);
debug_assert_eq!(contact_ids.len(), address_list.len());
Ok(contact_ids)
}
@@ -3841,11 +3797,7 @@ async fn lookup_key_contact_by_fingerprint(
context: &Context,
fingerprint: &str,
) -> Result<Option<ContactId>> {
logged_debug_assert!(
context,
!fingerprint.is_empty(),
"lookup_key_contact_by_fingerprint: fingerprint is empty."
);
debug_assert!(!fingerprint.is_empty());
if fingerprint.is_empty() {
// Avoid accidentally looking up a non-key-contact.
return Ok(None);
@@ -3929,7 +3881,7 @@ async fn lookup_key_contacts_by_address_list(
contact_ids.push(contact_id);
}
}
ensure_and_debug_assert_eq!(address_list.len(), contact_ids.len(),);
debug_assert_eq!(address_list.len(), contact_ids.len());
Ok(contact_ids)
}

View File

@@ -15,9 +15,8 @@ use crate::download::MIN_DOWNLOAD_LIMIT;
use crate::imap::prefetch_should_download;
use crate::imex::{ImexMode, imex};
use crate::securejoin::get_securejoin_qr;
use crate::test_utils::{
E2EE_INFO_MSGS, TestContext, TestContextManager, get_chat_msg, mark_as_verified,
};
use crate::test_utils::mark_as_verified;
use crate::test_utils::{TestContext, TestContextManager, get_chat_msg};
use crate::tools::{SystemTime, time};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -134,7 +133,7 @@ async fn test_adhoc_group_outgoing_show_accepted_contact_unaccepted() -> Result<
let chats = Chatlist::try_load(bob, 0, None, None).await?;
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0)?;
assert_eq!(chat_id.get_msg_cnt(bob).await?, E2EE_INFO_MSGS + 1);
assert_eq!(chat_id.get_msg_cnt(bob).await?, 1);
Ok(())
}
@@ -3683,24 +3682,6 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ignore_protected_headers_in_outer_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
send_text_msg(bob, bob_chat_id, "hi all!".to_string()).await?;
let mut sent_msg = bob.pop_sent_msg().await;
sent_msg.payload = sent_msg.payload.replace(
"Chat-Version:",
"Auto-Submitted: auto-generated\r\nChat-Version:",
);
alice.recv_msg(&sent_msg).await;
let ab_contact = alice.add_or_lookup_contact(bob).await;
assert!(!ab_contact.is_bot());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_member_list_on_rejoin() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -4411,7 +4392,7 @@ async fn test_create_group_with_big_msg() -> Result<()> {
// The big message must go away from the 1:1 chat.
let msgs = chat::get_chat_msgs(&alice, ab_chat_id).await?;
assert_eq!(msgs.len(), E2EE_INFO_MSGS);
assert!(msgs.is_empty());
Ok(())
}

View File

@@ -16,7 +16,6 @@ use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::key::{DcKey, Fingerprint, load_self_public_key};
use crate::log::{error, info, warn};
use crate::logged_debug_assert;
use crate::message::{Message, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
@@ -33,10 +32,9 @@ use qrinvite::QrInvite;
use crate::token::Namespace;
fn inviter_progress(context: &Context, contact_id: ContactId, progress: usize) {
logged_debug_assert!(
context,
debug_assert!(
progress <= 1000,
"inviter_progress: contact {contact_id}, progress={progress}, but value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success."
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
);
context.emit_event(EventType::SecurejoinInviterProgress {
contact_id,

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, messages_e2e_encrypted};
use crate::stock_str::{self, chat_protection_enabled};
use crate::test_utils::{
TestContext, TestContextManager, TimeShiftFalsePositiveNote, get_chat_msg,
};
@@ -246,7 +246,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
let chat = alice.get_chat(&bob).await;
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
assert!(msg.is_info());
let expected_text = messages_e2e_encrypted(&alice).await;
let expected_text = chat_protection_enabled(&alice).await;
assert_eq!(msg.get_text(), expected_text);
if case == SetupContactCase::CheckProtectionTimestamp {
assert_eq!(msg.timestamp_sort, vc_request_with_auth_ts_sent + 1);
@@ -296,7 +296,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
assert_eq!(msg.get_text(), stock_str::securejoin_wait(&bob).await);
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
assert!(msg.is_info());
assert_eq!(msg.get_text(), messages_e2e_encrypted(&bob).await);
assert_eq!(msg.get_text(), chat_protection_enabled(&bob).await);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -540,7 +540,7 @@ async fn test_secure_join() -> Result<()> {
// - You added member bob@example.net
let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await;
assert!(msg.is_info());
let expected_text = messages_e2e_encrypted(&alice).await;
let expected_text = chat_protection_enabled(&alice).await;
assert_eq!(msg.get_text(), expected_text);
}

File diff suppressed because one or more lines are too long

View File

@@ -65,6 +65,9 @@ pub enum StockMessage {
#[strum(props(fallback = "GIF"))]
Gif = 23,
#[strum(props(fallback = "Encrypted message"))]
EncryptedMsg = 24,
#[strum(props(fallback = "End-to-end encryption available"))]
E2eAvailable = 25,
@@ -282,7 +285,7 @@ pub enum StockMessage {
#[strum(props(fallback = "Member %1$s removed by %2$s."))]
MsgDelMemberBy = 131,
#[strum(props(fallback = "You left."))]
#[strum(props(fallback = "You left the group."))]
MsgYouLeftGroup = 132,
#[strum(props(fallback = "Group left by %1$s."))]
@@ -377,10 +380,9 @@ pub enum StockMessage {
#[strum(props(fallback = "I left the group."))]
MsgILeftGroup = 166,
#[strum(props(fallback = "Messages are end-to-end encrypted."))]
#[strum(props(fallback = "Messages are guaranteed to be end-to-end encrypted from now on."))]
ChatProtectionEnabled = 170,
// deprecated 2025-07
#[strum(props(fallback = "%1$s sent a message from another device."))]
ChatProtectionDisabled = 171,
@@ -411,16 +413,6 @@ pub enum StockMessage {
#[strum(props(fallback = "Establishing guaranteed end-to-end encryption, please wait…"))]
SecurejoinWait = 190,
#[strum(props(fallback = "❤️ Seems you're enjoying Delta Chat!
Please consider donating to help that Delta Chat stays free for everyone.
While Delta Chat is free to use and open source, development costs money.
Help keeping us to keep Delta Chat independent and make it more awesome in the future.
https://delta.chat/donate"))]
DonationRequest = 193,
}
impl StockMessage {
@@ -693,7 +685,7 @@ pub(crate) async fn msg_group_left_remote(context: &Context) -> String {
translated(context, StockMessage::MsgILeftGroup).await
}
/// Stock string: `You left.` or `Group left by %1$s.`.
/// Stock string: `You left the group.` or `Group left by %1$s.`.
pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouLeftGroup).await
@@ -793,11 +785,6 @@ pub(crate) async fn securejoin_wait(context: &Context) -> String {
translated(context, StockMessage::SecurejoinWait).await
}
/// Stock string: `❤️ Seems you're enjoying Delta Chat!`…
pub(crate) async fn donation_request(context: &Context) -> String {
translated(context, StockMessage::DonationRequest).await
}
/// Stock string: `Scan to chat with %1$s`.
pub(crate) async fn setup_contact_qr_description(
context: &Context,
@@ -1029,8 +1016,8 @@ pub(crate) async fn error_no_network(context: &Context) -> String {
translated(context, StockMessage::ErrorNoNetwork).await
}
/// Stock string: `Messages are end-to-end encrypted.`
pub(crate) async fn messages_e2e_encrypted(context: &Context) -> String {
/// Stock string: `Messages are guaranteed to be end-to-end encrypted from now on.`
pub(crate) async fn chat_protection_enabled(context: &Context) -> String {
translated(context, StockMessage::ChatProtectionEnabled).await
}
@@ -1301,7 +1288,7 @@ impl Context {
"[Error] No contact_id given".to_string()
}
}
ProtectionStatus::Protected => messages_e2e_encrypted(self).await,
ProtectionStatus::Protected => chat_protection_enabled(self).await,
}
}

View File

@@ -2,7 +2,6 @@
//!
//! This private module is only compiled for test runs.
use std::collections::{BTreeMap, HashSet};
use std::env::current_dir;
use std::fmt::Write;
use std::ops::{Deref, DerefMut};
use std::panic;
@@ -43,10 +42,6 @@ use crate::securejoin::{get_securejoin_qr, join_securejoin};
use crate::stock_str::StockStrings;
use crate::tools::time;
/// The number of info messages added to new e2ee chats.
/// Currently this is "End-to-end encryption available", string `E2eAvailable`.
pub const E2EE_INFO_MSGS: usize = 1;
#[allow(non_upper_case_globals)]
pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avatar900x900.png");
@@ -758,7 +753,7 @@ impl TestContext {
pub async fn add_or_lookup_address_contact(&self, other: &TestContext) -> Contact {
let contact_id = self.add_or_lookup_address_contact_id(other).await;
let contact = Contact::get_by_id(&self.ctx, contact_id).await.unwrap();
assert_eq!(contact.is_key_contact(), false);
debug_assert_eq!(contact.is_key_contact(), false);
contact
}
@@ -1068,25 +1063,6 @@ impl Drop for TestContext {
// Print the chats if runtime still exists.
handle.block_on(async move {
self.print_chats().await;
// If you set this to true, and a test fails,
// the sql databases will be saved into the current working directory
// so that you can examine them.
if std::env::var("DELTACHAT_SAVE_TMP_DB").is_ok() {
let _: u32 = self
.sql
.query_get_value("PRAGMA wal_checkpoint;", ())
.await
.unwrap()
.unwrap();
let from = self.get_dbfile();
let target = current_dir()
.unwrap()
.join(format!("test-account-{}.db", self.name()));
tokio::fs::copy(from, &target).await.unwrap();
eprintln!("Copied database from {from:?} to {target:?}\n");
}
});
}
});
@@ -1170,11 +1146,6 @@ impl Drop for InnerLogSink {
while let Ok(event) = self.events.try_recv() {
print_logevent(&event);
}
if std::env::var("DELTACHAT_SAVE_TMP_DB").is_err() {
eprintln!(
"note: If you want to examine the database files, set environment variable DELTACHAT_SAVE_TMP_DB=1"
)
}
}
}

View File

@@ -15,9 +15,7 @@ use crate::mimeparser::SystemMessage;
use crate::receive_imf::receive_imf;
use crate::securejoin::{get_securejoin_qr, join_securejoin};
use crate::stock_str;
use crate::test_utils::{
E2EE_INFO_MSGS, TestContext, TestContextManager, get_chat_msg, mark_as_verified,
};
use crate::test_utils::{TestContext, TestContextManager, get_chat_msg, mark_as_verified};
use crate::tools::SystemTime;
use crate::{e2ee, message};
@@ -134,7 +132,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
assert!(chat.is_protected());
let msg = get_chat_msg(&alice, chat.id, 0, 1).await;
let expected_text = stock_str::messages_e2e_encrypted(&alice).await;
let expected_text = stock_str::chat_protection_enabled(&alice).await;
assert_eq!(msg.text, expected_text);
}
@@ -144,7 +142,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
assert!(chat.is_protected());
let msg0 = get_chat_msg(&fiona, chat.id, 0, 1).await;
let expected_text = stock_str::messages_e2e_encrypted(&fiona).await;
let expected_text = stock_str::chat_protection_enabled(&fiona).await;
assert_eq!(msg0.text, expected_text);
}
@@ -164,7 +162,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
let chat = alice.get_chat(&fiona_new).await;
assert!(!chat.is_protected());
let msg = get_chat_msg(&alice, chat.id, 1, E2EE_INFO_MSGS + 1).await;
let msg = get_chat_msg(&alice, chat.id, 0, 1).await;
assert_eq!(msg.text, "I have a new device");
// After recreating the chat, it should still be unprotected
@@ -270,7 +268,7 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> {
.await?;
let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 1).await;
let enabled = stock_str::messages_e2e_encrypted(&alice).await;
let enabled = stock_str::chat_protection_enabled(&alice).await;
assert_eq!(msg0.text, enabled);
assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatProtectionEnabled);

View File

@@ -763,57 +763,5 @@ pub(crate) fn inc_and_check<T: PrimInt + AddAssign + std::fmt::Debug>(
Ok(())
}
/// Returns early with an error if a condition is not satisfied.
/// In non-optimized builds, panics instead if so.
#[macro_export]
macro_rules! ensure_and_debug_assert {
($cond:expr, $($arg:tt)*) => {
let cond_val = $cond;
debug_assert!(cond_val, $($arg)*);
anyhow::ensure!(cond_val, $($arg)*);
};
}
/// Returns early with an error on two expressions inequality.
/// In non-optimized builds, panics instead if so.
#[macro_export]
macro_rules! ensure_and_debug_assert_eq {
($left:expr, $right:expr, $($arg:tt)*) => {
match (&$left, &$right) {
(left_val, right_val) => {
debug_assert_eq!(left_val, right_val, $($arg)*);
anyhow::ensure!(left_val == right_val, $($arg)*);
}
}
};
}
/// Returns early with an error on two expressions equality.
/// In non-optimized builds, panics instead if so.
#[macro_export]
macro_rules! ensure_and_debug_assert_ne {
($left:expr, $right:expr, $($arg:tt)*) => {
match (&$left, &$right) {
(left_val, right_val) => {
debug_assert_ne!(left_val, right_val, $($arg)*);
anyhow::ensure!(left_val != right_val, $($arg)*);
}
}
};
}
/// Logs a warning if a condition is not satisfied.
/// In non-optimized builds, panics also if so.
#[macro_export]
macro_rules! logged_debug_assert {
($ctx:expr, $cond:expr, $($arg:tt)*) => {
let cond_val = $cond;
if !cond_val {
warn!($ctx, $($arg)*);
}
debug_assert!(cond_val, $($arg)*);
};
}
#[cfg(test)]
mod tools_tests;

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::{E2EE_INFO_MSGS, TestContext, TestContextManager};
use crate::test_utils::{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?, E2EE_INFO_MSGS + 1);
assert_eq!(bob_grp.get_msg_cnt(&bob).await?, 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?, E2EE_INFO_MSGS + 1);
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1);
// Bob receives the instance.
let bob_instance = bob.recv_msg(sent1).await;
let bob_chat_id = bob_instance.chat_id;
assert_eq!(bob_instance.rfc724_mid, alice_instance.rfc724_mid);
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 1);
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1);
// Bob receives the status updates.
bob.recv_msg_trash(sent2).await;
@@ -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?, E2EE_INFO_MSGS + 1);
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 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?, E2EE_INFO_MSGS + 1);
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 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?, E2EE_INFO_MSGS + 2);
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2);
let info_msg = alice.get_last_msg().await;
assert!(info_msg.is_info());
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage);
@@ -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?, E2EE_INFO_MSGS + 2);
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2);
let info_msg = bob.get_last_msg().await;
assert!(info_msg.is_info());
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage);
@@ -1536,10 +1536,7 @@ async fn test_webxdc_info_msg() -> Result<()> {
let alice2_instance = alice2.recv_msg(sent1).await;
let alice2_chat_id = alice2_instance.chat_id;
alice2.recv_msg_trash(sent2).await;
assert_eq!(
alice2_chat_id.get_msg_cnt(&alice2).await?,
E2EE_INFO_MSGS + 2
);
assert_eq!(alice2_chat_id.get_msg_cnt(&alice2).await?, 2);
let info_msg = alice2.get_last_msg().await;
assert!(info_msg.is_info());
assert_eq!(info_msg.get_info_type(), SystemMessage::WebxdcInfoMessage);
@@ -1575,13 +1572,13 @@ async fn test_webxdc_info_msg_cleanup_series() -> Result<()> {
.await?;
alice.flush_status_updates().await?;
let sent2 = &alice.pop_sent_msg().await;
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 2);
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2);
alice
.send_webxdc_status_update(alice_instance.id, r#"{"info":"i2", "payload":2}"#)
.await?;
alice.flush_status_updates().await?;
let sent3 = &alice.pop_sent_msg().await;
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, E2EE_INFO_MSGS + 2);
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2);
let info_msg = alice.get_last_msg().await;
assert_eq!(info_msg.get_text(), "i2");
@@ -1589,9 +1586,9 @@ async fn test_webxdc_info_msg_cleanup_series() -> Result<()> {
let bob_instance = bob.recv_msg(sent1).await;
let bob_chat_id = bob_instance.chat_id;
bob.recv_msg_trash(sent2).await;
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 2);
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2);
bob.recv_msg_trash(sent3).await;
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, E2EE_INFO_MSGS + 2);
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2);
let info_msg = bob.get_last_msg().await;
assert_eq!(info_msg.get_text(), "i2");

View File

@@ -1,8 +1,7 @@
Group#Chat#10: Group chat [3 member(s)]
--------------------------------------------------------------------------------
Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#11🔒: (Contact#Contact#10): Hi! I created a group. [FRESH]
Msg#12🔒: Me (Contact#Contact#Self): You left. [INFO]
Msg#13🔒: (Contact#Contact#10): Member charlie@example.net added by alice@example.org. [FRESH][INFO]
Msg#14🔒: (Contact#Contact#10): What a silence! [FRESH]
Msg#10🔒: (Contact#Contact#10): Hi! I created a group. [FRESH]
Msg#11🔒: Me (Contact#Contact#Self): You left the group. [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]
--------------------------------------------------------------------------------

View File

@@ -1,5 +1,5 @@
Single#Chat#10: bob@example.net [KEY bob@example.net] 🛡️
--------------------------------------------------------------------------------
Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO 🛡️]
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#11🔒: Me (Contact#Contact#Self): Test This is encrypted, signed, and has an Autocrypt Header without prefer-encrypt=mutual. √
--------------------------------------------------------------------------------

View File

@@ -1,6 +1,6 @@
Single#Chat#10: bob@example.net [KEY bob@example.net] 🛡️
--------------------------------------------------------------------------------
Msg#10: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO 🛡️]
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#11🔒: (Contact#Contact#10): Heyho from DC [FRESH]
Msg#13🔒: Me (Contact#Contact#Self): Sending with DC again √
--------------------------------------------------------------------------------

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 end-to-end encrypted. [NOTICED][INFO 🛡️]
Msg#17: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#18🔒: (Contact#Contact#10): Member Me added by alice@example.org. [FRESH][INFO]
--------------------------------------------------------------------------------