From ba573bb334a40c661867bb03490ef01bee569ec2 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 1 Nov 2025 00:16:45 +0100 Subject: [PATCH 01/44] fix python lint errors --- deltachat-rpc-client/tests/test_something.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index ad30f5eac7..027840a1d4 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -11,7 +11,7 @@ import pytest from deltachat_rpc_client import Contact, EventType, Message, events -from deltachat_rpc_client.const import DownloadState, MessageState +from deltachat_rpc_client.const import MessageState from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS from deltachat_rpc_client.rpc import JsonRpcError From b69082df0a03cffb5683f84974cc55ce6e4bed7b Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 6 Nov 2025 16:05:20 +0100 Subject: [PATCH 02/44] fix lint of python test --- deltachat-rpc-client/tests/test_something.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 027840a1d4..ad30f5eac7 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -11,7 +11,7 @@ import pytest from deltachat_rpc_client import Contact, EventType, Message, events -from deltachat_rpc_client.const import MessageState +from deltachat_rpc_client.const import DownloadState, MessageState from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS from deltachat_rpc_client.rpc import JsonRpcError From 17df902e3fb09732f11f29edb2456be7fb164b4b Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 14 Nov 2025 20:54:15 +0100 Subject: [PATCH 03/44] restore test `test_webxdc_update_for_not_downloaded_instance` and rename and modify it to employ message reordering instead of a partially downloaded instance --- src/webxdc/webxdc_tests.rs | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/webxdc/webxdc_tests.rs b/src/webxdc/webxdc_tests.rs index acd15cb480..6aa51b2b1a 100644 --- a/src/webxdc/webxdc_tests.rs +++ b/src/webxdc/webxdc_tests.rs @@ -328,6 +328,46 @@ async fn test_webxdc_contact_request() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_webxdc_update_for_not_yet_received_instance() -> Result<()> { + // Alice sends an instance and an update + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat = alice.create_chat(&bob).await; + let mut alice_instance = create_webxdc_instance( + &alice, + "chess.xdc", + include_bytes!("../../test-data/webxdc/chess.xdc"), + )?; + let sent1 = alice.send_msg(chat.id, &mut alice_instance).await; + let alice_instance = sent1.load_from_db().await; + alice + .send_webxdc_status_update( + alice_instance.id, + r#"{"payload": 7, "summary":"sum", "document":"doc"}"#, + ) + .await?; + alice.flush_status_updates().await?; + let sent2 = alice.pop_sent_msg().await; + + // Bob receives a status update before instance + bob.recv_msg_trash(&sent2).await; + // Bob downloads instance, updates should be assigned correctly + let _ = bob.recv_msg(&sent1).await; + let bob_instance = bob.get_last_msg().await; + assert_eq!(bob_instance.viewtype, Viewtype::Webxdc); + assert_eq!( + bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":7,"document":"doc","summary":"sum","serial":1,"max_serial":1}]"# + ); + let info = bob_instance.get_webxdc_info(&bob).await?; + assert_eq!(info.document, "doc"); + assert_eq!(info.summary, "sum"); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_delete_webxdc_instance() -> Result<()> { let t = TestContext::new_alice().await; From 6f9823f9bb51e5d1253aacd46cd3952d67f4fb49 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 14 Nov 2025 21:57:24 +0100 Subject: [PATCH 04/44] remove `test_webxdc_update_for_not_yet_received_instance` --- src/webxdc/webxdc_tests.rs | 40 -------------------------------------- 1 file changed, 40 deletions(-) diff --git a/src/webxdc/webxdc_tests.rs b/src/webxdc/webxdc_tests.rs index 6aa51b2b1a..acd15cb480 100644 --- a/src/webxdc/webxdc_tests.rs +++ b/src/webxdc/webxdc_tests.rs @@ -328,46 +328,6 @@ async fn test_webxdc_contact_request() -> Result<()> { Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_webxdc_update_for_not_yet_received_instance() -> Result<()> { - // Alice sends an instance and an update - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let chat = alice.create_chat(&bob).await; - let mut alice_instance = create_webxdc_instance( - &alice, - "chess.xdc", - include_bytes!("../../test-data/webxdc/chess.xdc"), - )?; - let sent1 = alice.send_msg(chat.id, &mut alice_instance).await; - let alice_instance = sent1.load_from_db().await; - alice - .send_webxdc_status_update( - alice_instance.id, - r#"{"payload": 7, "summary":"sum", "document":"doc"}"#, - ) - .await?; - alice.flush_status_updates().await?; - let sent2 = alice.pop_sent_msg().await; - - // Bob receives a status update before instance - bob.recv_msg_trash(&sent2).await; - // Bob downloads instance, updates should be assigned correctly - let _ = bob.recv_msg(&sent1).await; - let bob_instance = bob.get_last_msg().await; - assert_eq!(bob_instance.viewtype, Viewtype::Webxdc); - assert_eq!( - bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) - .await?, - r#"[{"payload":7,"document":"doc","summary":"sum","serial":1,"max_serial":1}]"# - ); - let info = bob_instance.get_webxdc_info(&bob).await?; - assert_eq!(info.document, "doc"); - assert_eq!(info.summary, "sum"); - - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_delete_webxdc_instance() -> Result<()> { let t = TestContext::new_alice().await; From 6042fd0faada589032c79c6d8c0d66b247ab7213 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sun, 2 Nov 2025 21:49:30 +0100 Subject: [PATCH 05/44] feat: send pre-message on messages with large attachments --- src/chat.rs | 66 ++++++++++++++++++++++++++++++++++++++++++---- src/download.rs | 14 ++++++++++ src/headerdef.rs | 22 ++++++++++++++++ src/mimefactory.rs | 60 +++++++++++++++++++++++++++++++++++++---- 4 files changed, 152 insertions(+), 10 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index e68ae7c61f..7876307088 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -27,7 +27,9 @@ use crate::constants::{ use crate::contact::{self, Contact, ContactId, Origin}; use crate::context::Context; use crate::debug_logging::maybe_set_logging_xdc; -use crate::download::DownloadState; +use crate::download::{ + DownloadState, PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD, PRE_MESSAGE_SIZE_WARNING_THRESHOLD, +}; use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers}; use crate::events::EventType; use crate::key::self_fingerprint; @@ -35,7 +37,7 @@ 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::mimefactory::{MimeFactory, RenderedEmail}; use crate::mimeparser::SystemMessage; use crate::param::{Param, Params}; use crate::receive_imf::ReceivedMsg; @@ -2804,7 +2806,47 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - return Ok(Vec::new()); } - let rendered_msg = match mimefactory.render(context).await { + // render message and pre message. + // pre message is a small message with metadata + // which announces a larger message. Large messages are not downloaded in the background. + + let needs_pre_message = msg.viewtype.has_file() + && msg + .get_filebytes(context) + .await? + .context("filebytes not available, even though message has attachment")? + > PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD; + + let render_result: Result<(RenderedEmail, Option)> = async { + if needs_pre_message { + let mut mimefactory_full_msg = mimefactory.clone(); + mimefactory_full_msg.set_as_full_message(); + let rendered_msg = mimefactory_full_msg.render(context).await?; + + let mut mimefactory_pre_msg = mimefactory; + mimefactory_pre_msg.set_as_pre_message_for(&rendered_msg); + let rendered_pre_msg = mimefactory_pre_msg + .render(context) + .await + .context("pre-message failed to render")?; + + if rendered_pre_msg.message.len() > PRE_MESSAGE_SIZE_WARNING_THRESHOLD { + warn!( + context, + "pre message for message (MsgId={}) is larger than expected: {}", + msg.id, + rendered_pre_msg.message.len() + ); + } + + Ok((rendered_msg, Some(rendered_pre_msg))) + } else { + Ok((mimefactory.render(context).await?, None)) + } + } + .await; + + let (rendered_msg, rendered_pre_msg) = match render_result { Ok(res) => Ok(res), Err(err) => { message::set_msg_failed(context, msg, &err.to_string()).await?; @@ -2825,7 +2867,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - msg.id, needs_encryption ); - } + }; let now = smeared_time(context); @@ -2870,12 +2912,26 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - } else { for recipients_chunk in recipients.chunks(chunk_size) { let recipients_chunk = recipients_chunk.join(" "); + // send pre-message before actual message + if let Some(pre_msg) = &rendered_pre_msg { + let row_id = t.execute( + "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \ + VALUES (?1, ?2, ?3, ?4)", + ( + &pre_msg.rfc724_mid, + &recipients_chunk, + &pre_msg.message, + msg.id, // TODO: check if this is correct or we need another id here? + ), + )?; + row_ids.push(row_id.try_into()?); + } let row_id = t.execute( "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \ VALUES (?1, ?2, ?3, ?4)", ( &rendered_msg.rfc724_mid, - recipients_chunk, + &recipients_chunk, &rendered_msg.message, msg.id, ), diff --git a/src/download.rs b/src/download.rs index bf54662175..0b2003dd02 100644 --- a/src/download.rs +++ b/src/download.rs @@ -18,6 +18,20 @@ use crate::{EventType, chatlist_events}; /// `MIN_DELETE_SERVER_AFTER` increases the timeout in this case. pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60; +/// From this point onward outgoing messages are considered large +/// and get a pre-message, which announces the full message. +// this is only about sending so we can modify it any time. +pub(crate) const PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD: u64 = 140_000; + +/// Max message size to be fetched in the background. +/// This limit defines what messages are fully fetched in the background. +/// This is for all messages that don't have the full message header. +pub(crate) const MAX_FETCH_MSG_SIZE: usize = 1_000_000; + +/// Max size for pre messages. A warning is emitted when this is exceeded. +/// Should be well below `MAX_FETCH_MSG_SIZE` +pub(crate) const PRE_MESSAGE_SIZE_WARNING_THRESHOLD: usize = 150_000; + /// Download state of the message. #[derive( Debug, diff --git a/src/headerdef.rs b/src/headerdef.rs index 32c2281b58..eca9bc2fb7 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -102,6 +102,15 @@ pub enum HeaderDef { /// used to encrypt and decrypt messages. /// This secret is sent to a new member in the member-addition message. ChatBroadcastSecret, + /// This message announces a bigger message with attachment that is refereced by rfc724_mid. + #[strum(serialize = "Chat-Full-Message-ID")] // correct casing + ChatFullMessageId, + + /// This message has a pre-message + /// and thus this message can be skipped while fetching messages. + /// This is a cleartext / unproteced header. + #[strum(serialize = "Chat-Is-Full-Message")] // correct casing + ChatIsFullMessage, /// [Autocrypt](https://autocrypt.org/) header. Autocrypt, @@ -194,4 +203,17 @@ mod tests { ); assert_eq!(headers.get_header_value(HeaderDef::Autocrypt), None); } + + #[test] + /// Tests that headers have correct casing for sending. + fn header_name_correct_casing_for_sending() { + assert_eq!( + HeaderDef::ChatFullMessageId.get_headername(), + "Chat-Full-Message-ID" + ); + assert_eq!( + HeaderDef::ChatIsFullMessage.get_headername(), + "Chat-Is-Full-Message" + ); + } } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 3c7a2df638..a0289cb889 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -58,6 +58,15 @@ pub enum Loaded { }, } +#[derive(Debug, Clone, PartialEq)] +pub enum PreMessageMode { + /// adds a is full message header in unpretected part + FullMessage, + /// adds reference to full message to protected part + /// also adds metadata and hashes and explicitly excludes attachment + PreMessage { full_msg_rfc724_mid: String }, +} + /// Helper to construct mime messages. #[derive(Debug, Clone)] pub struct MimeFactory { @@ -145,6 +154,9 @@ pub struct MimeFactory { /// This field is used to sustain the topic id of webxdcs needed for peer channels. webxdc_topic: Option, + + /// This field is used when this is either a pre-message or a full-message. + pre_message_mode: Option, } /// Result of rendering a message, ready to be submitted to a send job. @@ -498,6 +510,7 @@ impl MimeFactory { sync_ids_to_delete: None, attach_selfavatar, webxdc_topic, + pre_message_mode: None, }; Ok(factory) } @@ -546,6 +559,7 @@ impl MimeFactory { sync_ids_to_delete: None, attach_selfavatar: false, webxdc_topic: None, + pre_message_mode: None, }; Ok(res) @@ -778,7 +792,10 @@ impl MimeFactory { headers.push(("Date", mail_builder::headers::raw::Raw::new(date).into())); let rfc724_mid = match &self.loaded { - Loaded::Message { msg, .. } => msg.rfc724_mid.clone(), + Loaded::Message { msg, .. } => match &self.pre_message_mode { + Some(PreMessageMode::PreMessage { .. }) => create_outgoing_rfc724_mid(), + _ => msg.rfc724_mid.clone(), + }, Loaded::Mdn { .. } => create_outgoing_rfc724_mid(), }; headers.push(( @@ -980,6 +997,22 @@ impl MimeFactory { "MIME-Version", mail_builder::headers::raw::Raw::new("1.0").into(), )); + + if self.pre_message_mode == Some(PreMessageMode::FullMessage) { + unprotected_headers.push(( + HeaderDef::ChatIsFullMessage.get_headername(), + mail_builder::headers::raw::Raw::new("1").into(), + )); + } else if let Some(PreMessageMode::PreMessage { + full_msg_rfc724_mid, + }) = self.pre_message_mode.clone() + { + unprotected_headers.push(( + HeaderDef::ChatFullMessageId.get_headername(), + mail_builder::headers::raw::Raw::new(full_msg_rfc724_mid).into(), + )); + } + for header @ (original_header_name, _header_value) in &headers { let header_name = original_header_name.to_lowercase(); if header_name == "message-id" { @@ -1111,7 +1144,10 @@ impl MimeFactory { for (addr, key) in &encryption_pubkeys { let fingerprint = key.dc_fingerprint().hex(); let cmd = msg.param.get_cmd(); - let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup + let is_full_msg = + self.pre_message_mode != Some(PreMessageMode::FullMessage); + let should_do_gossip = is_full_msg + && cmd == SystemMessage::MemberAddedToGroup || cmd == SystemMessage::SecurejoinMessage || multiple_recipients && { let gossiped_timestamp: Option = context @@ -1837,8 +1873,12 @@ impl MimeFactory { // add attachment part if msg.viewtype.has_file() { - let file_part = build_body_file(context, &msg).await?; - parts.push(file_part); + if let Some(PreMessageMode::PreMessage { .. }) = self.pre_message_mode { + // TODO: generate thumbnail and attach it instead (if it makes sense) + } else { + let file_part = build_body_file(context, &msg).await?; + parts.push(file_part); + } } if let Some(msg_kml_part) = self.get_message_kml_part() { @@ -1883,7 +1923,7 @@ impl MimeFactory { } } - if self.attach_selfavatar { + if self.attach_selfavatar && self.pre_message_mode != Some(PreMessageMode::FullMessage) { match context.get_config(Config::Selfavatar).await? { Some(path) => match build_avatar_file(context, &path).await { Ok(avatar) => headers.push(( @@ -1952,6 +1992,16 @@ impl MimeFactory { Ok(message) } + + pub fn set_as_full_message(&mut self) { + self.pre_message_mode = Some(PreMessageMode::FullMessage); + } + + pub fn set_as_pre_message_for(&mut self, full_message: &RenderedEmail) { + self.pre_message_mode = Some(PreMessageMode::PreMessage { + full_msg_rfc724_mid: full_message.rfc724_mid.clone(), + }); + } } fn hidden_recipients() -> Address<'static> { From 19545067f4e4942dd0f7bddbcaad0e5e1da46583 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 6 Nov 2025 18:01:22 +0100 Subject: [PATCH 06/44] allow unused for `MAX_FETCH_MSG_SIZE` --- src/download.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/download.rs b/src/download.rs index 0b2003dd02..fb730a9e57 100644 --- a/src/download.rs +++ b/src/download.rs @@ -26,6 +26,7 @@ pub(crate) const PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD: u64 = 140_000; /// Max message size to be fetched in the background. /// This limit defines what messages are fully fetched in the background. /// This is for all messages that don't have the full message header. +#[allow(unused)] pub(crate) const MAX_FETCH_MSG_SIZE: usize = 1_000_000; /// Max size for pre messages. A warning is emitted when this is exceeded. From f49ecfa2c88ad59abd28d9d08746a7a06ff3ba5a Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 6 Nov 2025 21:15:10 +0100 Subject: [PATCH 07/44] remove todo comment --- src/chat.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat.rs b/src/chat.rs index 7876307088..a1e4c04a4b 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2921,7 +2921,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - &pre_msg.rfc724_mid, &recipients_chunk, &pre_msg.message, - msg.id, // TODO: check if this is correct or we need another id here? + msg.id, ), )?; row_ids.push(row_id.try_into()?); From bf5d08130ac45704e126c79bf8c60d8462af8d61 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 8 Nov 2025 19:27:19 +0100 Subject: [PATCH 08/44] add tests for sending pre-messages --- src/download.rs | 223 +++++++++++++++++++++++++++++++++++++++++++++- src/test_utils.rs | 21 +++++ 2 files changed, 243 insertions(+), 1 deletion(-) diff --git a/src/download.rs b/src/download.rs index fb730a9e57..260a6b86bc 100644 --- a/src/download.rs +++ b/src/download.rs @@ -208,10 +208,13 @@ impl Session { #[cfg(test)] mod tests { + use mailparse::MailHeaderMap; use num_traits::FromPrimitive; use super::*; - use crate::chat::send_msg; + use crate::chat::{self, create_group, send_msg}; + use crate::headerdef::{HeaderDef, HeaderDefMap}; + use crate::message::Viewtype; use crate::receive_imf::receive_imf_from_inbox; use crate::test_utils::TestContext; @@ -310,4 +313,222 @@ mod tests { Ok(()) } + /// Tests that pre message is sent for attachment larger than `PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD` + /// Also test that pre message is sent first, before the full message + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_sending_pre_message() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat = alice.create_chat(&bob).await; + + let mut msg = Message::new(Viewtype::File); + msg.set_file_from_bytes(&alice.ctx, "test.bin", &[0u8; 300_000], None)?; + msg.set_text("test".to_owned()); + + // assert that test attachment is bigger than limit + assert!( + msg.get_filebytes(&alice.ctx).await?.unwrap() > PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD + ); + + let msg_id = chat::send_msg(&alice.ctx, chat.id, &mut msg).await.unwrap(); + let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; + + // pre-message and full message should be present + // and test that correct headers are present on both messages + assert_eq!(smtp_rows.len(), 2); + let pre_message = mailparse::parse_mail( + smtp_rows + .first() + .expect("first element exists") + .2 + .as_bytes(), + )?; + let full_message = mailparse::parse_mail( + smtp_rows + .get(1) + .expect("second element exists") + .2 + .as_bytes(), + )?; + + assert!( + pre_message + .get_headers() + .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) + .is_none() + ); + assert!( + full_message + .get_headers() + .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) + .is_some() + ); + + assert_eq!( + pre_message + .headers + .get_header_value(HeaderDef::ChatFullMessageId), + full_message.headers.get_header_value(HeaderDef::MessageId) + ); + assert!( + full_message + .headers + .get_header_value(HeaderDef::ChatFullMessageId) + .is_none() + ); + + // full message should have the rfc message id + assert_eq!( + full_message.headers.get_header_value(HeaderDef::MessageId), + Some(msg.rfc724_mid) + ); + + // test that message ids are different + assert_ne!( + pre_message.headers.get_header_value(HeaderDef::MessageId), + full_message.headers.get_header_value(HeaderDef::MessageId) + ); + + // also test that Autocrypt-gossip and selfavatar should never go into full-messages + // TODO: (this needs decryption, right?) + Ok(()) + } + + /// Tests that no pre message is sent for normal message + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_not_sending_pre_message_no_attachment() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat = alice.create_chat(&bob).await; + + // send normal text message + let mut msg = Message::new(Viewtype::Text); + msg.set_text("test".to_owned()); + let msg_id = chat::send_msg(&alice.ctx, chat.id, &mut msg).await.unwrap(); + let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; + + // only one message and no "is full message" header should be present + assert_eq!(smtp_rows.len(), 1); + + let mime = smtp_rows.first().expect("first element exists").2.clone(); + let mail = mailparse::parse_mail(mime.as_bytes())?; + + assert!( + mail.get_headers() + .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) + .is_none() + ); + assert!( + mail.get_headers() + .get_first_header(HeaderDef::ChatFullMessageId.get_headername()) + .is_none() + ); + Ok(()) + } + + /// Tests that no pre message is sent for attachment smaller than `PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD` + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_not_sending_pre_message_for_small_attachment() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat = alice.create_chat(&bob).await; + + let mut msg = Message::new(Viewtype::File); + msg.set_file_from_bytes(&alice.ctx, "test.bin", &[0u8; 100_000], None)?; + msg.set_text("test".to_owned()); + + // assert that test attachment is smaller than limit + assert!( + msg.get_filebytes(&alice.ctx).await?.unwrap() < PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD + ); + + let msg_id = chat::send_msg(&alice.ctx, chat.id, &mut msg).await.unwrap(); + let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; + + // only one message and no "is full message" header should be present + assert_eq!(smtp_rows.len(), 1); + + let mime = smtp_rows.first().expect("first element exists").2.clone(); + let mail = mailparse::parse_mail(mime.as_bytes())?; + + assert!( + mail.get_headers() + .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) + .is_none() + ); + assert!( + mail.get_headers() + .get_first_header(HeaderDef::ChatFullMessageId.get_headername()) + .is_none() + ); + + Ok(()) + } + + /// Tests that pre message is not send for large webxdc updates + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_render_webxdc_status_update_object_range() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group(&t, "a chat").await?; + + let instance = { + let mut instance = Message::new(Viewtype::File); + instance.set_file_from_bytes( + &t, + "minimal.xdc", + include_bytes!("../test-data/webxdc/minimal.xdc"), + None, + )?; + let instance_msg_id = send_msg(&t, chat_id, &mut instance).await?; + assert_eq!(instance.viewtype, Viewtype::Webxdc); + Message::load_from_db(&t, instance_msg_id).await + } + .unwrap(); + + t.pop_sent_msg().await; + assert_eq!(t.sql.count("SELECT COUNT(*) FROM smtp", ()).await?, 0); + + let long_text = String::from_utf8(vec![b'a'; 300_000])?; + assert!(long_text.len() > PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD.try_into().unwrap()); + t.send_webxdc_status_update(instance.id, &format!("{{\"payload\": \"{long_text}\"}}")) + .await?; + t.flush_status_updates().await?; + + assert_eq!(t.sql.count("SELECT COUNT(*) FROM smtp", ()).await?, 1); + Ok(()) + } + + // test that pre message is not send for large large text + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_not_sending_pre_message_for_large_text() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat = alice.create_chat(&bob).await; + + // send normal text message + let mut msg = Message::new(Viewtype::Text); + let long_text = String::from_utf8(vec![b'a'; 300_000])?; + assert!(long_text.len() > PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD.try_into().unwrap()); + msg.set_text(long_text); + let msg_id = chat::send_msg(&alice.ctx, chat.id, &mut msg).await.unwrap(); + let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; + + // only one message and no "is full message" header should be present + assert_eq!(smtp_rows.len(), 1); + + let mime = smtp_rows.first().expect("first element exists").2.clone(); + let mail = mailparse::parse_mail(mime.as_bytes())?; + + assert!( + mail.get_headers() + .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) + .is_none() + ); + assert!( + mail.get_headers() + .get_first_header(HeaderDef::ChatFullMessageId.get_headername()) + .is_none() + ); + Ok(()) + } } diff --git a/src/test_utils.rs b/src/test_utils.rs index 73e9875f13..f3b7dc836b 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -697,6 +697,27 @@ impl TestContext { }) } + pub async fn get_smtp_rows_for_msg(&self, msg_id: MsgId) -> Vec<(i64, MsgId, String, String)> { + self.ctx + .sql + .query_map_vec( + r#" + SELECT id, msg_id, mime, recipients + FROM smtp + WHERE msg_id=?"#, + (msg_id,), + |row| { + let rowid: i64 = row.get(0)?; + let msg_id: MsgId = row.get(1)?; + let mime: String = row.get(2)?; + let recipients: String = row.get(3)?; + Ok((rowid, msg_id, mime, recipients)) + }, + ) + .await + .unwrap() + } + /// Retrieves a sent sync message from the db. /// /// This retrieves and removes a sync message which has been scheduled to send from the jobs From 709a9656603799dc1f8df0a59867d00fe655ccdb Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 8 Nov 2025 19:44:54 +0100 Subject: [PATCH 09/44] delimit `Chat-Full-Message-ID` with `<>` like other message ids. --- src/mimefactory.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index a0289cb889..e8e17795ea 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1009,7 +1009,7 @@ impl MimeFactory { { unprotected_headers.push(( HeaderDef::ChatFullMessageId.get_headername(), - mail_builder::headers::raw::Raw::new(full_msg_rfc724_mid).into(), + mail_builder::headers::message_id::MessageId::new(full_msg_rfc724_mid).into(), )); } From 9ffe3002f45479080382197352c371c657104941 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 8 Nov 2025 19:56:35 +0100 Subject: [PATCH 10/44] fix test --- src/download.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/download.rs b/src/download.rs index 260a6b86bc..c86b50d814 100644 --- a/src/download.rs +++ b/src/download.rs @@ -380,7 +380,7 @@ mod tests { // full message should have the rfc message id assert_eq!( full_message.headers.get_header_value(HeaderDef::MessageId), - Some(msg.rfc724_mid) + Some(format!("<{}>", msg.rfc724_mid)) ); // test that message ids are different From a3846ee45eac538d77efd8d2b8013abee4625e2d Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 8 Nov 2025 20:41:36 +0100 Subject: [PATCH 11/44] test that Autocrypt-gossip and selfavatar should never go into full-messages and fix exclusion of gossip header, which was broken --- src/download.rs | 29 +++++++++++++++++++---------- src/mimefactory.rs | 15 ++++++++++----- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/download.rs b/src/download.rs index c86b50d814..af479d4313 100644 --- a/src/download.rs +++ b/src/download.rs @@ -216,6 +216,7 @@ mod tests { use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::message::Viewtype; use crate::receive_imf::receive_imf_from_inbox; + use crate::mimeparser::MimeMessage; use crate::test_utils::TestContext; #[test] @@ -315,11 +316,15 @@ mod tests { } /// Tests that pre message is sent for attachment larger than `PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD` /// Also test that pre message is sent first, before the full message + /// And that Autocrypt-gossip and selfavatar never go into full-messages #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_sending_pre_message() -> Result<()> { let alice = TestContext::new_alice().await; let bob = TestContext::new_bob().await; - let chat = alice.create_chat(&bob).await; + let fiona = TestContext::new_fiona().await; + let group_id = alice + .create_group_with_members("test group", &[&bob, &fiona]) + .await; let mut msg = Message::new(Viewtype::File); msg.set_file_from_bytes(&alice.ctx, "test.bin", &[0u8; 300_000], None)?; @@ -330,7 +335,9 @@ mod tests { msg.get_filebytes(&alice.ctx).await?.unwrap() > PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD ); - let msg_id = chat::send_msg(&alice.ctx, chat.id, &mut msg).await.unwrap(); + let msg_id = chat::send_msg(&alice.ctx, group_id, &mut msg) + .await + .unwrap(); let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; // pre-message and full message should be present @@ -343,13 +350,12 @@ mod tests { .2 .as_bytes(), )?; - let full_message = mailparse::parse_mail( - smtp_rows - .get(1) - .expect("second element exists") - .2 - .as_bytes(), - )?; + let full_message_bytes = smtp_rows + .get(1) + .expect("second element exists") + .2 + .as_bytes(); + let full_message = mailparse::parse_mail(full_message_bytes)?; assert!( pre_message @@ -390,7 +396,10 @@ mod tests { ); // also test that Autocrypt-gossip and selfavatar should never go into full-messages - // TODO: (this needs decryption, right?) + let decrypted_full_message = MimeMessage::from_bytes(&bob.ctx, full_message_bytes).await?; + assert!(!decrypted_full_message.decrypting_failed); + assert_eq!(decrypted_full_message.gossiped_keys.len(), 0); + assert_eq!(decrypted_full_message.user_avatar, None); Ok(()) } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index e8e17795ea..b2d814cdab 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1145,11 +1145,12 @@ impl MimeFactory { let fingerprint = key.dc_fingerprint().hex(); let cmd = msg.param.get_cmd(); let is_full_msg = - self.pre_message_mode != Some(PreMessageMode::FullMessage); - let should_do_gossip = is_full_msg - && cmd == SystemMessage::MemberAddedToGroup - || cmd == SystemMessage::SecurejoinMessage - || multiple_recipients && { + self.pre_message_mode == Some(PreMessageMode::FullMessage); + let should_do_gossip = !is_full_msg + && (cmd == SystemMessage::MemberAddedToGroup + || cmd == SystemMessage::SecurejoinMessage + || multiple_recipients) + && { let gossiped_timestamp: Option = context .sql .query_get_value( @@ -1186,6 +1187,10 @@ impl MimeFactory { continue; } + debug_assert!( + self.pre_message_mode != Some(PreMessageMode::FullMessage) + ); + let header = Aheader { addr: addr.clone(), public_key: key.clone(), From 74a1fc95496205db98cdeb024206bbcc5f9409e1 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 8 Nov 2025 21:21:16 +0100 Subject: [PATCH 12/44] fix bug that broke `receive_imf::receive_imf_tests::test_dont_reverify_by_self_on_outgoing_msg` --- src/mimefactory.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index b2d814cdab..c9be47db7f 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1144,13 +1144,9 @@ impl MimeFactory { for (addr, key) in &encryption_pubkeys { let fingerprint = key.dc_fingerprint().hex(); let cmd = msg.param.get_cmd(); - let is_full_msg = - self.pre_message_mode == Some(PreMessageMode::FullMessage); - let should_do_gossip = !is_full_msg - && (cmd == SystemMessage::MemberAddedToGroup - || cmd == SystemMessage::SecurejoinMessage - || multiple_recipients) - && { + let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup + || cmd == SystemMessage::SecurejoinMessage + || multiple_recipients && { let gossiped_timestamp: Option = context .sql .query_get_value( @@ -1183,7 +1179,10 @@ impl MimeFactory { let is_verified = verifier_id.is_some_and(|verifier_id| verifier_id != 0); - if !should_do_gossip { + let is_full_msg = + self.pre_message_mode == Some(PreMessageMode::FullMessage); + + if !should_do_gossip || is_full_msg { continue; } From 2d32a5882be55f1e2bdaef3ac1662515b9cc022d Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 14 Nov 2025 22:37:02 +0100 Subject: [PATCH 13/44] cargo fmt after rebase --- src/download.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/download.rs b/src/download.rs index af479d4313..1cff3a3c66 100644 --- a/src/download.rs +++ b/src/download.rs @@ -215,8 +215,8 @@ mod tests { use crate::chat::{self, create_group, send_msg}; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::message::Viewtype; - use crate::receive_imf::receive_imf_from_inbox; use crate::mimeparser::MimeMessage; + use crate::receive_imf::receive_imf_from_inbox; use crate::test_utils::TestContext; #[test] From 305498d577124c5480b31c2033144ae746dedee0 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 14 Nov 2025 23:10:22 +0100 Subject: [PATCH 14/44] fix typo --- src/mimefactory.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index c9be47db7f..355b6e8d49 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -60,7 +60,7 @@ pub enum Loaded { #[derive(Debug, Clone, PartialEq)] pub enum PreMessageMode { - /// adds a is full message header in unpretected part + /// adds a is full message header in unprotected part FullMessage, /// adds reference to full message to protected part /// also adds metadata and hashes and explicitly excludes attachment From 07c2d8e889a7080a3629c5f958b08466563401bf Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 20 Nov 2025 19:10:01 +0000 Subject: [PATCH 15/44] Apply suggestions from code review Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com> --- src/chat.rs | 4 ++-- src/download.rs | 2 +- src/mimefactory.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index a1e4c04a4b..e02c1414c6 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2808,7 +2808,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - // render message and pre message. // pre message is a small message with metadata - // which announces a larger message. Large messages are not downloaded in the background. + // which announces a larger full message. Full messages are not downloaded in the background. let needs_pre_message = msg.viewtype.has_file() && msg @@ -2833,7 +2833,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - if rendered_pre_msg.message.len() > PRE_MESSAGE_SIZE_WARNING_THRESHOLD { warn!( context, - "pre message for message (MsgId={}) is larger than expected: {}", + "Pre-message for message (MsgId={}) is larger than expected: {}.", msg.id, rendered_pre_msg.message.len() ); diff --git a/src/download.rs b/src/download.rs index 1cff3a3c66..20db97e7fd 100644 --- a/src/download.rs +++ b/src/download.rs @@ -25,7 +25,7 @@ pub(crate) const PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD: u64 = 140_000; /// Max message size to be fetched in the background. /// This limit defines what messages are fully fetched in the background. -/// This is for all messages that don't have the full message header. +/// This is for all messages that don't have the Chat-Is-Full-Message header. #[allow(unused)] pub(crate) const MAX_FETCH_MSG_SIZE: usize = 1_000_000; diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 355b6e8d49..12d4ca12d8 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -60,9 +60,9 @@ pub enum Loaded { #[derive(Debug, Clone, PartialEq)] pub enum PreMessageMode { - /// adds a is full message header in unprotected part + /// adds the Chat-Is-Full-Message header in unprotected part FullMessage, - /// adds reference to full message to protected part + /// adds the Chat-Full-Message-ID header to protected part /// also adds metadata and hashes and explicitly excludes attachment PreMessage { full_msg_rfc724_mid: String }, } From 7668b7fb0de7bef5df6461bef6bf569286e93d77 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 20 Nov 2025 20:11:43 +0100 Subject: [PATCH 16/44] remove mention of hashes from doc comment --- src/mimefactory.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 12d4ca12d8..949f643fa8 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -63,7 +63,7 @@ pub enum PreMessageMode { /// adds the Chat-Is-Full-Message header in unprotected part FullMessage, /// adds the Chat-Full-Message-ID header to protected part - /// also adds metadata and hashes and explicitly excludes attachment + /// also adds metadata and explicitly excludes attachment PreMessage { full_msg_rfc724_mid: String }, } From 23dd202536aaa5fefc52dae54aa6b6372a90be8c Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 20 Nov 2025 20:23:25 +0100 Subject: [PATCH 17/44] rename`PRE_MESSAGE_SIZE_WARNING_THRESHOLD` to `PRE_MSG_ATTACHMENT_SIZE_THRESHOLD` --- src/chat.rs | 4 ++-- src/download.rs | 18 +++++++----------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index e02c1414c6..ed20dd63ae 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -28,7 +28,7 @@ use crate::contact::{self, Contact, ContactId, Origin}; use crate::context::Context; use crate::debug_logging::maybe_set_logging_xdc; use crate::download::{ - DownloadState, PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD, PRE_MESSAGE_SIZE_WARNING_THRESHOLD, + DownloadState, PRE_MESSAGE_SIZE_WARNING_THRESHOLD, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD, }; use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers}; use crate::events::EventType; @@ -2815,7 +2815,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - .get_filebytes(context) .await? .context("filebytes not available, even though message has attachment")? - > PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD; + > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD; let render_result: Result<(RenderedEmail, Option)> = async { if needs_pre_message { diff --git a/src/download.rs b/src/download.rs index 20db97e7fd..096ef36719 100644 --- a/src/download.rs +++ b/src/download.rs @@ -21,7 +21,7 @@ pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60; /// From this point onward outgoing messages are considered large /// and get a pre-message, which announces the full message. // this is only about sending so we can modify it any time. -pub(crate) const PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD: u64 = 140_000; +pub(crate) const PRE_MSG_ATTACHMENT_SIZE_THRESHOLD: u64 = 140_000; /// Max message size to be fetched in the background. /// This limit defines what messages are fully fetched in the background. @@ -314,7 +314,7 @@ mod tests { Ok(()) } - /// Tests that pre message is sent for attachment larger than `PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD` + /// Tests that pre message is sent for attachment larger than `PRE_MSG_ATTACHMENT_SIZE_THRESHOLD` /// Also test that pre message is sent first, before the full message /// And that Autocrypt-gossip and selfavatar never go into full-messages #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -331,9 +331,7 @@ mod tests { msg.set_text("test".to_owned()); // assert that test attachment is bigger than limit - assert!( - msg.get_filebytes(&alice.ctx).await?.unwrap() > PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD - ); + assert!(msg.get_filebytes(&alice.ctx).await?.unwrap() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD); let msg_id = chat::send_msg(&alice.ctx, group_id, &mut msg) .await @@ -435,7 +433,7 @@ mod tests { Ok(()) } - /// Tests that no pre message is sent for attachment smaller than `PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD` + /// Tests that no pre message is sent for attachment smaller than `PRE_MSG_ATTACHMENT_SIZE_THRESHOLD` #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_not_sending_pre_message_for_small_attachment() -> Result<()> { let alice = TestContext::new_alice().await; @@ -447,9 +445,7 @@ mod tests { msg.set_text("test".to_owned()); // assert that test attachment is smaller than limit - assert!( - msg.get_filebytes(&alice.ctx).await?.unwrap() < PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD - ); + assert!(msg.get_filebytes(&alice.ctx).await?.unwrap() < PRE_MSG_ATTACHMENT_SIZE_THRESHOLD); let msg_id = chat::send_msg(&alice.ctx, chat.id, &mut msg).await.unwrap(); let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; @@ -498,7 +494,7 @@ mod tests { assert_eq!(t.sql.count("SELECT COUNT(*) FROM smtp", ()).await?, 0); let long_text = String::from_utf8(vec![b'a'; 300_000])?; - assert!(long_text.len() > PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD.try_into().unwrap()); + assert!(long_text.len() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD.try_into().unwrap()); t.send_webxdc_status_update(instance.id, &format!("{{\"payload\": \"{long_text}\"}}")) .await?; t.flush_status_updates().await?; @@ -517,7 +513,7 @@ mod tests { // send normal text message let mut msg = Message::new(Viewtype::Text); let long_text = String::from_utf8(vec![b'a'; 300_000])?; - assert!(long_text.len() > PRE_MESSAGE_ATTACHMENT_SIZE_THRESHOLD.try_into().unwrap()); + assert!(long_text.len() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD.try_into().unwrap()); msg.set_text(long_text); let msg_id = chat::send_msg(&alice.ctx, chat.id, &mut msg).await.unwrap(); let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; From d37c5651e7a9693360150c6c2646fb8172449822 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 20 Nov 2025 20:25:12 +0100 Subject: [PATCH 18/44] explain current `PRE_MSG_ATTACHMENT_SIZE_THRESHOLD` value --- src/download.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/download.rs b/src/download.rs index 096ef36719..de96b05c6e 100644 --- a/src/download.rs +++ b/src/download.rs @@ -21,6 +21,7 @@ pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60; /// From this point onward outgoing messages are considered large /// and get a pre-message, which announces the full message. // this is only about sending so we can modify it any time. +// current value is a bit less than the minimum auto download setting from the UIs (which is 160 KiB) pub(crate) const PRE_MSG_ATTACHMENT_SIZE_THRESHOLD: u64 = 140_000; /// Max message size to be fetched in the background. From 273f4970b16da08cbea56c209a4dbad94b8dc187 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 20 Nov 2025 20:26:23 +0100 Subject: [PATCH 19/44] rename `PRE_MESSAGE_SIZE_WARNING_THRESHOLD` to `PRE_MSG_ATTACHMENT_SIZE_THRESHOLD` --- src/chat.rs | 4 ++-- src/download.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index ed20dd63ae..4a7600b6f8 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -28,7 +28,7 @@ use crate::contact::{self, Contact, ContactId, Origin}; use crate::context::Context; use crate::debug_logging::maybe_set_logging_xdc; use crate::download::{ - DownloadState, PRE_MESSAGE_SIZE_WARNING_THRESHOLD, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD, + DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD, PRE_MSG_SIZE_WARNING_THRESHOLD, }; use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers}; use crate::events::EventType; @@ -2830,7 +2830,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - .await .context("pre-message failed to render")?; - if rendered_pre_msg.message.len() > PRE_MESSAGE_SIZE_WARNING_THRESHOLD { + if rendered_pre_msg.message.len() > PRE_MSG_SIZE_WARNING_THRESHOLD { warn!( context, "Pre-message for message (MsgId={}) is larger than expected: {}.", diff --git a/src/download.rs b/src/download.rs index de96b05c6e..68f4313bdc 100644 --- a/src/download.rs +++ b/src/download.rs @@ -32,7 +32,7 @@ pub(crate) const MAX_FETCH_MSG_SIZE: usize = 1_000_000; /// Max size for pre messages. A warning is emitted when this is exceeded. /// Should be well below `MAX_FETCH_MSG_SIZE` -pub(crate) const PRE_MESSAGE_SIZE_WARNING_THRESHOLD: usize = 150_000; +pub(crate) const PRE_MSG_SIZE_WARNING_THRESHOLD: usize = 150_000; /// Download state of the message. #[derive( From ef873d04e4176bf0c9072b6da8b7d6126c98aced Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 20 Nov 2025 20:33:38 +0100 Subject: [PATCH 20/44] use `TestContextManager` --- src/download.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/download.rs b/src/download.rs index 68f4313bdc..c8ce4ed339 100644 --- a/src/download.rs +++ b/src/download.rs @@ -218,7 +218,7 @@ mod tests { use crate::message::Viewtype; use crate::mimeparser::MimeMessage; use crate::receive_imf::receive_imf_from_inbox; - use crate::test_utils::TestContext; + use crate::test_utils::{TestContext, TestContextManager}; #[test] fn test_downloadstate_values() { @@ -320,9 +320,10 @@ mod tests { /// And that Autocrypt-gossip and selfavatar never go into full-messages #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_sending_pre_message() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let fiona = TestContext::new_fiona().await; + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let fiona = tcm.fiona().await; let group_id = alice .create_group_with_members("test group", &[&bob, &fiona]) .await; @@ -405,8 +406,9 @@ mod tests { /// Tests that no pre message is sent for normal message #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_not_sending_pre_message_no_attachment() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; let chat = alice.create_chat(&bob).await; // send normal text message @@ -437,8 +439,9 @@ mod tests { /// Tests that no pre message is sent for attachment smaller than `PRE_MSG_ATTACHMENT_SIZE_THRESHOLD` #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_not_sending_pre_message_for_small_attachment() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; let chat = alice.create_chat(&bob).await; let mut msg = Message::new(Viewtype::File); @@ -507,8 +510,9 @@ mod tests { // test that pre message is not send for large large text #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_not_sending_pre_message_for_large_text() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; let chat = alice.create_chat(&bob).await; // send normal text message From c2fc525fa6cf97108d70c6e9ce215ff0285b0112 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 20 Nov 2025 20:39:16 +0100 Subject: [PATCH 21/44] convert some comments in assert errors --- src/download.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/download.rs b/src/download.rs index c8ce4ed339..cb387919ad 100644 --- a/src/download.rs +++ b/src/download.rs @@ -383,16 +383,16 @@ mod tests { .is_none() ); - // full message should have the rfc message id assert_eq!( full_message.headers.get_header_value(HeaderDef::MessageId), - Some(format!("<{}>", msg.rfc724_mid)) + Some(format!("<{}>", msg.rfc724_mid)), + "full message should have the rfc message id of the database message" ); - // test that message ids are different assert_ne!( pre_message.headers.get_header_value(HeaderDef::MessageId), - full_message.headers.get_header_value(HeaderDef::MessageId) + full_message.headers.get_header_value(HeaderDef::MessageId), + "message ids of pre message and full message should be different" ); // also test that Autocrypt-gossip and selfavatar should never go into full-messages @@ -417,8 +417,7 @@ mod tests { let msg_id = chat::send_msg(&alice.ctx, chat.id, &mut msg).await.unwrap(); let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; - // only one message and no "is full message" header should be present - assert_eq!(smtp_rows.len(), 1); + assert_eq!(smtp_rows.len(), 1, "only one message should be sent"); let mime = smtp_rows.first().expect("first element exists").2.clone(); let mail = mailparse::parse_mail(mime.as_bytes())?; @@ -426,12 +425,14 @@ mod tests { assert!( mail.get_headers() .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) - .is_none() + .is_none(), + "no 'Chat-Is-Full-Message'-header should be present" ); assert!( mail.get_headers() .get_first_header(HeaderDef::ChatFullMessageId.get_headername()) - .is_none() + .is_none(), + "no 'Chat-Full-Message-ID'-header should be present" ); Ok(()) } From 49fc3b89126dbe6e4c8dc743807b653e1d0bf2ca Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 20 Nov 2025 20:49:25 +0100 Subject: [PATCH 22/44] rm raw string literal in `get_smtp_rows_for_msg` --- src/test_utils.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index f3b7dc836b..199d45aff9 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -701,10 +701,7 @@ impl TestContext { self.ctx .sql .query_map_vec( - r#" - SELECT id, msg_id, mime, recipients - FROM smtp - WHERE msg_id=?"#, + "SELECT id, msg_id, mime, recipients FROM smtp WHERE msg_id=?", (msg_id,), |row| { let rowid: i64 = row.get(0)?; From 67baf10262de9051f769c6cd31042694dccaf882 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 20 Nov 2025 21:00:36 +0100 Subject: [PATCH 23/44] move logic "!is_full_msg" check to `should_do_gossip` --- src/mimefactory.rs | 51 +++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 949f643fa8..fedd087e76 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1144,29 +1144,33 @@ impl MimeFactory { for (addr, key) in &encryption_pubkeys { let fingerprint = key.dc_fingerprint().hex(); let cmd = msg.param.get_cmd(); - let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup - || cmd == SystemMessage::SecurejoinMessage - || multiple_recipients && { - let gossiped_timestamp: Option = context - .sql - .query_get_value( - "SELECT timestamp + let is_full_msg = + self.pre_message_mode == Some(PreMessageMode::FullMessage); + let should_do_gossip = !is_full_msg + && (cmd == SystemMessage::MemberAddedToGroup + || cmd == SystemMessage::SecurejoinMessage + || multiple_recipients && { + let gossiped_timestamp: Option = context + .sql + .query_get_value( + "SELECT timestamp FROM gossip_timestamp WHERE chat_id=? AND fingerprint=?", - (chat.id, &fingerprint), - ) - .await?; - - // `gossip_period == 0` is a special case for testing, - // enabling gossip in every message. - // - // If current time is in the past compared to - // `gossiped_timestamp`, we also gossip because - // either the `gossiped_timestamp` or clock is wrong. - gossip_period == 0 - || gossiped_timestamp - .is_none_or(|ts| now >= ts + gossip_period || now < ts) - }; + (chat.id, &fingerprint), + ) + .await?; + + // `gossip_period == 0` is a special case for testing, + // enabling gossip in every message. + // + // If current time is in the past compared to + // `gossiped_timestamp`, we also gossip because + // either the `gossiped_timestamp` or clock is wrong. + gossip_period == 0 + || gossiped_timestamp.is_none_or(|ts| { + now >= ts + gossip_period || now < ts + }) + }); let verifier_id: Option = context .sql @@ -1179,10 +1183,7 @@ impl MimeFactory { let is_verified = verifier_id.is_some_and(|verifier_id| verifier_id != 0); - let is_full_msg = - self.pre_message_mode == Some(PreMessageMode::FullMessage); - - if !should_do_gossip || is_full_msg { + if !should_do_gossip { continue; } From 8e1468a04f8633b0c8ef6a7dde9290342792065f Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 20 Nov 2025 23:35:02 +0100 Subject: [PATCH 24/44] test that case insensitive header parsing works for HeaderDef::ChatIsFullMessage --- src/headerdef.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/headerdef.rs b/src/headerdef.rs index eca9bc2fb7..5445b700f8 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -202,6 +202,12 @@ mod tests { Some("Bob".to_string()) ); assert_eq!(headers.get_header_value(HeaderDef::Autocrypt), None); + + let (headers, _) = mailparse::parse_headers(b"chat-is-FuLL-MeSSage: 1").unwrap(); + assert_eq!( + headers.get_header_value(HeaderDef::ChatIsFullMessage), + Some("1".to_string()) + ); } #[test] From 1626fd4144dc0498f9405d1ccb722d8e7772d10e Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 21 Nov 2025 01:59:53 +0100 Subject: [PATCH 25/44] don't use HeaderDef for sending pre message headers in mimefactory --- src/headerdef.rs | 18 +++--------------- src/mimefactory.rs | 4 ++-- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/headerdef.rs b/src/headerdef.rs index 5445b700f8..23498244f5 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -103,13 +103,11 @@ pub enum HeaderDef { /// This secret is sent to a new member in the member-addition message. ChatBroadcastSecret, /// This message announces a bigger message with attachment that is refereced by rfc724_mid. - #[strum(serialize = "Chat-Full-Message-ID")] // correct casing ChatFullMessageId, /// This message has a pre-message /// and thus this message can be skipped while fetching messages. /// This is a cleartext / unproteced header. - #[strum(serialize = "Chat-Is-Full-Message")] // correct casing ChatIsFullMessage, /// [Autocrypt](https://autocrypt.org/) header. @@ -153,6 +151,9 @@ pub enum HeaderDef { impl HeaderDef { /// Returns the corresponding header string. + /// + /// Format is lower-kebab-case for easy comparisons. + /// This method is used in message receiving and testing. pub fn get_headername(&self) -> &'static str { self.into() } @@ -209,17 +210,4 @@ mod tests { Some("1".to_string()) ); } - - #[test] - /// Tests that headers have correct casing for sending. - fn header_name_correct_casing_for_sending() { - assert_eq!( - HeaderDef::ChatFullMessageId.get_headername(), - "Chat-Full-Message-ID" - ); - assert_eq!( - HeaderDef::ChatIsFullMessage.get_headername(), - "Chat-Is-Full-Message" - ); - } } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index fedd087e76..9b6c923968 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1000,7 +1000,7 @@ impl MimeFactory { if self.pre_message_mode == Some(PreMessageMode::FullMessage) { unprotected_headers.push(( - HeaderDef::ChatIsFullMessage.get_headername(), + "Chat-Is-Full-Message", mail_builder::headers::raw::Raw::new("1").into(), )); } else if let Some(PreMessageMode::PreMessage { @@ -1008,7 +1008,7 @@ impl MimeFactory { }) = self.pre_message_mode.clone() { unprotected_headers.push(( - HeaderDef::ChatFullMessageId.get_headername(), + "Chat-Full-Message-ID", mail_builder::headers::message_id::MessageId::new(full_msg_rfc724_mid).into(), )); } From cdf839ac90444e78bf02126ac0be2c6c251d5d64 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 21 Nov 2025 02:12:51 +0100 Subject: [PATCH 26/44] encrypt "Chat-Full-Message-ID" header --- src/download.rs | 64 ++++++++++++++++++++++++++++++---------------- src/mimefactory.rs | 2 +- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/download.rs b/src/download.rs index cb387919ad..567893c94e 100644 --- a/src/download.rs +++ b/src/download.rs @@ -343,13 +343,12 @@ mod tests { // pre-message and full message should be present // and test that correct headers are present on both messages assert_eq!(smtp_rows.len(), 2); - let pre_message = mailparse::parse_mail( - smtp_rows - .first() - .expect("first element exists") - .2 - .as_bytes(), - )?; + let pre_message_bytes = smtp_rows + .first() + .expect("first element exists") + .2 + .as_bytes(); + let pre_message = mailparse::parse_mail(pre_message_bytes)?; let full_message_bytes = smtp_rows .get(1) .expect("second element exists") @@ -370,19 +369,6 @@ mod tests { .is_some() ); - assert_eq!( - pre_message - .headers - .get_header_value(HeaderDef::ChatFullMessageId), - full_message.headers.get_header_value(HeaderDef::MessageId) - ); - assert!( - full_message - .headers - .get_header_value(HeaderDef::ChatFullMessageId) - .is_none() - ); - assert_eq!( full_message.headers.get_header_value(HeaderDef::MessageId), Some(format!("<{}>", msg.rfc724_mid)), @@ -400,6 +386,23 @@ mod tests { assert!(!decrypted_full_message.decrypting_failed); assert_eq!(decrypted_full_message.gossiped_keys.len(), 0); assert_eq!(decrypted_full_message.user_avatar, None); + assert!(!decrypted_full_message.header_exists(HeaderDef::ChatFullMessageId)); + + let decrypted_pre_message = MimeMessage::from_bytes(&bob.ctx, pre_message_bytes).await?; + assert_eq!( + decrypted_pre_message + .get_header(HeaderDef::ChatFullMessageId) + .map(String::from), + full_message.headers.get_header_value(HeaderDef::MessageId) + ); + assert!( + pre_message + .headers + .get_header_value(HeaderDef::ChatFullMessageId) + .is_none(), + "no Chat-Full-Message-ID header in unprotected headers of Pre-Message" + ); + Ok(()) } @@ -432,6 +435,11 @@ mod tests { mail.get_headers() .get_first_header(HeaderDef::ChatFullMessageId.get_headername()) .is_none(), + "no 'Chat-Full-Message-ID'-header should be present in clear text headers" + ); + let decrypted_message = MimeMessage::from_bytes(&bob.ctx, mime.as_bytes()).await?; + assert!( + !decrypted_message.header_exists(HeaderDef::ChatFullMessageId), "no 'Chat-Full-Message-ID'-header should be present" ); Ok(()) @@ -469,7 +477,13 @@ mod tests { assert!( mail.get_headers() .get_first_header(HeaderDef::ChatFullMessageId.get_headername()) - .is_none() + .is_none(), + "no 'Chat-Full-Message-ID'-header should be present in clear text headers" + ); + let decrypted_message = MimeMessage::from_bytes(&bob.ctx, mime.as_bytes()).await?; + assert!( + !decrypted_message.header_exists(HeaderDef::ChatFullMessageId), + "no 'Chat-Full-Message-ID'-header should be present" ); Ok(()) @@ -538,7 +552,13 @@ mod tests { assert!( mail.get_headers() .get_first_header(HeaderDef::ChatFullMessageId.get_headername()) - .is_none() + .is_none(), + "no 'Chat-Full-Message-ID'-header should be present in clear text headers" + ); + let decrypted_message = MimeMessage::from_bytes(&bob.ctx, mime.as_bytes()).await?; + assert!( + !decrypted_message.header_exists(HeaderDef::ChatFullMessageId), + "no 'Chat-Full-Message-ID'-header should be present" ); Ok(()) } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 9b6c923968..7d8b986191 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1007,7 +1007,7 @@ impl MimeFactory { full_msg_rfc724_mid, }) = self.pre_message_mode.clone() { - unprotected_headers.push(( + protected_headers.push(( "Chat-Full-Message-ID", mail_builder::headers::message_id::MessageId::new(full_msg_rfc724_mid).into(), )); From 1c9df9df91a55afc98f30c500fd1b6016fefd5d9 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 21 Nov 2025 02:26:15 +0100 Subject: [PATCH 27/44] remove accidental semicolon --- src/chat.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat.rs b/src/chat.rs index 4a7600b6f8..bdf019dd68 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2867,7 +2867,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - msg.id, needs_encryption ); - }; + } let now = smeared_time(context); From d9cf44e7b713cd34c3d3359932af33304db82c10 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 21 Nov 2025 02:28:40 +0100 Subject: [PATCH 28/44] format sql statement --- src/chat.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat.rs b/src/chat.rs index bdf019dd68..468d73f282 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2916,7 +2916,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - if let Some(pre_msg) = &rendered_pre_msg { let row_id = t.execute( "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \ - VALUES (?1, ?2, ?3, ?4)", + VALUES (?1, ?2, ?3, ?4)", ( &pre_msg.rfc724_mid, &recipients_chunk, From dcfab1bfd8fdbe159e180a822e73329084f0737c Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 21 Nov 2025 02:45:44 +0100 Subject: [PATCH 29/44] async closure to extra method for readability --- src/chat.rs | 98 ++++++++++++++++++++++++++++------------------------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 468d73f282..4d291bfa28 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2735,6 +2735,51 @@ async fn prepare_send_msg( Ok(row_ids) } +/// Renders the message or Full-Message and Pre-Message. +/// +/// Pre-Message is a small message with metadata which announces a larger Full-Message. +/// Full messages are not downloaded in the background. +/// +/// If pre-message is not nessesary this returns a normal message instead. +async fn render_mime_message_and_pre_message( + context: &Context, + msg: &mut Message, + mimefactory: MimeFactory, +) -> Result<(RenderedEmail, Option)> { + let needs_pre_message = msg.viewtype.has_file() + && msg + .get_filebytes(context) + .await? + .context("filebytes not available, even though message has attachment")? + > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD; + + if needs_pre_message { + let mut mimefactory_full_msg = mimefactory.clone(); + mimefactory_full_msg.set_as_full_message(); + let rendered_msg = mimefactory_full_msg.render(context).await?; + + let mut mimefactory_pre_msg = mimefactory; + mimefactory_pre_msg.set_as_pre_message_for(&rendered_msg); + let rendered_pre_msg = mimefactory_pre_msg + .render(context) + .await + .context("pre-message failed to render")?; + + if rendered_pre_msg.message.len() > PRE_MSG_SIZE_WARNING_THRESHOLD { + warn!( + context, + "Pre-message for message (MsgId={}) is larger than expected: {}.", + msg.id, + rendered_pre_msg.message.len() + ); + } + + Ok((rendered_msg, Some(rendered_pre_msg))) + } else { + Ok((mimefactory.render(context).await?, None)) + } +} + /// Constructs jobs for sending a message and inserts them into the appropriate table. /// /// Updates the message `GuaranteeE2ee` parameter and persists it @@ -2806,53 +2851,14 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - return Ok(Vec::new()); } - // render message and pre message. - // pre message is a small message with metadata - // which announces a larger full message. Full messages are not downloaded in the background. - - let needs_pre_message = msg.viewtype.has_file() - && msg - .get_filebytes(context) - .await? - .context("filebytes not available, even though message has attachment")? - > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD; - - let render_result: Result<(RenderedEmail, Option)> = async { - if needs_pre_message { - let mut mimefactory_full_msg = mimefactory.clone(); - mimefactory_full_msg.set_as_full_message(); - let rendered_msg = mimefactory_full_msg.render(context).await?; - - let mut mimefactory_pre_msg = mimefactory; - mimefactory_pre_msg.set_as_pre_message_for(&rendered_msg); - let rendered_pre_msg = mimefactory_pre_msg - .render(context) - .await - .context("pre-message failed to render")?; - - if rendered_pre_msg.message.len() > PRE_MSG_SIZE_WARNING_THRESHOLD { - warn!( - context, - "Pre-message for message (MsgId={}) is larger than expected: {}.", - msg.id, - rendered_pre_msg.message.len() - ); + let (rendered_msg, rendered_pre_msg) = + match render_mime_message_and_pre_message(context, msg, mimefactory).await { + Ok(res) => Ok(res), + Err(err) => { + message::set_msg_failed(context, msg, &err.to_string()).await?; + Err(err) } - - Ok((rendered_msg, Some(rendered_pre_msg))) - } else { - Ok((mimefactory.render(context).await?, None)) - } - } - .await; - - let (rendered_msg, rendered_pre_msg) = match render_result { - Ok(res) => Ok(res), - Err(err) => { - message::set_msg_failed(context, msg, &err.to_string()).await?; - Err(err) - } - }?; + }?; if needs_encryption && !rendered_msg.is_encrypted { /* unrecoverable */ From 6a3446467f42903f0ae5a6956835e4bac9033af0 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 21 Nov 2025 03:15:26 +0100 Subject: [PATCH 30/44] add Tests that pre message has autocrypt gossip headers and self avatar and full message doesn't have these headers --- src/download.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/src/download.rs b/src/download.rs index 567893c94e..77240e6bc4 100644 --- a/src/download.rs +++ b/src/download.rs @@ -211,14 +211,16 @@ impl Session { mod tests { use mailparse::MailHeaderMap; use num_traits::FromPrimitive; + use tokio::fs; use super::*; use crate::chat::{self, create_group, send_msg}; + use crate::config::Config; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::message::Viewtype; use crate::mimeparser::MimeMessage; use crate::receive_imf::receive_imf_from_inbox; - use crate::test_utils::{TestContext, TestContextManager}; + use crate::test_utils::{self, TestContext, TestContextManager}; #[test] fn test_downloadstate_values() { @@ -406,6 +408,74 @@ mod tests { Ok(()) } + /// Tests that pre message has autocrypt gossip headers and self avatar + /// and full message doesn't have these headers + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_pre_message_contains_selfavatar_and_gossip_and_full_message_does_not() + -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let fiona = tcm.fiona().await; + let group_id = alice + .create_group_with_members("test group", &[&bob, &fiona]) + .await; + + let mut msg = Message::new(Viewtype::File); + msg.set_file_from_bytes(&alice.ctx, "test.bin", &[0u8; 300_000], None)?; + msg.set_text("test".to_owned()); + + // assert that test attachment is bigger than limit + assert!(msg.get_filebytes(&alice.ctx).await?.unwrap() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD); + + // simulate conditions for sending self avatar + let avatar_src = alice.get_blobdir().join("avatar.png"); + fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES).await?; + alice + .set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) + .await?; + + let msg_id = chat::send_msg(&alice.ctx, group_id, &mut msg) + .await + .unwrap(); + let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; + + assert_eq!(smtp_rows.len(), 2); + let pre_message_bytes = smtp_rows + .first() + .expect("first element exists") + .2 + .as_bytes(); + let full_message_bytes = smtp_rows + .get(1) + .expect("second element exists") + .2 + .as_bytes(); + let full_message = mailparse::parse_mail(full_message_bytes)?; + + let decrypted_pre_message = MimeMessage::from_bytes(&bob.ctx, pre_message_bytes).await?; + assert!( + decrypted_pre_message + .get_header(HeaderDef::ChatFullMessageId) + .is_some(), + "tested message is not a pre-message, sending order may be broken" + ); + assert_ne!(decrypted_pre_message.gossiped_keys.len(), 0); + assert_ne!(decrypted_pre_message.user_avatar, None); + + let decrypted_full_message = MimeMessage::from_bytes(&bob.ctx, full_message_bytes).await?; + assert!( + full_message + .get_headers() + .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) + .is_some(), + "tested message is not a full-message, sending order may be broken" + ); + assert_eq!(decrypted_full_message.gossiped_keys.len(), 0); + assert_eq!(decrypted_full_message.user_avatar, None); + Ok(()) + } + /// Tests that no pre message is sent for normal message #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_not_sending_pre_message_no_attachment() -> Result<()> { From 9f42ade8c692170052f9844704d2b74004ca0c73 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 21 Nov 2025 03:40:54 +0100 Subject: [PATCH 31/44] don't send pre message for unencrypted messages and add test for it --- src/chat.rs | 1 + src/download.rs | 42 +++++++++++++++++++++++++++++++++++------- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 4d291bfa28..a8fc053d62 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2747,6 +2747,7 @@ async fn render_mime_message_and_pre_message( mimefactory: MimeFactory, ) -> Result<(RenderedEmail, Option)> { let needs_pre_message = msg.viewtype.has_file() + && msg.get_showpadlock() // unencrypted is likely email, we don't want to spam by sending multiple messages && msg .get_filebytes(context) .await? diff --git a/src/download.rs b/src/download.rs index 77240e6bc4..b9ae2711c5 100644 --- a/src/download.rs +++ b/src/download.rs @@ -214,8 +214,9 @@ mod tests { use tokio::fs; use super::*; - use crate::chat::{self, create_group, send_msg}; + use crate::chat::{self, ChatId, create_group, send_msg}; use crate::config::Config; + use crate::contact::Contact; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::message::Viewtype; use crate::mimeparser::MimeMessage; @@ -337,9 +338,7 @@ mod tests { // assert that test attachment is bigger than limit assert!(msg.get_filebytes(&alice.ctx).await?.unwrap() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD); - let msg_id = chat::send_msg(&alice.ctx, group_id, &mut msg) - .await - .unwrap(); + let msg_id = chat::send_msg(&alice.ctx, group_id, &mut msg).await?; let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; // pre-message and full message should be present @@ -435,9 +434,7 @@ mod tests { .set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) .await?; - let msg_id = chat::send_msg(&alice.ctx, group_id, &mut msg) - .await - .unwrap(); + let msg_id = chat::send_msg(&alice.ctx, group_id, &mut msg).await?; let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; assert_eq!(smtp_rows.len(), 2); @@ -476,6 +473,37 @@ mod tests { Ok(()) } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_unecrypted_gets_no_pre_message() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + + let contact_id = Contact::create(&alice.ctx, "example", "email@example.org").await?; + let chat_id = ChatId::create_for_contact(&alice.ctx, contact_id).await?; + + let mut msg = Message::new(Viewtype::File); + msg.set_file_from_bytes(&alice.ctx, "test.bin", &[0u8; 300_000], None)?; + msg.set_text("test".to_owned()); + + let msg_id = chat::send_msg(&alice.ctx, chat_id, &mut msg).await?; + let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; + + assert_eq!(smtp_rows.len(), 1); + let message_bytes = smtp_rows + .first() + .expect("first element exists") + .2 + .as_bytes(); + let message = mailparse::parse_mail(message_bytes)?; + assert!( + message + .get_headers() + .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) + .is_none(), + ); + Ok(()) + } + /// Tests that no pre message is sent for normal message #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_not_sending_pre_message_no_attachment() -> Result<()> { From f45faae8c1b5e0a368a94211d97d41ef69ce6bcb Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 21 Nov 2025 04:10:00 +0100 Subject: [PATCH 32/44] fix encryption check --- src/chat.rs | 2 +- src/mimefactory.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index a8fc053d62..5efd18519a 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2747,7 +2747,7 @@ async fn render_mime_message_and_pre_message( mimefactory: MimeFactory, ) -> Result<(RenderedEmail, Option)> { let needs_pre_message = msg.viewtype.has_file() - && msg.get_showpadlock() // unencrypted is likely email, we don't want to spam by sending multiple messages + && mimefactory.will_be_encrypted() // unencrypted is likely email, we don't want to spam by sending multiple messages && msg .get_filebytes(context) .await? diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 7d8b986191..b9f33a5b01 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -912,7 +912,7 @@ impl MimeFactory { )); } - let is_encrypted = self.encryption_pubkeys.is_some(); + let is_encrypted = self.will_be_encrypted(); // Add ephemeral timer for non-MDN messages. // For MDNs it does not matter because they are not visible @@ -1998,6 +1998,10 @@ impl MimeFactory { Ok(message) } + pub fn will_be_encrypted(&self) -> bool { + self.encryption_pubkeys.is_some() + } + pub fn set_as_full_message(&mut self) { self.pre_message_mode = Some(PreMessageMode::FullMessage); } From 0a5c1e285928921d38327abecf3e4325f53d7c9e Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sun, 23 Nov 2025 03:27:01 +0100 Subject: [PATCH 33/44] apply suggestions from code review: easier solution for `should_do_gossip` with less changes. thanks to iequidoo for suggesting it apply test naming suggestion remove autoxrypt and self avatar check in `test_sending_pre_message` because it is already in `test_pre_message_contains_selfavatar_and_gossip_and_full_message_does_not` set self.attach_selfavatar to false in render method if pre message mode is full message merge `test_not_sending_pre_message_for_large_text` into `test_not_sending_pre_message_no_attachment` --- src/download.rs | 77 +++++++++++++++++++--------------------------- src/mimefactory.rs | 57 +++++++++++++++++----------------- 2 files changed, 60 insertions(+), 74 deletions(-) diff --git a/src/download.rs b/src/download.rs index b9ae2711c5..bc9ed51e9d 100644 --- a/src/download.rs +++ b/src/download.rs @@ -382,11 +382,8 @@ mod tests { "message ids of pre message and full message should be different" ); - // also test that Autocrypt-gossip and selfavatar should never go into full-messages let decrypted_full_message = MimeMessage::from_bytes(&bob.ctx, full_message_bytes).await?; assert!(!decrypted_full_message.decrypting_failed); - assert_eq!(decrypted_full_message.gossiped_keys.len(), 0); - assert_eq!(decrypted_full_message.user_avatar, None); assert!(!decrypted_full_message.header_exists(HeaderDef::ChatFullMessageId)); let decrypted_pre_message = MimeMessage::from_bytes(&bob.ctx, pre_message_bytes).await?; @@ -410,8 +407,7 @@ mod tests { /// Tests that pre message has autocrypt gossip headers and self avatar /// and full message doesn't have these headers #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_pre_message_contains_selfavatar_and_gossip_and_full_message_does_not() - -> Result<()> { + async fn test_selfavatar_and_autocrypt_gossip_goto_pre_message() -> Result<()> { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; let bob = tcm.bob().await; @@ -540,6 +536,36 @@ mod tests { !decrypted_message.header_exists(HeaderDef::ChatFullMessageId), "no 'Chat-Full-Message-ID'-header should be present" ); + + // test that pre message is not send for large large text + let mut msg = Message::new(Viewtype::Text); + let long_text = String::from_utf8(vec![b'a'; 300_000])?; + assert!(long_text.len() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD.try_into().unwrap()); + msg.set_text(long_text); + let msg_id = chat::send_msg(&alice.ctx, chat.id, &mut msg).await.unwrap(); + let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; + + assert_eq!(smtp_rows.len(), 1, "only one message should be sent"); + + let mime = smtp_rows.first().expect("first element exists").2.clone(); + let mail = mailparse::parse_mail(mime.as_bytes())?; + + assert!( + mail.get_headers() + .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) + .is_none() + ); + assert!( + mail.get_headers() + .get_first_header(HeaderDef::ChatFullMessageId.get_headername()) + .is_none(), + "no 'Chat-Full-Message-ID'-header should be present in clear text headers" + ); + let decrypted_message = MimeMessage::from_bytes(&bob.ctx, mime.as_bytes()).await?; + assert!( + !decrypted_message.header_exists(HeaderDef::ChatFullMessageId), + "no 'Chat-Full-Message-ID'-header should be present" + ); Ok(()) } @@ -619,45 +645,4 @@ mod tests { assert_eq!(t.sql.count("SELECT COUNT(*) FROM smtp", ()).await?, 1); Ok(()) } - - // test that pre message is not send for large large text - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_not_sending_pre_message_for_large_text() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - let chat = alice.create_chat(&bob).await; - - // send normal text message - let mut msg = Message::new(Viewtype::Text); - let long_text = String::from_utf8(vec![b'a'; 300_000])?; - assert!(long_text.len() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD.try_into().unwrap()); - msg.set_text(long_text); - let msg_id = chat::send_msg(&alice.ctx, chat.id, &mut msg).await.unwrap(); - let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; - - // only one message and no "is full message" header should be present - assert_eq!(smtp_rows.len(), 1); - - let mime = smtp_rows.first().expect("first element exists").2.clone(); - let mail = mailparse::parse_mail(mime.as_bytes())?; - - assert!( - mail.get_headers() - .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) - .is_none() - ); - assert!( - mail.get_headers() - .get_first_header(HeaderDef::ChatFullMessageId.get_headername()) - .is_none(), - "no 'Chat-Full-Message-ID'-header should be present in clear text headers" - ); - let decrypted_message = MimeMessage::from_bytes(&bob.ctx, mime.as_bytes()).await?; - assert!( - !decrypted_message.header_exists(HeaderDef::ChatFullMessageId), - "no 'Chat-Full-Message-ID'-header should be present" - ); - Ok(()) - } } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index b9f33a5b01..10cf372b74 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1146,31 +1146,34 @@ impl MimeFactory { let cmd = msg.param.get_cmd(); let is_full_msg = self.pre_message_mode == Some(PreMessageMode::FullMessage); - let should_do_gossip = !is_full_msg - && (cmd == SystemMessage::MemberAddedToGroup - || cmd == SystemMessage::SecurejoinMessage - || multiple_recipients && { - let gossiped_timestamp: Option = context - .sql - .query_get_value( - "SELECT timestamp + + if is_full_msg { + continue; + } + + let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup + || cmd == SystemMessage::SecurejoinMessage + || multiple_recipients && { + let gossiped_timestamp: Option = context + .sql + .query_get_value( + "SELECT timestamp FROM gossip_timestamp WHERE chat_id=? AND fingerprint=?", - (chat.id, &fingerprint), - ) - .await?; - - // `gossip_period == 0` is a special case for testing, - // enabling gossip in every message. - // - // If current time is in the past compared to - // `gossiped_timestamp`, we also gossip because - // either the `gossiped_timestamp` or clock is wrong. - gossip_period == 0 - || gossiped_timestamp.is_none_or(|ts| { - now >= ts + gossip_period || now < ts - }) - }); + (chat.id, &fingerprint), + ) + .await?; + + // `gossip_period == 0` is a special case for testing, + // enabling gossip in every message. + // + // If current time is in the past compared to + // `gossiped_timestamp`, we also gossip because + // either the `gossiped_timestamp` or clock is wrong. + gossip_period == 0 + || gossiped_timestamp + .is_none_or(|ts| now >= ts + gossip_period || now < ts) + }; let verifier_id: Option = context .sql @@ -1187,10 +1190,6 @@ impl MimeFactory { continue; } - debug_assert!( - self.pre_message_mode != Some(PreMessageMode::FullMessage) - ); - let header = Aheader { addr: addr.clone(), public_key: key.clone(), @@ -1928,7 +1927,9 @@ impl MimeFactory { } } - if self.attach_selfavatar && self.pre_message_mode != Some(PreMessageMode::FullMessage) { + self.attach_selfavatar = + self.attach_selfavatar && self.pre_message_mode != Some(PreMessageMode::FullMessage); + if self.attach_selfavatar { match context.get_config(Config::Selfavatar).await? { Some(path) => match build_avatar_file(context, &path).await { Ok(avatar) => headers.push(( From 027f3d99988bc931a585fb528200ddfc461e6621 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Mon, 24 Nov 2025 03:01:48 +0100 Subject: [PATCH 34/44] reference to test contexts --- src/download.rs | 72 ++++++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/download.rs b/src/download.rs index bc9ed51e9d..875ba1dadb 100644 --- a/src/download.rs +++ b/src/download.rs @@ -324,21 +324,21 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_sending_pre_message() -> Result<()> { let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - let fiona = tcm.fiona().await; + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let fiona = &tcm.fiona().await; let group_id = alice - .create_group_with_members("test group", &[&bob, &fiona]) + .create_group_with_members("test group", &[bob, fiona]) .await; let mut msg = Message::new(Viewtype::File); - msg.set_file_from_bytes(&alice.ctx, "test.bin", &[0u8; 300_000], None)?; + msg.set_file_from_bytes(alice, "test.bin", &[0u8; 300_000], None)?; msg.set_text("test".to_owned()); // assert that test attachment is bigger than limit - assert!(msg.get_filebytes(&alice.ctx).await?.unwrap() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD); + assert!(msg.get_filebytes(alice).await?.unwrap() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD); - let msg_id = chat::send_msg(&alice.ctx, group_id, &mut msg).await?; + let msg_id = chat::send_msg(alice, group_id, &mut msg).await?; let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; // pre-message and full message should be present @@ -382,11 +382,11 @@ mod tests { "message ids of pre message and full message should be different" ); - let decrypted_full_message = MimeMessage::from_bytes(&bob.ctx, full_message_bytes).await?; + let decrypted_full_message = MimeMessage::from_bytes(bob, full_message_bytes).await?; assert!(!decrypted_full_message.decrypting_failed); assert!(!decrypted_full_message.header_exists(HeaderDef::ChatFullMessageId)); - let decrypted_pre_message = MimeMessage::from_bytes(&bob.ctx, pre_message_bytes).await?; + let decrypted_pre_message = MimeMessage::from_bytes(bob, pre_message_bytes).await?; assert_eq!( decrypted_pre_message .get_header(HeaderDef::ChatFullMessageId) @@ -409,19 +409,19 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_selfavatar_and_autocrypt_gossip_goto_pre_message() -> Result<()> { let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - let fiona = tcm.fiona().await; + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let fiona = &tcm.fiona().await; let group_id = alice - .create_group_with_members("test group", &[&bob, &fiona]) + .create_group_with_members("test group", &[bob, fiona]) .await; let mut msg = Message::new(Viewtype::File); - msg.set_file_from_bytes(&alice.ctx, "test.bin", &[0u8; 300_000], None)?; + msg.set_file_from_bytes(alice, "test.bin", &[0u8; 300_000], None)?; msg.set_text("test".to_owned()); // assert that test attachment is bigger than limit - assert!(msg.get_filebytes(&alice.ctx).await?.unwrap() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD); + assert!(msg.get_filebytes(alice).await?.unwrap() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD); // simulate conditions for sending self avatar let avatar_src = alice.get_blobdir().join("avatar.png"); @@ -430,7 +430,7 @@ mod tests { .set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap())) .await?; - let msg_id = chat::send_msg(&alice.ctx, group_id, &mut msg).await?; + let msg_id = chat::send_msg(alice, group_id, &mut msg).await?; let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; assert_eq!(smtp_rows.len(), 2); @@ -446,7 +446,7 @@ mod tests { .as_bytes(); let full_message = mailparse::parse_mail(full_message_bytes)?; - let decrypted_pre_message = MimeMessage::from_bytes(&bob.ctx, pre_message_bytes).await?; + let decrypted_pre_message = MimeMessage::from_bytes(bob, pre_message_bytes).await?; assert!( decrypted_pre_message .get_header(HeaderDef::ChatFullMessageId) @@ -456,7 +456,7 @@ mod tests { assert_ne!(decrypted_pre_message.gossiped_keys.len(), 0); assert_ne!(decrypted_pre_message.user_avatar, None); - let decrypted_full_message = MimeMessage::from_bytes(&bob.ctx, full_message_bytes).await?; + let decrypted_full_message = MimeMessage::from_bytes(bob, full_message_bytes).await?; assert!( full_message .get_headers() @@ -472,16 +472,16 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_unecrypted_gets_no_pre_message() -> Result<()> { let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; + let alice = &tcm.alice().await; - let contact_id = Contact::create(&alice.ctx, "example", "email@example.org").await?; - let chat_id = ChatId::create_for_contact(&alice.ctx, contact_id).await?; + let contact_id = Contact::create(alice, "example", "email@example.org").await?; + let chat_id = ChatId::create_for_contact(alice, contact_id).await?; let mut msg = Message::new(Viewtype::File); - msg.set_file_from_bytes(&alice.ctx, "test.bin", &[0u8; 300_000], None)?; + msg.set_file_from_bytes(alice, "test.bin", &[0u8; 300_000], None)?; msg.set_text("test".to_owned()); - let msg_id = chat::send_msg(&alice.ctx, chat_id, &mut msg).await?; + let msg_id = chat::send_msg(alice, chat_id, &mut msg).await?; let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; assert_eq!(smtp_rows.len(), 1); @@ -504,14 +504,14 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_not_sending_pre_message_no_attachment() -> Result<()> { let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - let chat = alice.create_chat(&bob).await; + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + let chat = alice.create_chat(bob).await; // send normal text message let mut msg = Message::new(Viewtype::Text); msg.set_text("test".to_owned()); - let msg_id = chat::send_msg(&alice.ctx, chat.id, &mut msg).await.unwrap(); + let msg_id = chat::send_msg(alice, chat.id, &mut msg).await.unwrap(); let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; assert_eq!(smtp_rows.len(), 1, "only one message should be sent"); @@ -531,7 +531,7 @@ mod tests { .is_none(), "no 'Chat-Full-Message-ID'-header should be present in clear text headers" ); - let decrypted_message = MimeMessage::from_bytes(&bob.ctx, mime.as_bytes()).await?; + let decrypted_message = MimeMessage::from_bytes(bob, mime.as_bytes()).await?; assert!( !decrypted_message.header_exists(HeaderDef::ChatFullMessageId), "no 'Chat-Full-Message-ID'-header should be present" @@ -542,7 +542,7 @@ mod tests { let long_text = String::from_utf8(vec![b'a'; 300_000])?; assert!(long_text.len() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD.try_into().unwrap()); msg.set_text(long_text); - let msg_id = chat::send_msg(&alice.ctx, chat.id, &mut msg).await.unwrap(); + let msg_id = chat::send_msg(alice, chat.id, &mut msg).await.unwrap(); let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; assert_eq!(smtp_rows.len(), 1, "only one message should be sent"); @@ -561,7 +561,7 @@ mod tests { .is_none(), "no 'Chat-Full-Message-ID'-header should be present in clear text headers" ); - let decrypted_message = MimeMessage::from_bytes(&bob.ctx, mime.as_bytes()).await?; + let decrypted_message = MimeMessage::from_bytes(bob, mime.as_bytes()).await?; assert!( !decrypted_message.header_exists(HeaderDef::ChatFullMessageId), "no 'Chat-Full-Message-ID'-header should be present" @@ -573,18 +573,18 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_not_sending_pre_message_for_small_attachment() -> Result<()> { let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; let chat = alice.create_chat(&bob).await; let mut msg = Message::new(Viewtype::File); - msg.set_file_from_bytes(&alice.ctx, "test.bin", &[0u8; 100_000], None)?; + msg.set_file_from_bytes(alice, "test.bin", &[0u8; 100_000], None)?; msg.set_text("test".to_owned()); // assert that test attachment is smaller than limit - assert!(msg.get_filebytes(&alice.ctx).await?.unwrap() < PRE_MSG_ATTACHMENT_SIZE_THRESHOLD); + assert!(msg.get_filebytes(alice).await?.unwrap() < PRE_MSG_ATTACHMENT_SIZE_THRESHOLD); - let msg_id = chat::send_msg(&alice.ctx, chat.id, &mut msg).await.unwrap(); + let msg_id = chat::send_msg(alice, chat.id, &mut msg).await.unwrap(); let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; // only one message and no "is full message" header should be present @@ -604,7 +604,7 @@ mod tests { .is_none(), "no 'Chat-Full-Message-ID'-header should be present in clear text headers" ); - let decrypted_message = MimeMessage::from_bytes(&bob.ctx, mime.as_bytes()).await?; + let decrypted_message = MimeMessage::from_bytes(bob, mime.as_bytes()).await?; assert!( !decrypted_message.header_exists(HeaderDef::ChatFullMessageId), "no 'Chat-Full-Message-ID'-header should be present" From 3a36854687b637f9f78a9f69d904a4fb6bc58747 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Mon, 24 Nov 2025 03:07:34 +0100 Subject: [PATCH 35/44] `assert!(!` to `assert_eq!(,false` for "easier" code reading --- src/download.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/download.rs b/src/download.rs index 875ba1dadb..f8167a776e 100644 --- a/src/download.rs +++ b/src/download.rs @@ -383,8 +383,11 @@ mod tests { ); let decrypted_full_message = MimeMessage::from_bytes(bob, full_message_bytes).await?; - assert!(!decrypted_full_message.decrypting_failed); - assert!(!decrypted_full_message.header_exists(HeaderDef::ChatFullMessageId)); + assert_eq!(decrypted_full_message.decrypting_failed, false); + assert_eq!( + decrypted_full_message.header_exists(HeaderDef::ChatFullMessageId), + false + ); let decrypted_pre_message = MimeMessage::from_bytes(bob, pre_message_bytes).await?; assert_eq!( From 5ec079c5757a162eb6e4bbb65a39ed64d3a49bc3 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Mon, 24 Nov 2025 03:12:27 +0100 Subject: [PATCH 36/44] test: simplify chat creation by using `.create_chat_with_contact` --- src/download.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/download.rs b/src/download.rs index f8167a776e..26cbda025e 100644 --- a/src/download.rs +++ b/src/download.rs @@ -477,14 +477,15 @@ mod tests { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; - let contact_id = Contact::create(alice, "example", "email@example.org").await?; - let chat_id = ChatId::create_for_contact(alice, contact_id).await?; + let chat = alice + .create_chat_with_contact("example", "email@example.org") + .await; let mut msg = Message::new(Viewtype::File); msg.set_file_from_bytes(alice, "test.bin", &[0u8; 300_000], None)?; msg.set_text("test".to_owned()); - let msg_id = chat::send_msg(alice, chat_id, &mut msg).await?; + let msg_id = chat::send_msg(alice, chat.id, &mut msg).await?; let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; assert_eq!(smtp_rows.len(), 1); From d5f88194ebbf2fe7661295edc0d8057ebbf2da8b Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Mon, 24 Nov 2025 02:22:10 +0000 Subject: [PATCH 37/44] Apply suggestions from code review Co-authored-by: Hocuri --- src/headerdef.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/headerdef.rs b/src/headerdef.rs index 23498244f5..e2ac8c72e1 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -102,10 +102,14 @@ pub enum HeaderDef { /// used to encrypt and decrypt messages. /// This secret is sent to a new member in the member-addition message. ChatBroadcastSecret, - /// This message announces a bigger message with attachment that is refereced by rfc724_mid. + /// A message with a large attachment is split into two MIME messages: + /// A pre-message, which contains everything but the attachment, + /// and a full-message. + /// The pre-message gets a `Chat-Full-Message-Id` header + /// referencing the full-message's rfc724_mid. ChatFullMessageId, - /// This message has a pre-message + /// This message is preceded by a pre-message /// and thus this message can be skipped while fetching messages. /// This is a cleartext / unproteced header. ChatIsFullMessage, From 3cc7a56de1f292f90e72aa6d840b078da0d10ddb Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Mon, 24 Nov 2025 03:24:54 +0100 Subject: [PATCH 38/44] remove now unessesary test --- src/headerdef.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/headerdef.rs b/src/headerdef.rs index e2ac8c72e1..510c2d9e29 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -207,11 +207,5 @@ mod tests { Some("Bob".to_string()) ); assert_eq!(headers.get_header_value(HeaderDef::Autocrypt), None); - - let (headers, _) = mailparse::parse_headers(b"chat-is-FuLL-MeSSage: 1").unwrap(); - assert_eq!( - headers.get_header_value(HeaderDef::ChatIsFullMessage), - Some("1".to_string()) - ); } } From 2ef8b6f7bdb84fd2e15cb61eba9cd5efcb0435b6 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Mon, 24 Nov 2025 03:32:12 +0100 Subject: [PATCH 39/44] sql style --- src/chat.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 5efd18519a..280af205da 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2922,7 +2922,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - // send pre-message before actual message if let Some(pre_msg) = &rendered_pre_msg { let row_id = t.execute( - "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \ + "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) VALUES (?1, ?2, ?3, ?4)", ( &pre_msg.rfc724_mid, @@ -2934,7 +2934,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - row_ids.push(row_id.try_into()?); } let row_id = t.execute( - "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) \ + "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) VALUES (?1, ?2, ?3, ?4)", ( &rendered_msg.rfc724_mid, From 413a6ca4893c329140b2f5bdf3c96392cb6317a4 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Mon, 24 Nov 2025 03:32:38 +0100 Subject: [PATCH 40/44] rm unused imports --- src/download.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/download.rs b/src/download.rs index 26cbda025e..1b4d317833 100644 --- a/src/download.rs +++ b/src/download.rs @@ -214,9 +214,8 @@ mod tests { use tokio::fs; use super::*; - use crate::chat::{self, ChatId, create_group, send_msg}; + use crate::chat::{self, create_group, send_msg}; use crate::config::Config; - use crate::contact::Contact; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::message::Viewtype; use crate::mimeparser::MimeMessage; From 9d3a506d0b4266b0859b9b6afdf41cb8c6c853da Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Mon, 24 Nov 2025 03:36:03 +0100 Subject: [PATCH 41/44] tests: `.headers` instead of `.get_headers()` --- src/download.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/download.rs b/src/download.rs index 1b4d317833..8640700c4e 100644 --- a/src/download.rs +++ b/src/download.rs @@ -358,13 +358,13 @@ mod tests { assert!( pre_message - .get_headers() + .headers .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) .is_none() ); assert!( full_message - .get_headers() + .headers .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) .is_some() ); @@ -461,7 +461,7 @@ mod tests { let decrypted_full_message = MimeMessage::from_bytes(bob, full_message_bytes).await?; assert!( full_message - .get_headers() + .headers .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) .is_some(), "tested message is not a full-message, sending order may be broken" @@ -496,7 +496,7 @@ mod tests { let message = mailparse::parse_mail(message_bytes)?; assert!( message - .get_headers() + .headers .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) .is_none(), ); @@ -523,13 +523,13 @@ mod tests { let mail = mailparse::parse_mail(mime.as_bytes())?; assert!( - mail.get_headers() + mail.headers .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) .is_none(), "no 'Chat-Is-Full-Message'-header should be present" ); assert!( - mail.get_headers() + mail.headers .get_first_header(HeaderDef::ChatFullMessageId.get_headername()) .is_none(), "no 'Chat-Full-Message-ID'-header should be present in clear text headers" @@ -554,12 +554,12 @@ mod tests { let mail = mailparse::parse_mail(mime.as_bytes())?; assert!( - mail.get_headers() + mail.headers .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) .is_none() ); assert!( - mail.get_headers() + mail.headers .get_first_header(HeaderDef::ChatFullMessageId.get_headername()) .is_none(), "no 'Chat-Full-Message-ID'-header should be present in clear text headers" @@ -597,12 +597,12 @@ mod tests { let mail = mailparse::parse_mail(mime.as_bytes())?; assert!( - mail.get_headers() + mail.headers .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) .is_none() ); assert!( - mail.get_headers() + mail.headers .get_first_header(HeaderDef::ChatFullMessageId.get_headername()) .is_none(), "no 'Chat-Full-Message-ID'-header should be present in clear text headers" From 1dcd328b0e3f74178f34d05b988471137f8e203e Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Wed, 26 Nov 2025 14:51:06 +0100 Subject: [PATCH 42/44] get_smtp_rows_for_msg now returns SentMessage instead of tuple --- src/download.rs | 28 ++++++++++++++++++++-------- src/test_utils.rs | 14 +++++++++++--- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/download.rs b/src/download.rs index 8640700c4e..1646e16816 100644 --- a/src/download.rs +++ b/src/download.rs @@ -346,13 +346,13 @@ mod tests { let pre_message_bytes = smtp_rows .first() .expect("first element exists") - .2 + .payload .as_bytes(); let pre_message = mailparse::parse_mail(pre_message_bytes)?; let full_message_bytes = smtp_rows .get(1) .expect("second element exists") - .2 + .payload .as_bytes(); let full_message = mailparse::parse_mail(full_message_bytes)?; @@ -439,12 +439,12 @@ mod tests { let pre_message_bytes = smtp_rows .first() .expect("first element exists") - .2 + .payload .as_bytes(); let full_message_bytes = smtp_rows .get(1) .expect("second element exists") - .2 + .payload .as_bytes(); let full_message = mailparse::parse_mail(full_message_bytes)?; @@ -491,7 +491,7 @@ mod tests { let message_bytes = smtp_rows .first() .expect("first element exists") - .2 + .payload .as_bytes(); let message = mailparse::parse_mail(message_bytes)?; assert!( @@ -519,7 +519,11 @@ mod tests { assert_eq!(smtp_rows.len(), 1, "only one message should be sent"); - let mime = smtp_rows.first().expect("first element exists").2.clone(); + let mime = smtp_rows + .first() + .expect("first element exists") + .payload + .clone(); let mail = mailparse::parse_mail(mime.as_bytes())?; assert!( @@ -550,7 +554,11 @@ mod tests { assert_eq!(smtp_rows.len(), 1, "only one message should be sent"); - let mime = smtp_rows.first().expect("first element exists").2.clone(); + let mime = smtp_rows + .first() + .expect("first element exists") + .payload + .clone(); let mail = mailparse::parse_mail(mime.as_bytes())?; assert!( @@ -593,7 +601,11 @@ mod tests { // only one message and no "is full message" header should be present assert_eq!(smtp_rows.len(), 1); - let mime = smtp_rows.first().expect("first element exists").2.clone(); + let mime = smtp_rows + .first() + .expect("first element exists") + .payload + .clone(); let mail = mailparse::parse_mail(mime.as_bytes())?; assert!( diff --git a/src/test_utils.rs b/src/test_utils.rs index 199d45aff9..f397ad1d5a 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -697,22 +697,30 @@ impl TestContext { }) } - pub async fn get_smtp_rows_for_msg(&self, msg_id: MsgId) -> Vec<(i64, MsgId, String, String)> { + pub async fn get_smtp_rows_for_msg<'a>(&'a self, msg_id: MsgId) -> Vec> { self.ctx .sql .query_map_vec( "SELECT id, msg_id, mime, recipients FROM smtp WHERE msg_id=?", (msg_id,), |row| { - let rowid: i64 = row.get(0)?; + let _id: MsgId = row.get(0)?; let msg_id: MsgId = row.get(1)?; let mime: String = row.get(2)?; let recipients: String = row.get(3)?; - Ok((rowid, msg_id, mime, recipients)) + Ok((msg_id, mime, recipients)) }, ) .await .unwrap() + .into_iter() + .map(|(msg_id, mime, recipients)| SentMessage { + payload: mime, + sender_msg_id: msg_id, + sender_context: &self.ctx, + recipients, + }) + .collect() } /// Retrieves a sent sync message from the db. From 8737c29c57b981fcc029fca9590911a252d84783 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Wed, 26 Nov 2025 15:02:02 +0100 Subject: [PATCH 43/44] use `bob.parse_msg(msg)` instead of using mimeparser directly --- src/download.rs | 95 +++++++++++++++++++------------------------------ 1 file changed, 37 insertions(+), 58 deletions(-) diff --git a/src/download.rs b/src/download.rs index 1646e16816..fa075886ef 100644 --- a/src/download.rs +++ b/src/download.rs @@ -218,7 +218,6 @@ mod tests { use crate::config::Config; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::message::Viewtype; - use crate::mimeparser::MimeMessage; use crate::receive_imf::receive_imf_from_inbox; use crate::test_utils::{self, TestContext, TestContextManager}; @@ -343,60 +342,60 @@ mod tests { // pre-message and full message should be present // and test that correct headers are present on both messages assert_eq!(smtp_rows.len(), 2); - let pre_message_bytes = smtp_rows - .first() - .expect("first element exists") - .payload - .as_bytes(); - let pre_message = mailparse::parse_mail(pre_message_bytes)?; - let full_message_bytes = smtp_rows - .get(1) - .expect("second element exists") - .payload - .as_bytes(); - let full_message = mailparse::parse_mail(full_message_bytes)?; + let pre_message = smtp_rows.first().expect("first element exists"); + let pre_message_parsed = mailparse::parse_mail(pre_message.payload.as_bytes())?; + let full_message = smtp_rows.get(1).expect("second element exists"); + let full_message_parsed = mailparse::parse_mail(full_message.payload.as_bytes())?; assert!( - pre_message + pre_message_parsed .headers .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) .is_none() ); assert!( - full_message + full_message_parsed .headers .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) .is_some() ); assert_eq!( - full_message.headers.get_header_value(HeaderDef::MessageId), + full_message_parsed + .headers + .get_header_value(HeaderDef::MessageId), Some(format!("<{}>", msg.rfc724_mid)), "full message should have the rfc message id of the database message" ); assert_ne!( - pre_message.headers.get_header_value(HeaderDef::MessageId), - full_message.headers.get_header_value(HeaderDef::MessageId), + pre_message_parsed + .headers + .get_header_value(HeaderDef::MessageId), + full_message_parsed + .headers + .get_header_value(HeaderDef::MessageId), "message ids of pre message and full message should be different" ); - let decrypted_full_message = MimeMessage::from_bytes(bob, full_message_bytes).await?; + let decrypted_full_message = bob.parse_msg(full_message).await; assert_eq!(decrypted_full_message.decrypting_failed, false); assert_eq!( decrypted_full_message.header_exists(HeaderDef::ChatFullMessageId), false ); - let decrypted_pre_message = MimeMessage::from_bytes(bob, pre_message_bytes).await?; + let decrypted_pre_message = bob.parse_msg(pre_message).await; assert_eq!( decrypted_pre_message .get_header(HeaderDef::ChatFullMessageId) .map(String::from), - full_message.headers.get_header_value(HeaderDef::MessageId) + full_message_parsed + .headers + .get_header_value(HeaderDef::MessageId) ); assert!( - pre_message + pre_message_parsed .headers .get_header_value(HeaderDef::ChatFullMessageId) .is_none(), @@ -436,19 +435,11 @@ mod tests { let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; assert_eq!(smtp_rows.len(), 2); - let pre_message_bytes = smtp_rows - .first() - .expect("first element exists") - .payload - .as_bytes(); - let full_message_bytes = smtp_rows - .get(1) - .expect("second element exists") - .payload - .as_bytes(); - let full_message = mailparse::parse_mail(full_message_bytes)?; + let pre_message = smtp_rows.first().expect("first element exists"); + let full_message = smtp_rows.get(1).expect("second element exists"); + let full_message_parsed = mailparse::parse_mail(full_message.payload.as_bytes())?; - let decrypted_pre_message = MimeMessage::from_bytes(bob, pre_message_bytes).await?; + let decrypted_pre_message = bob.parse_msg(pre_message).await; assert!( decrypted_pre_message .get_header(HeaderDef::ChatFullMessageId) @@ -458,9 +449,9 @@ mod tests { assert_ne!(decrypted_pre_message.gossiped_keys.len(), 0); assert_ne!(decrypted_pre_message.user_avatar, None); - let decrypted_full_message = MimeMessage::from_bytes(bob, full_message_bytes).await?; + let decrypted_full_message = bob.parse_msg(full_message).await; assert!( - full_message + full_message_parsed .headers .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) .is_some(), @@ -519,12 +510,8 @@ mod tests { assert_eq!(smtp_rows.len(), 1, "only one message should be sent"); - let mime = smtp_rows - .first() - .expect("first element exists") - .payload - .clone(); - let mail = mailparse::parse_mail(mime.as_bytes())?; + let msg = smtp_rows.first().expect("first element exists"); + let mail = mailparse::parse_mail(msg.payload.as_bytes())?; assert!( mail.headers @@ -538,7 +525,7 @@ mod tests { .is_none(), "no 'Chat-Full-Message-ID'-header should be present in clear text headers" ); - let decrypted_message = MimeMessage::from_bytes(bob, mime.as_bytes()).await?; + let decrypted_message = bob.parse_msg(msg).await; assert!( !decrypted_message.header_exists(HeaderDef::ChatFullMessageId), "no 'Chat-Full-Message-ID'-header should be present" @@ -554,12 +541,8 @@ mod tests { assert_eq!(smtp_rows.len(), 1, "only one message should be sent"); - let mime = smtp_rows - .first() - .expect("first element exists") - .payload - .clone(); - let mail = mailparse::parse_mail(mime.as_bytes())?; + let msg = smtp_rows.first().expect("first element exists"); + let mail = mailparse::parse_mail(msg.payload.as_bytes())?; assert!( mail.headers @@ -572,7 +555,7 @@ mod tests { .is_none(), "no 'Chat-Full-Message-ID'-header should be present in clear text headers" ); - let decrypted_message = MimeMessage::from_bytes(bob, mime.as_bytes()).await?; + let decrypted_message = bob.parse_msg(msg).await; assert!( !decrypted_message.header_exists(HeaderDef::ChatFullMessageId), "no 'Chat-Full-Message-ID'-header should be present" @@ -586,7 +569,7 @@ mod tests { let mut tcm = TestContextManager::new(); let alice = &tcm.alice().await; let bob = &tcm.bob().await; - let chat = alice.create_chat(&bob).await; + let chat = alice.create_chat(bob).await; let mut msg = Message::new(Viewtype::File); msg.set_file_from_bytes(alice, "test.bin", &[0u8; 100_000], None)?; @@ -601,12 +584,8 @@ mod tests { // only one message and no "is full message" header should be present assert_eq!(smtp_rows.len(), 1); - let mime = smtp_rows - .first() - .expect("first element exists") - .payload - .clone(); - let mail = mailparse::parse_mail(mime.as_bytes())?; + let msg = smtp_rows.first().expect("first element exists"); + let mail = mailparse::parse_mail(msg.payload.as_bytes())?; assert!( mail.headers @@ -619,7 +598,7 @@ mod tests { .is_none(), "no 'Chat-Full-Message-ID'-header should be present in clear text headers" ); - let decrypted_message = MimeMessage::from_bytes(bob, mime.as_bytes()).await?; + let decrypted_message = bob.parse_msg(msg).await; assert!( !decrypted_message.header_exists(HeaderDef::ChatFullMessageId), "no 'Chat-Full-Message-ID'-header should be present" From 381b70917ebeb2000b3b32d6f090668265fea425 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Wed, 26 Nov 2025 14:03:10 +0000 Subject: [PATCH 44/44] Update src/mimefactory.rs Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com> --- src/mimefactory.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 10cf372b74..cf71bb329c 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1144,10 +1144,7 @@ impl MimeFactory { for (addr, key) in &encryption_pubkeys { let fingerprint = key.dc_fingerprint().hex(); let cmd = msg.param.get_cmd(); - let is_full_msg = - self.pre_message_mode == Some(PreMessageMode::FullMessage); - - if is_full_msg { + if self.pre_message_mode == Some(PreMessageMode::FullMessage) { continue; }