From 146c7d3c1307128ac883f4d0bf63ed110b2bfd71 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 25 Aug 2026 19:05:28 +0000 Subject: [PATCH 1/5] Encryption.is_encrypted() --- src/mimefactory.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index eac134f809..478daa9a1a 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -85,6 +85,16 @@ enum Encryption { Symmetric { shared_secret: String }, } +impl Encryption { + pub(crate) fn is_encrypted(&self) -> bool { + match self { + Self::No => false, + Self::Asymmetric { .. } => true, + Self::Symmetric { .. } => true, + } + } +} + /// Helper to construct mime messages. #[derive(Debug, Clone)] pub struct MimeFactory { @@ -270,7 +280,7 @@ pub(crate) fn render_queued_mail( let mut inner_headers: Vec = Vec::new(); let mut outer_headers: Vec = Vec::new(); - let is_encrypted = !matches!(encryption, Encryption::No); + let is_encrypted = encryption.is_encrypted(); fn add_header( name: &[u8], @@ -843,8 +853,8 @@ impl MimeFactory { // We don't display avatars for address-contacts, so sending avatars w/o encryption is not // useful and causes e.g. Outlook to reject a message with a big header, see // https://support.delta.chat/t/invalid-mime-content-single-text-value-size-32822-exceeded-allowed-maximum-32768-for-the-chat-user-avatar-header/4067. - let attach_selfavatar = Self::should_attach_selfavatar(context, &msg).await - && !matches!(encryption, Encryption::No); + let attach_selfavatar = + Self::should_attach_selfavatar(context, &msg).await && encryption.is_encrypted(); ensure_and_debug_assert!( member_timestamps.is_empty() From 2f5844ea47ddb95d29ec707177c7a40a7cafb47a Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 21 Aug 2026 19:22:50 +0000 Subject: [PATCH 2/5] test: cleanup get_smtp_rows_for_msg() It was incorrectly converting unused row ID to MsgId type and selecting already known msg_id. --- src/test_utils.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/test_utils.rs b/src/test_utils.rs index 810217fcc3..64b15a094d 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -679,20 +679,18 @@ ORDER BY id" .ctx .sql .query_map_vec( - "SELECT id, msg_id, mime, recipients FROM smtp WHERE msg_id=?", + "SELECT mime, recipients FROM smtp WHERE msg_id=?", (msg_id,), |row| { - 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((msg_id, mime, recipients)) + let mime: String = row.get(0)?; + let recipients: String = row.get(1)?; + Ok((mime, recipients)) }, ) .await .unwrap() .into_iter() - .map(|(msg_id, mime, recipients)| SentMessage { + .map(|(mime, recipients)| SentMessage { payload: mime, sender_msg_id: msg_id, sender_context: &self.ctx, From c1e39394a78d432af89676ff040b3ae90f9a6d06 Mon Sep 17 00:00:00 2001 From: link2xt Date: Fri, 14 Aug 2026 00:50:59 +0000 Subject: [PATCH 3/5] feat: late encryption --- docs/schema.sql | 55 +++++- src/chat.rs | 268 +++++++++++++++------------ src/config.rs | 7 - src/download.rs | 3 - src/ephemeral/ephemeral_tests.rs | 2 +- src/message.rs | 2 +- src/message/message_tests.rs | 2 +- src/mimefactory.rs | 68 +++---- src/mimefactory/mimefactory_tests.rs | 4 +- src/receive_imf.rs | 2 +- src/receive_imf/receive_imf_tests.rs | 37 +++- src/securejoin.rs | 38 ++-- src/securejoin/bob.rs | 22 ++- src/smtp.rs | 150 +++++++++++++-- src/sql/migrations.rs | 24 +++ src/test_utils.rs | 95 ++++++++-- src/tests/pre_messages/sending.rs | 4 +- 17 files changed, 521 insertions(+), 262 deletions(-) diff --git a/docs/schema.sql b/docs/schema.sql index dbe487adeb..32940baf48 100644 --- a/docs/schema.sql +++ b/docs/schema.sql @@ -400,14 +400,43 @@ CREATE TABLE bobstate ( chat_id INTEGER NOT NULL ); -CREATE TABLE smtp ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - rfc724_mid TEXT NOT NULL, -- Message-ID - mime TEXT NOT NULL, -- SMTP payload - msg_id INTEGER NOT NULL, -- ID of the message in `msgs` table - recipients TEXT NOT NULL, -- List of recipients separated by space - retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message -); +CREATE TABLE smtp2 ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + display_name TEXT NOT NULL, -- Display name to put into the From field. + rfc724_mid TEXT NOT NULL, -- Message-ID + + -- Unencrypted payload with some headers. + mime BLOB NOT NULL, + + -- True if Autocrypt header should be added before sending. + should_attach_pubkey INTEGER NOT NULL, + + -- True if OpenPGP-encrypted message may use compression. + may_compress INTEGER NOT NULL, + + -- True if encrypted message should be signed as well. + should_sign INTEGER NOT NULL, + + -- ID of the message in `msgs` table + msg_id INTEGER NOT NULL, + + -- List of recipients separated by space + recipients TEXT NOT NULL, + + -- True if the message is encrypted. + -- If true, exactly one of the shared_secret or encryption_fingerprints should be non-empty. + -- If false, both must be empty. + is_encrypted INTEGER NOT NULL, + + -- Shared secret if the message is to be encrypted symmetrically. + shared_secret TEXT NOT NULL DEFAULT '', + + -- Pairs of addresses and fingerprints of the key the message should be encrypted to. + -- Stored as a JSON. + encryption_fingerprints TEXT NOT NULL DEFAULT '', + + retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message +) STRICT; CREATE TABLE smtp_mdns ( msg_id INTEGER NOT NULL, -- id of the message in msgs table which requested MDN (DEPRECATED 2024-06-21) @@ -784,3 +813,13 @@ CREATE TABLE sending_domains( domain TEXT PRIMARY KEY, dkim_works INTEGER DEFAULT 0 ); + +-- Replaced with smtp2. +CREATE TABLE smtp ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + rfc724_mid TEXT NOT NULL, -- Message-ID + mime TEXT NOT NULL, -- SMTP payload + msg_id INTEGER NOT NULL, -- ID of the message in `msgs` table + recipients TEXT NOT NULL, -- List of recipients separated by space + retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message +); diff --git a/src/chat.rs b/src/chat.rs index aa92a88010..4f616bcf47 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -11,7 +11,6 @@ use std::time::Duration; use anyhow::{Context as _, Result, anyhow, bail, ensure}; use chrono::TimeZone; use deltachat_contact_tools::{ContactAddress, sanitize_bidi_characters, sanitize_single_line}; -use humansize::{BINARY, format_size}; use mail_builder::mime::MimePart; use serde::{Deserialize, Serialize}; use strum_macros::EnumIter; @@ -27,20 +26,17 @@ 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, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD, PRE_MSG_SIZE_WARNING_THRESHOLD, -}; +use crate::download::{DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD}; use crate::ensure_and_debug_assert_eq; use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers}; use crate::events::EventType; -use crate::key; -use crate::key::{Fingerprint, self_fingerprint}; +use crate::key::{DcKey as _, Fingerprint, self_fingerprint}; use crate::location; use crate::log::{LogExt, warn}; use crate::logged_debug_assert; use crate::message::{self, Message, MessageState, MsgId, Viewtype}; use crate::mimefactory; -use crate::mimefactory::{MimeFactory, RenderedEmail}; +use crate::mimefactory::{MimeFactory, QueuedMail, RenderSideEffects}; use crate::mimeparser::SystemMessage; use crate::param::{Param, Params}; use crate::pgp::addresses_from_public_key; @@ -2776,11 +2772,10 @@ async fn render_mime_message_and_pre_message( context: &Context, msg: &mut Message, mimefactory: MimeFactory, -) -> Result<(Option, RenderedEmail)> { - let from_addr = context.get_primary_self_addr().await?; - let public_key = key::load_self_public_key(context).await?; - let secret_key = key::load_self_secret_key(context).await?; - +) -> Result<( + Option<(QueuedMail, RenderSideEffects)>, + (QueuedMail, RenderSideEffects), +)> { let needs_pre_message = msg.viewtype.has_file() && mimefactory.will_be_encrypted() // unencrypted is likely email, we don't want to spam by sending multiple messages && msg @@ -2801,50 +2796,117 @@ async fn render_mime_message_and_pre_message( .await .context("Failed to render post-message")?; - let rendered_msg = mimefactory::render_queued_mail( - queued_msg, - &public_key, - &secret_key, - from_addr.clone(), - side_effects, - )?; - let mut mimefactory_pre_msg = mimefactory; - mimefactory_pre_msg.set_as_pre_message_for(&rendered_msg); + mimefactory_pre_msg.set_as_pre_message_for(&queued_msg.rfc724_mid); let (queued_pre_msg, pre_side_effects) = Box::pin(mimefactory_pre_msg.into_queued_mail(context)) .await .context("pre-message failed to render")?; - let rendered_pre_msg = mimefactory::render_queued_mail( - queued_pre_msg, - &public_key, - &secret_key, - from_addr, - pre_side_effects, - )?; - if rendered_pre_msg.message.len() > PRE_MSG_SIZE_WARNING_THRESHOLD { - warn!( - context, - "Pre-message for message {} is larger than expected: {}.", - msg.id, - rendered_pre_msg.message.len() - ); - } - - Ok((Some(rendered_pre_msg), rendered_msg)) + Ok(( + Some((queued_pre_msg, pre_side_effects)), + (queued_msg, side_effects), + )) } else { let (queued_msg, side_effects) = Box::pin(mimefactory.into_queued_mail(context)).await?; - let rendered_msg = mimefactory::render_queued_mail( - queued_msg, - &public_key, - &secret_key, - from_addr, - side_effects, - )?; - Ok((None, rendered_msg)) + Ok((None, (queued_msg, side_effects))) + } +} + +/// Process side effects and store queued mail. +/// +/// TODO: convert to sync and do everything in a single transaction +pub(crate) async fn enqueue_mail( + context: &Context, + now: i64, + msg_id: MsgId, + // TODO: chat ID is only used with side effects, make it optional + chat_id: ChatId, + queued_mail: &QueuedMail, + side_effects: &RenderSideEffects, + recipients: &[String], +) -> Result { + if let Some(last_added_location_timestamp) = side_effects.last_added_location_timestamp { + location::set_kml_sent_timestamp(context, chat_id, last_added_location_timestamp).await?; + } + + if side_effects.avatar_is_attached { + chat_id + .set_selfavatar_timestamp(context, now) + .await + .context("Failed to set selfavatar timestamp")?; + } + + if let Some(ref sync_ids) = side_effects.sync_ids_to_delete { + context + .sql + .execute( + &format!("DELETE FROM multi_device_sync WHERE id IN ({sync_ids})"), + (), + ) + .await?; } + + // Store mail into queue. + let all_recipients = recipients.join(" "); + let is_encrypted = queued_mail.encryption.is_encrypted(); + + let row_id = context + .sql + .insert( + " +INSERT INTO smtp2 ( + display_name, + rfc724_mid, + mime, + should_attach_pubkey, + may_compress, + should_sign, + msg_id, + recipients, + is_encrypted, + shared_secret, + encryption_fingerprints +) +VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? +) +", + ( + &queued_mail.display_name, + &queued_mail.rfc724_mid, + &queued_mail.raw_message, + queued_mail.should_attach_pubkey, + queued_mail.may_compress, + queued_mail.should_sign, + msg_id, + &all_recipients, + is_encrypted, + if let mimefactory::Encryption::Symmetric { ref shared_secret } = + queued_mail.encryption + { + shared_secret + } else { + "" + }, + if let mimefactory::Encryption::Asymmetric { + ref encryption_pubkeys, + } = queued_mail.encryption + { + let res: Vec<(String, String)> = encryption_pubkeys + .iter() + .map(|(addr, pubkey)| (addr.clone(), pubkey.dc_fingerprint().hex())) + .collect(); + serde_json::to_string(&res)? + } else { + "".to_string() + }, + ), + ) + .await + .context("Failed to insert a row into smtp2 table")?; + Ok(row_id) } /// Constructs jobs for sending a message and inserts them into the `smtp` table. @@ -2857,6 +2919,8 @@ async fn render_mime_message_and_pre_message( /// /// The caller has to interrupt SMTP loop or otherwise process new rows. pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result> { + let now = time(); + let cmd = msg.param.get_cmd(); if cmd == SystemMessage::GroupNameChanged || cmd == SystemMessage::GroupDescriptionChanged { msg.chat_id @@ -2906,7 +2970,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - return Ok(Vec::new()); } - let (rendered_pre_msg, rendered_msg) = + let (queued_pre_msg_pair, queued_msg_pair) = match render_mime_message_and_pre_message(context, msg, mimefactory).await { Ok(res) => Ok(res), Err(err) => { @@ -2915,29 +2979,17 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - } }?; - if let (post_msg, Some(pre_msg)) = (&rendered_msg, &rendered_pre_msg) { - info!( - context, - "Message {} sizes: pre-message: {}; post-message: {}.", - msg.id, - format_size(pre_msg.message.len(), BINARY), - format_size(post_msg.message.len(), BINARY), - ); + if let Some((pre_msg, _)) = &queued_pre_msg_pair { msg.pre_rfc724_mid = pre_msg.rfc724_mid.clone(); - } else { - info!( - context, - "Message {} will be sent in one shot (no pre- and post-message). Size: {}.", - msg.id, - format_size(rendered_msg.message.len(), BINARY), - ); } + let (queued_msg, side_effects) = queued_msg_pair; + let is_encrypted = queued_msg.encryption.is_encrypted(); if context.get_config_bool(Config::BccSelf).await? { - smtp::add_self_recipients(context, &mut recipients, rendered_msg.is_encrypted).await?; + smtp::add_self_recipients(context, &mut recipients, is_encrypted).await?; } - if needs_encryption && !rendered_msg.is_encrypted { + if needs_encryption && !is_encrypted { let addr = context.get_config(Config::ConfiguredAddr).await?; let text = stock_str::unencrypted_email( context, @@ -2967,32 +3019,13 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - ); } - let now = time(); - - if let Some(last_added_location_timestamp) = - rendered_msg.side_effects.last_added_location_timestamp - { - location::set_kml_sent_timestamp(context, msg.chat_id, last_added_location_timestamp) - .await?; - } - - if rendered_msg.side_effects.avatar_is_attached - || rendered_pre_msg - .as_ref() - .is_some_and(|msg| msg.side_effects.avatar_is_attached) - { - msg.chat_id - .set_selfavatar_timestamp(context, now) - .await - .context("Failed to set selfavatar timestamp")?; - } - - if rendered_msg.is_encrypted { + msg.subject.clone_from(&side_effects.subject); + if is_encrypted { msg.param.set_int(Param::GuaranteeE2ee, 1); } else { msg.param.remove(Param::GuaranteeE2ee); } - msg.subject.clone_from(&rendered_msg.side_effects.subject); + // Sort the message to the bottom. Employ `msgs_index7` to compute `timestamp`. context .sql @@ -3021,41 +3054,36 @@ WHERE id=? ) .await?; - let trans_fn = |t: &mut rusqlite::Transaction| { - let mut row_ids = Vec::::new(); + let mut row_ids = Vec::new(); + if let Some((queued_pre_msg, pre_side_effects)) = queued_pre_msg_pair { + let row_id = enqueue_mail( + context, + now, + msg.id, + msg.chat_id, + &queued_pre_msg, + &pre_side_effects, + &recipients, + ) + .await + .context("Failed to enqueue pre-message")?; + row_ids.push(row_id) + } + row_ids.push( + enqueue_mail( + context, + now, + msg.id, + msg.chat_id, + &queued_msg, + &side_effects, + &recipients, + ) + .await + .context("Failed to enqueue message")?, + ); - if let Some(sync_ids) = rendered_msg.side_effects.sync_ids_to_delete { - t.execute( - &format!("DELETE FROM multi_device_sync WHERE id IN ({sync_ids})"), - (), - )?; - } - if !recipients.is_empty() { - let mut stmt = t.prepare( - "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) - VALUES (?1, ?2, ?3, ?4)", - )?; - let all_recipients = recipients.join(" "); - if let Some(pre_msg) = &rendered_pre_msg { - let row_id = stmt.insert(( - &pre_msg.rfc724_mid, - &all_recipients, - &pre_msg.message, - msg.id, - ))?; - row_ids.push(row_id); - } - let row_id = stmt.insert(( - &rendered_msg.rfc724_mid, - &all_recipients, - &rendered_msg.message, - msg.id, - ))?; - row_ids.push(row_id); - } - Ok(row_ids) - }; - context.sql.transaction(trans_fn).await + Ok(row_ids) } /// Sends a text message to the given chat. diff --git a/src/config.rs b/src/config.rs index 8cf59b025a..c308af3172 100644 --- a/src/config.rs +++ b/src/config.rs @@ -812,13 +812,6 @@ impl Context { "Failed to update add_timestamp for the new primary transport", )?; - // Clean up SMTP queue. - // - // The messages in the queue have a different - // From address so we cannot send them over - // the new SMTP transport. - transaction.execute("DELETE FROM smtp", ())?; - Ok(()) }) .await?; diff --git a/src/download.rs b/src/download.rs index e1bfd86c9f..7cdd3a9950 100644 --- a/src/download.rs +++ b/src/download.rs @@ -23,9 +23,6 @@ pub(crate) use post_msg_metadata::PostMsgMetadata; /// KiB). pub(crate) const PRE_MSG_ATTACHMENT_SIZE_THRESHOLD: u64 = 140_000; -/// Max size for pre messages. A warning is emitted when this is exceeded. -pub(crate) const PRE_MSG_SIZE_WARNING_THRESHOLD: usize = 150_000; - /// Download state of the message. #[derive( Debug, diff --git a/src/ephemeral/ephemeral_tests.rs b/src/ephemeral/ephemeral_tests.rs index 81e003612a..110dd0aca8 100644 --- a/src/ephemeral/ephemeral_tests.rs +++ b/src/ephemeral/ephemeral_tests.rs @@ -676,7 +676,7 @@ async fn test_ephemeral_msg_offline() -> Result<()> { .await?; let mut msg = Message::new_text("hi".to_string()); assert!(chat::send_msg_sync(alice, chat.id, &mut msg).await.is_err()); - let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?"; + let stmt = "SELECT COUNT(*) FROM smtp2 WHERE msg_id=?"; assert!(alice.sql.exists(stmt, (msg.id,)).await?); let now = time(); check_msg_will_be_deleted(alice, msg.id, &chat, now, now + i64::from(duration) + 1).await?; diff --git a/src/message.rs b/src/message.rs index f6def15ada..abb39aaa0b 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1697,7 +1697,7 @@ pub async fn delete_msgs_ext( if !msg.pre_rfc724_mid.is_empty() { stmt.execute((&msg.pre_rfc724_mid,))?; } - trans.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?; + trans.execute("DELETE FROM smtp2 WHERE msg_id=?", (msg_id,))?; trans.execute( "DELETE FROM download WHERE rfc724_mid=?", (&msg.rfc724_mid,), diff --git a/src/message/message_tests.rs b/src/message/message_tests.rs index e34cf8c2f7..fb81760448 100644 --- a/src/message/message_tests.rs +++ b/src/message/message_tests.rs @@ -626,7 +626,7 @@ async fn test_delete_msgs_offline() -> Result<()> { let chat_id = alice.create_chat_id(bob).await; let mut msg = Message::new_text("hi".to_string()); assert!(chat::send_msg_sync(alice, chat_id, &mut msg).await.is_err()); - let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?"; + let stmt = "SELECT COUNT(*) FROM smtp2 WHERE msg_id=?"; assert!(alice.sql.exists(stmt, (msg.id,)).await?); delete_msgs(alice, &[msg.id]).await?; assert!(!alice.sql.exists(stmt, (msg.id,)).await?); diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 478daa9a1a..52eefb2311 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -68,7 +68,7 @@ pub enum PreMessageMode { } #[derive(Debug, Clone)] -enum Encryption { +pub(crate) enum Encryption { /// Unencrypted message. No, @@ -207,27 +207,27 @@ pub(crate) struct QueuedMail { /// but without the From, Autocrypt and Message-ID headers. /// /// For encrypted messages this is the OpenPGP payload. - raw_message: Vec, + pub(crate) raw_message: Vec, /// Display name to put in the `From:` field. /// /// Email address is not determined yet here. - display_name: String, + pub(crate) display_name: String, /// Message-ID. - rfc724_mid: String, + pub(crate) rfc724_mid: String, /// Whether the message is encrypted and encryption keys. - encryption: Encryption, + pub(crate) encryption: Encryption, /// If true, Autocrypt header should be added before sending. - should_attach_pubkey: bool, + pub(crate) should_attach_pubkey: bool, /// If true, OpenPGP compression may be used. - should_compress: bool, + pub(crate) may_compress: bool, /// If true, encrypted message should be signed as well. - should_sign: bool, + pub(crate) should_sign: bool, } /// Side effects that should be applied at the same time @@ -265,7 +265,6 @@ pub(crate) fn render_queued_mail( public_key: &SignedPublicKey, secret_key: &SignedSecretKey, from_addr: String, - side_effects: RenderSideEffects, ) -> Result { let QueuedMail { rfc724_mid, @@ -273,7 +272,7 @@ pub(crate) fn render_queued_mail( raw_message, encryption, should_attach_pubkey, - should_compress, + may_compress, should_sign, } = queued_mail; @@ -443,7 +442,7 @@ pub(crate) fn render_queued_mail( full_raw_message, encryption_keyring, secret_key.clone(), - should_compress, + may_compress, seipd_version, )?; @@ -464,7 +463,7 @@ pub(crate) fn render_queued_mail( full_raw_message, sign_key, shared_secret, - should_compress, + may_compress, )?; let message = wrap_encrypted_part(encrypted); @@ -476,9 +475,7 @@ pub(crate) fn render_queued_mail( full_message.extend(message); Ok(RenderedEmail { message: String::from_utf8_lossy(&full_message).to_string(), - is_encrypted, rfc724_mid, - side_effects, }) } @@ -487,12 +484,8 @@ pub(crate) fn render_queued_mail( pub struct RenderedEmail { pub message: String, - pub is_encrypted: bool, - /// Message ID (Message in the sense of Email) pub rfc724_mid: String, - - pub side_effects: RenderSideEffects, } fn new_address_with_name(name: &str, address: String) -> Address<'static> { @@ -1349,14 +1342,8 @@ impl MimeFactory { let from_addr = context.get_primary_self_addr().await?; let public_key = key::load_self_public_key(context).await?; let secret_key = key::load_self_secret_key(context).await?; - let (queued_mail, side_effects) = Box::pin(self.into_queued_mail(context)).await?; - let rendered_mail = render_queued_mail( - queued_mail, - &public_key, - &secret_key, - from_addr, - side_effects, - )?; + let (queued_mail, _side_effects) = Box::pin(self.into_queued_mail(context)).await?; + let rendered_mail = render_queued_mail(queued_mail, &public_key, &secret_key, from_addr)?; Ok(rendered_mail) } @@ -1451,7 +1438,7 @@ impl MimeFactory { // Disable compression for SecureJoin to ensure // there are no compression side channels // leaking information about the tokens. - let should_compress = !is_securejoin_message; + let may_compress = !is_securejoin_message; if let Encryption::Asymmetric { ref encryption_pubkeys, @@ -1603,7 +1590,7 @@ impl MimeFactory { encryption: self.encryption, should_attach_pubkey, should_sign, - should_compress, + may_compress, }; Ok((queued_email, side_effects)) } @@ -2290,9 +2277,9 @@ impl MimeFactory { self.pre_message_mode = PreMessageMode::Post; } - pub fn set_as_pre_message_for(&mut self, post_message: &RenderedEmail) { + pub fn set_as_pre_message_for(&mut self, rfc724_mid: &str) { self.pre_message_mode = PreMessageMode::Pre { - post_msg_rfc724_mid: post_message.rfc724_mid.clone(), + post_msg_rfc724_mid: rfc724_mid.to_string(), }; } } @@ -2456,7 +2443,7 @@ pub(crate) async fn render_symm_encrypted_securejoin_message( should_attach_pubkey: bool, auth: &str, shared_secret: &str, -) -> Result { +) -> Result { info!(context, "Sending secure-join message {step:?}."); let message: MimePart<'static> = MimePart::new("text/plain", "Secure-Join"); @@ -2503,7 +2490,7 @@ pub(crate) async fn render_symm_encrypted_securejoin_message( // Disable compression for SecureJoin to ensure // there are no compression side channels // leaking information about the tokens. - let should_compress = false; + let may_compress = false; // Only sign the message if we attach the pubkey. let should_sign = should_attach_pubkey; @@ -2519,23 +2506,10 @@ pub(crate) async fn render_symm_encrypted_securejoin_message( }, should_attach_pubkey, should_sign, - should_compress, + may_compress, }; - let public_key = key::load_self_public_key(context).await?; - let secret_key = key::load_self_secret_key(context).await?; - let side_effects = RenderSideEffects::default(); - - let from_addr = context.get_primary_self_addr().await?; - let rendered_mail = render_queued_mail( - queued_mail, - &public_key, - &secret_key, - from_addr, - side_effects, - )?; - - Ok(rendered_mail.message) + Ok(queued_mail) } /// Renders MIME part into a vector of bytes. diff --git a/src/mimefactory/mimefactory_tests.rs b/src/mimefactory/mimefactory_tests.rs index 0a5b9d8e67..658b4b9167 100644 --- a/src/mimefactory/mimefactory_tests.rs +++ b/src/mimefactory/mimefactory_tests.rs @@ -308,9 +308,9 @@ async fn test_mdn_create_encrypted() -> Result<()> { message::markseen_msgs(&bob, vec![rcvd.id]).await?; let mimefactory = MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?; + assert!(!mimefactory.will_be_encrypted()); let rendered_msg = mimefactory.render(&bob).await?; - assert!(!rendered_msg.is_encrypted); assert!(!rendered_msg.message.contains("Bob Examplenet")); assert!(!rendered_msg.message.contains("Alice Exampleorg")); let bob_alice_contact = bob.add_or_lookup_contact(&alice).await; @@ -321,9 +321,9 @@ async fn test_mdn_create_encrypted() -> Result<()> { message::markseen_msgs(&bob, vec![rcvd.id]).await?; let mimefactory = MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?; + assert!(mimefactory.will_be_encrypted()); let rendered_msg = mimefactory.render(&bob).await?; - assert!(rendered_msg.is_encrypted); assert!(!rendered_msg.message.contains("Bob Examplenet")); assert!(!rendered_msg.message.contains("Alice Exampleorg")); diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 9a0df5e2d9..eeba74c9e7 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -561,7 +561,7 @@ pub(crate) async fn receive_imf_inner( context .sql .execute( - "DELETE FROM smtp \ + "DELETE FROM smtp2 \ WHERE rfc724_mid=?1 AND (recipients LIKE ?2 OR recipients LIKE ('% ' || ?2))", (rfc724_mid_orig, &self_addr), ) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 3808a3c1c4..d307f01aa5 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -15,7 +15,9 @@ use crate::headerdef::HeaderDefMap as _; use crate::imap::prefetch_should_download; use crate::imex::{ImexMode, imex}; use crate::key; +use crate::mimefactory; use crate::securejoin::get_securejoin_qr; +use crate::smtp; use crate::test_utils; use crate::test_utils::{ TestContext, TestContextManager, alice_keypair, get_chat_msg, mark_as_verified, @@ -5822,15 +5824,34 @@ async fn test_mark_message_as_delivered_only_after_sent_out_fully() -> Result<() /// This simulates the case that a message is successfully sent out, /// but the 'OK' answer from the server doesn't arrive, /// so that the SMTP row stays in the database. -pub(crate) async fn first_row_in_smtp_queue(alice: &TestContext) -> (MsgId, String) { - alice +pub(crate) async fn first_row_in_smtp_queue(context: &TestContext) -> (MsgId, String) { + let (rowid, msg_id) = context .sql - .query_row_optional("SELECT msg_id, mime FROM smtp ORDER BY id", (), |row| { - let msg_id: MsgId = row.get(0)?; - let mime: String = row.get(1)?; - Ok((msg_id, mime)) - }) + .query_row_optional( + "SELECT id, msg_id FROM smtp2 ORDER BY id LIMIT 1", + (), + |row| { + let rowid: i64 = row.get(0)?; + let msg_id: MsgId = row.get(1)?; + Ok((rowid, msg_id)) + }, + ) .await .expect("query_row_optional failed") - .expect("No SMTP row found") + .expect("No SMTP row found"); + let public_key = key::load_self_public_key(context).await.unwrap(); + let secret_key = key::load_self_secret_key(context).await.unwrap(); + let from_addr = context.get_primary_self_addr().await.unwrap(); + let query_only = true; + let (queued_mail, _recipients) = context + .sql + .transaction_ext(query_only, |transaction| { + smtp::load_queued_mail(transaction, rowid) + }) + .await + .unwrap(); + let rendered_mail = + mimefactory::render_queued_mail(queued_mail, &public_key, &secret_key, from_addr).unwrap(); + + (msg_id, rendered_mail.message) } diff --git a/src/securejoin.rs b/src/securejoin.rs index 7102e7bf23..ccd8480fb3 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -16,7 +16,7 @@ use crate::key; use crate::key::{DcKey, Fingerprint, load_self_public_key, self_fingerprint}; use crate::log::LogExt as _; use crate::log::warn; -use crate::message::{self, Message, MsgId, Viewtype}; +use crate::message::{self, Message, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::Param; use crate::qr::check_qr; @@ -552,11 +552,13 @@ pub(crate) async fn handle_securejoin_handshake( } let rfc724_mid = create_outgoing_rfc724_mid(); - let addr = ContactAddress::new(&mime_message.from.addr)?; + let addr = mime_message.from.addr.clone(); let attach_self_pubkey = true; let self_fp = self_fingerprint(context).await?; let shared_secret = format!("securejoin/{self_fp}/{auth}"); - let rendered_message = mimefactory::render_symm_encrypted_securejoin_message( + let now = time(); + let msg_id = message::insert_tombstone(context, &rfc724_mid).await?; + let queued_message = mimefactory::render_symm_encrypted_securejoin_message( context, "vc-pubkey", &rfc724_mid, @@ -566,8 +568,16 @@ pub(crate) async fn handle_securejoin_handshake( ) .await?; - let msg_id = message::insert_tombstone(context, &rfc724_mid).await?; - insert_into_smtp(context, &rfc724_mid, &addr, rendered_message, msg_id).await?; + chat::enqueue_mail( + context, + now, + msg_id, + ChatId::TRASH, + &queued_message, + &Default::default(), + &[addr], + ) + .await?; context.scheduler.interrupt_smtp().await; Ok(HandshakeMessage::Done) @@ -743,24 +753,6 @@ pub(crate) async fn handle_securejoin_handshake( } } -async fn insert_into_smtp( - context: &Context, - rfc724_mid: &str, - recipients: &str, - rendered_message: String, - msg_id: MsgId, -) -> Result<(), Error> { - context - .sql - .execute( - "INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id) - VALUES (?1, ?2, ?3, ?4)", - (&rfc724_mid, &recipients, &rendered_message, msg_id), - ) - .await?; - Ok(()) -} - /// Observe self-sent Securejoin message. /// /// In a multi-device-setup, there may be other devices that "see" the handshake messages. diff --git a/src/securejoin/bob.rs b/src/securejoin/bob.rs index 4ce5aed41e..884ad3943b 100644 --- a/src/securejoin/bob.rs +++ b/src/securejoin/bob.rs @@ -16,9 +16,7 @@ use crate::message::{self, Message, MsgId, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::{Param, Params}; use crate::pgp::addresses_from_public_key; -use crate::securejoin::{ - ContactId, encrypted_and_signed, insert_into_smtp, verify_sender_by_fingerprint, -}; +use crate::securejoin::{ContactId, encrypted_and_signed, verify_sender_by_fingerprint}; use crate::stock_str; use crate::sync::Sync::*; use crate::tools::{create_outgoing_rfc724_mid, time}; @@ -326,12 +324,12 @@ pub(crate) async fn send_handshake_message( if invite.is_v3() && matches!(step, BobHandshakeMsg::Request) { // Send a minimal symmetrically-encrypted vc-request-pubkey message let rfc724_mid = create_outgoing_rfc724_mid(); - let recipients = invite.addrs().join(" "); + let recipients = invite.addrs(); let alice_fp = invite.fingerprint().hex(); let auth = invite.authcode(); let shared_secret = format!("securejoin/{alice_fp}/{auth}"); let attach_self_pubkey = false; - let rendered_message = mimefactory::render_symm_encrypted_securejoin_message( + let queued_msg = mimefactory::render_symm_encrypted_securejoin_message( context, "vc-request-pubkey", &rfc724_mid, @@ -340,9 +338,19 @@ pub(crate) async fn send_handshake_message( &shared_secret, ) .await?; - + let now = time(); let msg_id = message::insert_tombstone(context, &rfc724_mid).await?; - insert_into_smtp(context, &rfc724_mid, &recipients, rendered_message, msg_id).await?; + chat::enqueue_mail( + context, + now, + msg_id, + chat_id, + &queued_msg, + &Default::default(), + recipients, + ) + .await?; + context.scheduler.interrupt_smtp().await; } else { let mut msg = Message { diff --git a/src/smtp.rs b/src/smtp.rs index 28ecb0fc6a..d771062fa4 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -6,6 +6,7 @@ pub mod send; use anyhow::{Context as _, Error, Result, bail, format_err}; use async_smtp::response::{Category, Code, Detail}; use async_smtp::{EmailAddress, SmtpTransport}; +use pgp::composed::SignedPublicKey; use tokio::task; use crate::chat::{ChatId, add_info_msg_with_cmd}; @@ -13,10 +14,14 @@ use crate::config::Config; use crate::contact::{Contact, ContactId}; use crate::context::Context; use crate::events::EventType; +use crate::key; +use crate::key::DcKey; use crate::log::{LogExt, warn}; use crate::message::Message; use crate::message::{self, MsgId}; +use crate::mimefactory; use crate::mimefactory::MimeFactory; +use crate::mimefactory::QueuedMail; use crate::net::proxy::ProxyConfig; use crate::net::session::SessionBufStream; use crate::scheduler::connectivity::ConnectivityStore; @@ -350,31 +355,44 @@ pub(crate) async fn send_msg_to_smtp( // database. context .sql - .execute("UPDATE smtp SET retries=retries+1 WHERE id=?", (rowid,)) + .execute("UPDATE smtp2 SET retries=retries+1 WHERE id=?", (rowid,)) .await .context("failed to update retries count")?; - let Some((body, recipients, msg_id, retries)) = context + // TODO: load together with queued mail + let Some((msg_id, retries)) = context .sql .query_row_optional( - "SELECT mime, recipients, msg_id, retries FROM smtp WHERE id=?", + "SELECT msg_id, retries FROM smtp2 WHERE id=?", (rowid,), |row| { - let mime: String = row.get(0)?; - let recipients: String = row.get(1)?; - let msg_id: MsgId = row.get(2)?; - let retries: i64 = row.get(3)?; - Ok((mime, recipients, msg_id, retries)) + let msg_id: MsgId = row.get(0)?; + let retries: i64 = row.get(1)?; + Ok((msg_id, retries)) }, ) .await? else { return Ok(()); }; + let (queued_mail, recipients) = context + .sql + .transaction_ext(true, |transaction| load_queued_mail(transaction, rowid)) + .await?; + let public_key = key::load_self_public_key(context).await?; + let secret_key = key::load_self_secret_key(context).await?; + + // FIXME: use the address of Smtp + let from_addr = context.get_primary_self_addr().await?; + + let rendered_mail = + mimefactory::render_queued_mail(queued_mail, &public_key, &secret_key, from_addr)?; + let body = rendered_mail.message; + if retries > 6 { context .sql - .execute("DELETE FROM smtp WHERE id=?", (rowid,)) + .execute("DELETE FROM smtp2 WHERE id=?", (rowid,)) .await .context("Failed to remove message with exceeded retry limit from smtp table")?; if let Some(mut msg) = Message::load_from_db_optional(context, msg_id).await? { @@ -419,7 +437,10 @@ pub(crate) async fn send_msg_to_smtp( .join(" "); context .sql - .execute("UPDATE smtp SET recipients=? WHERE id=?", (rest_str, rowid)) + .execute( + "UPDATE smtp2 SET recipients=? WHERE id=?", + (rest_str, rowid), + ) .await?; unsent = rest; }; @@ -429,7 +450,7 @@ pub(crate) async fn send_msg_to_smtp( SendResult::Success => { context .sql - .execute("DELETE FROM smtp WHERE id=?", (rowid,)) + .execute("DELETE FROM smtp2 WHERE id=?", (rowid,)) .await?; } SendResult::Failure(ref err) => { @@ -473,7 +494,7 @@ pub(crate) async fn send_msg_to_smtp( } context .sql - .execute("DELETE FROM smtp WHERE id=?", (rowid,)) + .execute("DELETE FROM smtp2 WHERE id=?", (rowid,)) .await?; } }; @@ -496,7 +517,7 @@ pub(crate) async fn msg_has_pending_smtp_job( ) -> Result { context .sql - .exists("SELECT COUNT(*) FROM smtp WHERE msg_id=?", (msg_id,)) + .exists("SELECT COUNT(*) FROM smtp2 WHERE msg_id=?", (msg_id,)) .await } @@ -529,7 +550,7 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp) let rowids = context .sql - .query_map_vec("SELECT id FROM smtp ORDER BY id ASC", (), |row| { + .query_map_vec("SELECT id FROM smtp2 ORDER BY id ASC", (), |row| { let rowid: i64 = row.get(0)?; Ok(rowid) }) @@ -729,3 +750,104 @@ pub(crate) async fn add_self_recipients( Ok(()) } + +/// Loads the queued mail from `smtp2` table and the list of recipients. +pub(crate) fn load_queued_mail( + transaction: &mut rusqlite::Transaction<'_>, + row_id: i64, +) -> Result<(QueuedMail, String)> { + let (mut queued_mail, encryption_fingerprints, recipients) = transaction + .query_row_and_then( + " +SELECT display_name, + rfc724_mid, + mime, + should_attach_pubkey, + may_compress, + should_sign, + is_encrypted, + shared_secret, + encryption_fingerprints, + recipients +FROM smtp2 WHERE id = ? +", + (row_id,), + |row| { + let display_name: String = row.get(0)?; + let rfc724_mid: String = row.get(1)?; + let raw_message: Vec = row.get(2)?; + let should_attach_pubkey: bool = row.get(3)?; + let may_compress: bool = row.get(4)?; + let should_sign: bool = row.get(5)?; + let is_encrypted: bool = row.get(6)?; + let shared_secret: String = row.get(7)?; + let encryption_fingerprints_json: String = row.get(8)?; + let encryption_fingerprints: Vec<(String, String)> = + if encryption_fingerprints_json.is_empty() { + Vec::new() + } else { + serde_json::from_str(&encryption_fingerprints_json).with_context(|| { + format!( + "Failed to parse JSON from encryption_fingerprints column: {:?}", + encryption_fingerprints_json + ) + })? + }; + let recipients: String = row.get(9)?; + + let encryption = match ( + is_encrypted, + shared_secret.is_empty(), + encryption_fingerprints.is_empty(), + ) { + (false, true, true) => mimefactory::Encryption::No, + (true, false, true) => mimefactory::Encryption::Symmetric { shared_secret }, + (true, true, _) => mimefactory::Encryption::Asymmetric { + // Public keys are loaded below based on the encryption fingerprints. + encryption_pubkeys: Vec::new(), + }, + _ => bail!("Invalid encryption in smtp2 row"), + }; + Ok::<_, anyhow::Error>(( + QueuedMail { + raw_message, + display_name, + rfc724_mid, + encryption, + should_attach_pubkey, + may_compress, + should_sign, + }, + encryption_fingerprints, + recipients, + )) + }, + ) + .with_context(|| format!("Failed to select row {row_id} from smtp2 table"))?; + + if let mimefactory::Encryption::Asymmetric { + ref mut encryption_pubkeys, + } = queued_mail.encryption + { + for (addr, fingerprint) in encryption_fingerprints { + use crate::rusqlite::OptionalExtension; + let public_key_bytes: Option> = transaction + .query_row( + "SELECT public_key FROM public_keys WHERE fingerprint=?", + (fingerprint,), + |row| { + let bytes: Vec = row.get(0)?; + Ok(bytes) + }, + ) + .optional() + .context("Failed to select public key by fingerprint")?; + if let Some(public_key_bytes) = public_key_bytes { + let public_key = SignedPublicKey::from_slice(&public_key_bytes)?; + encryption_pubkeys.push((addr, public_key)); + } + } + } + + Ok((queued_mail, recipients)) +} diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 805295d714..104e4ade4f 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -2610,6 +2610,30 @@ UPDATE msgs SET state=24 WHERE state=18; -- Change OutPreparing to OutFailed. .await?; } + inc_and_check(&mut migration_version, 164)?; + if dbversion < migration_version { + sql.execute_migration( + " +CREATE TABLE smtp2 ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + display_name TEXT NOT NULL, + rfc724_mid TEXT NOT NULL, + mime BLOB NOT NULL, + should_attach_pubkey INTEGER NOT NULL, + may_compress INTEGER NOT NULL, + should_sign INTEGER NOT NULL, + msg_id INTEGER NOT NULL, + recipients TEXT NOT NULL, + is_encrypted INTEGER NOT NULL, + shared_secret TEXT NOT NULL DEFAULT '', + encryption_fingerprints TEXT NOT NULL DEFAULT '', + retries INTEGER NOT NULL DEFAULT 0 +) STRICT;", + migration_version, + ) + .await?; + } + let new_version = sql .get_raw_config_int(VERSION_CFG) .await? diff --git a/src/test_utils.rs b/src/test_utils.rs index 64b15a094d..57ba0c1835 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -38,10 +38,12 @@ use crate::events::{Event, EventEmitter, EventType, Events}; use crate::key::{self, DcKey, self_fingerprint}; use crate::login_param::EnteredLoginParam; use crate::message::{Message, MessageState, MsgId}; +use crate::mimefactory; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::pgp::SeipdVersion; use crate::receive_imf::{ReceivedMsg, receive_imf}; use crate::securejoin::{get_securejoin_qr, join_securejoin}; +use crate::smtp; use crate::smtp::msg_has_pending_smtp_job; use crate::stock_str::StockStrings; use crate::tools::time; @@ -596,28 +598,36 @@ impl TestContext { pub async fn pop_sent_msg_ext(&self, rev_order: bool) -> Option> { let mut query = " -SELECT id, msg_id, mime, recipients -FROM smtp +SELECT id, msg_id, recipients +FROM smtp2 ORDER BY id" .to_string(); if rev_order { query += " DESC"; } - let (rowid, msg_id, payload, recipients) = self + let (rowid, msg_id, recipients) = self .ctx .sql .query_row_optional(&query, (), |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)) + let recipients: String = row.get(2)?; + Ok((rowid, msg_id, recipients)) }) .await .expect("query_row_optional failed")?; + let query_only = true; + let (queued_mail, _recipients) = self + .ctx + .sql + .transaction_ext(query_only, |transaction| { + smtp::load_queued_mail(transaction, rowid) + }) + .await + .expect("Failed to load queued mail"); self.ctx .sql - .execute("DELETE FROM smtp WHERE id=?;", (rowid,)) + .execute("DELETE FROM smtp2 WHERE id=?;", (rowid,)) .await .expect("failed to remove job"); if !msg_has_pending_smtp_job(self, msg_id) @@ -637,6 +647,24 @@ ORDER BY id" .expect("Failed to update timestamp_sent"); } + let public_key = key::load_self_public_key(self) + .await + .expect("Failed to load own public key"); + let secret_key = key::load_self_secret_key(self) + .await + .expect("Failed to load own secret key"); + + // FIXME: does not matter much for tests, + // can probably take the first transport address later + let from_addr = self + .get_primary_self_addr() + .await + .expect("Failed to get the From address"); + let rendered_mail = + mimefactory::render_queued_mail(queued_mail, &public_key, &secret_key, from_addr) + .expect("Failed to render queued mail"); + let payload = rendered_mail.message; + let payload_headers = payload.split("\r\n\r\n").next().unwrap().lines(); let payload_header_names: Vec<_> = payload_headers .map(|h| h.split(':').next().unwrap()) @@ -675,31 +703,64 @@ ORDER BY id" } pub async fn get_smtp_rows_for_msg<'a>(&'a self, msg_id: MsgId) -> Vec> { - let sent_msgs = self + let public_key = key::load_self_public_key(self) + .await + .expect("Failed to load own public key"); + let secret_key = key::load_self_secret_key(self) + .await + .expect("Failed to load own secret key"); + let from_addr = self + .get_primary_self_addr() + .await + .expect("Failed to get the From address"); + + let mut sent_msgs = Vec::new(); + + for (rowid, recipients) in self .ctx .sql .query_map_vec( - "SELECT mime, recipients FROM smtp WHERE msg_id=?", + "SELECT id, recipients FROM smtp2 WHERE msg_id=?", (msg_id,), |row| { - let mime: String = row.get(0)?; + let rowid: i64 = row.get(0)?; let recipients: String = row.get(1)?; - Ok((mime, recipients)) + Ok((rowid, recipients)) }, ) .await .unwrap() - .into_iter() - .map(|(mime, recipients)| SentMessage { - payload: mime, + { + let query_only = true; + let (queued_mail, _recipients) = self + .ctx + .sql + .transaction_ext(query_only, |transaction| { + smtp::load_queued_mail(transaction, rowid) + }) + .await + .expect("Failed to load queued mail"); + let rendered_mail = mimefactory::render_queued_mail( + queued_mail, + &public_key, + &secret_key, + from_addr.clone(), + ) + .expect("Failed to render queued mail"); + let payload = rendered_mail.message; + + let sent_message = SentMessage { + payload, sender_msg_id: msg_id, sender_context: &self.ctx, recipients, - }) - .collect(); + }; + sent_msgs.push(sent_message) + } + self.ctx .sql - .execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,)) + .execute("DELETE FROM smtp2 WHERE msg_id=?", (msg_id,)) .await .expect("Delete smtp jobs"); if msg_id diff --git a/src/tests/pre_messages/sending.rs b/src/tests/pre_messages/sending.rs index bc46c0b7d6..2c173e4304 100644 --- a/src/tests/pre_messages/sending.rs +++ b/src/tests/pre_messages/sending.rs @@ -346,7 +346,7 @@ async fn test_render_webxdc_status_update_object_range() -> Result<()> { .unwrap(); t.pop_sent_msg().await; - assert_eq!(t.sql.count("SELECT COUNT(*) FROM smtp", ()).await?, 0); + assert_eq!(t.sql.count("SELECT COUNT(*) FROM smtp2", ()).await?, 0); let long_text = String::from_utf8(vec![b'a'; 300_000])?; assert!(long_text.len() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD.try_into().unwrap()); @@ -354,6 +354,6 @@ async fn test_render_webxdc_status_update_object_range() -> Result<()> { .await?; t.flush_status_updates().await?; - assert_eq!(t.sql.count("SELECT COUNT(*) FROM smtp", ()).await?, 1); + assert_eq!(t.sql.count("SELECT COUNT(*) FROM smtp2", ()).await?, 1); Ok(()) } From 1fe0683abd90922faab773a72651dd5bdb48ef58 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 22 Aug 2026 13:34:07 +0000 Subject: [PATCH 4/5] refactor: move recipients into QueuedMail --- src/chat.rs | 20 +++++++++++--------- src/mimefactory.rs | 10 +++++++++- src/receive_imf/receive_imf_tests.rs | 2 +- src/securejoin.rs | 3 ++- src/securejoin/bob.rs | 2 +- src/smtp.rs | 19 +++++++++++++------ src/test_utils.rs | 28 ++++++++++++---------------- 7 files changed, 49 insertions(+), 35 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 4f616bcf47..fbe6ceb922 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2825,7 +2825,6 @@ pub(crate) async fn enqueue_mail( chat_id: ChatId, queued_mail: &QueuedMail, side_effects: &RenderSideEffects, - recipients: &[String], ) -> Result { if let Some(last_added_location_timestamp) = side_effects.last_added_location_timestamp { location::set_kml_sent_timestamp(context, chat_id, last_added_location_timestamp).await?; @@ -2849,7 +2848,7 @@ pub(crate) async fn enqueue_mail( } // Store mail into queue. - let all_recipients = recipients.join(" "); + let all_recipients = queued_mail.recipients.join(" "); let is_encrypted = queued_mail.encryption.is_encrypted(); let row_id = context @@ -2952,7 +2951,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - return Err(err); } }; - let mut recipients = mimefactory.recipients(); + let recipients = mimefactory.recipients(); // Default Webxdc integrations are hidden messages and must not be sent out: if (msg.param.get_int(Param::WebxdcIntegration).is_some() && msg.hidden) @@ -2970,7 +2969,8 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - return Ok(Vec::new()); } - let (queued_pre_msg_pair, queued_msg_pair) = + let is_encrypted = mimefactory.will_be_encrypted(); + let (mut queued_pre_msg_pair, queued_msg_pair) = match render_mime_message_and_pre_message(context, msg, mimefactory).await { Ok(res) => Ok(res), Err(err) => { @@ -2983,10 +2983,14 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - msg.pre_rfc724_mid = pre_msg.rfc724_mid.clone(); } - let (queued_msg, side_effects) = queued_msg_pair; - let is_encrypted = queued_msg.encryption.is_encrypted(); + let (mut queued_msg, side_effects) = queued_msg_pair; + if context.get_config_bool(Config::BccSelf).await? { - smtp::add_self_recipients(context, &mut recipients, is_encrypted).await?; + smtp::add_self_recipients(context, &mut queued_msg.recipients, is_encrypted).await?; + if let Some((ref mut queued_pre_msg, _)) = queued_pre_msg_pair { + smtp::add_self_recipients(context, &mut queued_pre_msg.recipients, is_encrypted) + .await?; + } } if needs_encryption && !is_encrypted { @@ -3063,7 +3067,6 @@ WHERE id=? msg.chat_id, &queued_pre_msg, &pre_side_effects, - &recipients, ) .await .context("Failed to enqueue pre-message")?; @@ -3077,7 +3080,6 @@ WHERE id=? msg.chat_id, &queued_msg, &side_effects, - &recipients, ) .await .context("Failed to enqueue message")?, diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 52eefb2311..6be2d8bbef 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -228,6 +228,9 @@ pub(crate) struct QueuedMail { /// If true, encrypted message should be signed as well. pub(crate) should_sign: bool, + + /// Recipient addresses. + pub(crate) recipients: Vec, } /// Side effects that should be applied at the same time @@ -274,6 +277,7 @@ pub(crate) fn render_queued_mail( should_attach_pubkey, may_compress, should_sign, + recipients: _, } = queued_mail; let mut inner_headers: Vec = Vec::new(); @@ -1546,7 +1550,7 @@ impl MimeFactory { let is_mdn = matches!(self.loaded, Loaded::Mdn { .. }); let should_sign = true; - let message = if self.will_be_encrypted() { + let message = if is_encrypted { add_headers_to_encrypted_part(message, headers) } else if is_mdn { // Never add outer multipart/mixed wrapper to MDN @@ -1582,6 +1586,7 @@ impl MimeFactory { }) }; let raw_message = part_to_bytes(message); + let recipients = self.recipients(); let queued_email = QueuedMail { raw_message, @@ -1591,6 +1596,7 @@ impl MimeFactory { should_attach_pubkey, should_sign, may_compress, + recipients, }; Ok((queued_email, side_effects)) } @@ -2443,6 +2449,7 @@ pub(crate) async fn render_symm_encrypted_securejoin_message( should_attach_pubkey: bool, auth: &str, shared_secret: &str, + recipients: Vec, ) -> Result { info!(context, "Sending secure-join message {step:?}."); @@ -2507,6 +2514,7 @@ pub(crate) async fn render_symm_encrypted_securejoin_message( should_attach_pubkey, should_sign, may_compress, + recipients, }; Ok(queued_mail) diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index d307f01aa5..cd467e1511 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -5843,7 +5843,7 @@ pub(crate) async fn first_row_in_smtp_queue(context: &TestContext) -> (MsgId, St let secret_key = key::load_self_secret_key(context).await.unwrap(); let from_addr = context.get_primary_self_addr().await.unwrap(); let query_only = true; - let (queued_mail, _recipients) = context + let queued_mail = context .sql .transaction_ext(query_only, |transaction| { smtp::load_queued_mail(transaction, rowid) diff --git a/src/securejoin.rs b/src/securejoin.rs index ccd8480fb3..e2d7db57fa 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -558,6 +558,7 @@ pub(crate) async fn handle_securejoin_handshake( let shared_secret = format!("securejoin/{self_fp}/{auth}"); let now = time(); let msg_id = message::insert_tombstone(context, &rfc724_mid).await?; + let recipients = vec![addr]; let queued_message = mimefactory::render_symm_encrypted_securejoin_message( context, "vc-pubkey", @@ -565,6 +566,7 @@ pub(crate) async fn handle_securejoin_handshake( attach_self_pubkey, auth, &shared_secret, + recipients, ) .await?; @@ -575,7 +577,6 @@ pub(crate) async fn handle_securejoin_handshake( ChatId::TRASH, &queued_message, &Default::default(), - &[addr], ) .await?; context.scheduler.interrupt_smtp().await; diff --git a/src/securejoin/bob.rs b/src/securejoin/bob.rs index 884ad3943b..a49f902e8a 100644 --- a/src/securejoin/bob.rs +++ b/src/securejoin/bob.rs @@ -336,6 +336,7 @@ pub(crate) async fn send_handshake_message( attach_self_pubkey, auth, &shared_secret, + recipients.clone(), ) .await?; let now = time(); @@ -347,7 +348,6 @@ pub(crate) async fn send_handshake_message( chat_id, &queued_msg, &Default::default(), - recipients, ) .await?; diff --git a/src/smtp.rs b/src/smtp.rs index d771062fa4..cc34d7dfdf 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -375,10 +375,11 @@ pub(crate) async fn send_msg_to_smtp( else { return Ok(()); }; - let (queued_mail, recipients) = context + let queued_mail = context .sql .transaction_ext(true, |transaction| load_queued_mail(transaction, rowid)) .await?; + let recipients = queued_mail.recipients.clone(); let public_key = key::load_self_public_key(context).await?; let secret_key = key::load_self_secret_key(context).await?; @@ -407,7 +408,7 @@ pub(crate) async fn send_msg_to_smtp( ); let recipients_list = recipients - .split(' ') + .into_iter() .filter_map( |addr| match async_smtp::EmailAddress::new(addr.to_string()) { Ok(addr) => Some(addr), @@ -755,8 +756,8 @@ pub(crate) async fn add_self_recipients( pub(crate) fn load_queued_mail( transaction: &mut rusqlite::Transaction<'_>, row_id: i64, -) -> Result<(QueuedMail, String)> { - let (mut queued_mail, encryption_fingerprints, recipients) = transaction +) -> Result { + let (mut queued_mail, encryption_fingerprints) = transaction .query_row_and_then( " SELECT display_name, @@ -794,6 +795,12 @@ FROM smtp2 WHERE id = ? })? }; let recipients: String = row.get(9)?; + let recipients: Vec = if recipients.is_empty() { + Vec::new() + } else { + recipients.split(' ').map(|s| s.to_string()).collect() + }; + debug_assert!(!recipients.iter().any(|s| s.is_empty())); let encryption = match ( is_encrypted, @@ -817,9 +824,9 @@ FROM smtp2 WHERE id = ? should_attach_pubkey, may_compress, should_sign, + recipients, }, encryption_fingerprints, - recipients, )) }, ) @@ -849,5 +856,5 @@ FROM smtp2 WHERE id = ? } } - Ok((queued_mail, recipients)) + Ok(queued_mail) } diff --git a/src/test_utils.rs b/src/test_utils.rs index 57ba0c1835..f3346c24b0 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -598,26 +598,25 @@ impl TestContext { pub async fn pop_sent_msg_ext(&self, rev_order: bool) -> Option> { let mut query = " -SELECT id, msg_id, recipients +SELECT id, msg_id FROM smtp2 ORDER BY id" .to_string(); if rev_order { query += " DESC"; } - let (rowid, msg_id, recipients) = self + let (rowid, msg_id) = self .ctx .sql .query_row_optional(&query, (), |row| { let rowid: i64 = row.get(0)?; let msg_id: MsgId = row.get(1)?; - let recipients: String = row.get(2)?; - Ok((rowid, msg_id, recipients)) + Ok((rowid, msg_id)) }) .await .expect("query_row_optional failed")?; let query_only = true; - let (queued_mail, _recipients) = self + let queued_mail = self .ctx .sql .transaction_ext(query_only, |transaction| { @@ -625,6 +624,7 @@ ORDER BY id" }) .await .expect("Failed to load queued mail"); + let recipients = queued_mail.recipients.join(" "); self.ctx .sql .execute("DELETE FROM smtp2 WHERE id=?;", (rowid,)) @@ -716,23 +716,18 @@ ORDER BY id" let mut sent_msgs = Vec::new(); - for (rowid, recipients) in self + for rowid in self .ctx .sql - .query_map_vec( - "SELECT id, recipients FROM smtp2 WHERE msg_id=?", - (msg_id,), - |row| { - let rowid: i64 = row.get(0)?; - let recipients: String = row.get(1)?; - Ok((rowid, recipients)) - }, - ) + .query_map_vec("SELECT id FROM smtp2 WHERE msg_id=?", (msg_id,), |row| { + let rowid: i64 = row.get(0)?; + Ok(rowid) + }) .await .unwrap() { let query_only = true; - let (queued_mail, _recipients) = self + let queued_mail = self .ctx .sql .transaction_ext(query_only, |transaction| { @@ -740,6 +735,7 @@ ORDER BY id" }) .await .expect("Failed to load queued mail"); + let recipients = queued_mail.recipients.join(" "); let rendered_mail = mimefactory::render_queued_mail( queued_mail, &public_key, From 6809578c6009d1e7817a644ed9e3d70cb7c2b7c1 Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 25 Aug 2026 18:15:59 +0000 Subject: [PATCH 5/5] Add BCC-self recipients late For unencrypted messages we only want to send a copy to the sending address, but we don't know the sending address when queueing the message. Adding bcc-self recipients when dequeuing the message also makes it possible to send copies to updated list of relays. --- docs/schema.sql | 6 ++++++ src/chat.rs | 37 ++++++++++++++++++------------------- src/mimefactory.rs | 19 ++++++++++++++++++- src/receive_imf.rs | 12 ++++-------- src/smtp.rs | 16 ++++++++++++++-- src/sql/migrations.rs | 1 + src/test_utils.rs | 24 ++++++++++++++++++++++-- 7 files changed, 83 insertions(+), 32 deletions(-) diff --git a/docs/schema.sql b/docs/schema.sql index 32940baf48..3310cf4a47 100644 --- a/docs/schema.sql +++ b/docs/schema.sql @@ -423,6 +423,12 @@ CREATE TABLE smtp2 ( -- List of recipients separated by space recipients TEXT NOT NULL, + -- If true, copy should be sent to self in addition to the recipient list. + -- + -- For encrypted messages copy is sent to all addresses. + -- For unencrypted messages, copy is sent to the From address only. + bcc_self INTEGER NOT NULL, + -- True if the message is encrypted. -- If true, exactly one of the shared_secret or encryption_fingerprints should be non-empty. -- If false, both must be empty. diff --git a/src/chat.rs b/src/chat.rs index fbe6ceb922..0850219cca 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -42,7 +42,7 @@ use crate::param::{Param, Params}; use crate::pgp::addresses_from_public_key; use crate::reaction::broadcast_reactions; use crate::receive_imf::ReceivedMsg; -use crate::smtp::{self, send_msg_to_smtp}; +use crate::smtp::send_msg_to_smtp; use crate::stock_str; use crate::sync::{self, Sync::*, SyncData}; use crate::tools::{ @@ -2772,6 +2772,7 @@ async fn render_mime_message_and_pre_message( context: &Context, msg: &mut Message, mimefactory: MimeFactory, + bcc_self: bool, ) -> Result<( Option<(QueuedMail, RenderSideEffects)>, (QueuedMail, RenderSideEffects), @@ -2792,14 +2793,15 @@ async fn render_mime_message_and_pre_message( let mut mimefactory_post_msg = mimefactory.clone(); mimefactory_post_msg.set_as_post_message(); - let (queued_msg, side_effects) = Box::pin(mimefactory_post_msg.into_queued_mail(context)) - .await - .context("Failed to render post-message")?; + let (queued_msg, side_effects) = + Box::pin(mimefactory_post_msg.into_queued_mail(context, bcc_self)) + .await + .context("Failed to render post-message")?; let mut mimefactory_pre_msg = mimefactory; mimefactory_pre_msg.set_as_pre_message_for(&queued_msg.rfc724_mid); let (queued_pre_msg, pre_side_effects) = - Box::pin(mimefactory_pre_msg.into_queued_mail(context)) + Box::pin(mimefactory_pre_msg.into_queued_mail(context, bcc_self)) .await .context("pre-message failed to render")?; @@ -2808,7 +2810,8 @@ async fn render_mime_message_and_pre_message( (queued_msg, side_effects), )) } else { - let (queued_msg, side_effects) = Box::pin(mimefactory.into_queued_mail(context)).await?; + let (queued_msg, side_effects) = + Box::pin(mimefactory.into_queued_mail(context, bcc_self)).await?; Ok((None, (queued_msg, side_effects))) } @@ -2864,12 +2867,13 @@ INSERT INTO smtp2 ( should_sign, msg_id, recipients, + bcc_self, is_encrypted, shared_secret, encryption_fingerprints ) VALUES ( - ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) ", ( @@ -2881,6 +2885,7 @@ VALUES ( queued_mail.should_sign, msg_id, &all_recipients, + queued_mail.bcc_self, is_encrypted, if let mimefactory::Encryption::Symmetric { ref shared_secret } = queued_mail.encryption @@ -2952,11 +2957,13 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - } }; let recipients = mimefactory.recipients(); + debug_assert!(!recipients.iter().any(|s| s.is_empty())); + let bcc_self = context.get_config_bool(Config::BccSelf).await?; // Default Webxdc integrations are hidden messages and must not be sent out: if (msg.param.get_int(Param::WebxdcIntegration).is_some() && msg.hidden) // This may happen eg. for groups with only SELF and bcc_self disabled: - || (!context.get_config_bool(Config::BccSelf).await? && recipients.is_empty()) + || (!bcc_self && recipients.is_empty()) { info!( context, @@ -2970,8 +2977,8 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - } let is_encrypted = mimefactory.will_be_encrypted(); - let (mut queued_pre_msg_pair, queued_msg_pair) = - match render_mime_message_and_pre_message(context, msg, mimefactory).await { + let (queued_pre_msg_pair, queued_msg_pair) = + match render_mime_message_and_pre_message(context, msg, mimefactory, bcc_self).await { Ok(res) => Ok(res), Err(err) => { message::set_msg_failed(context, msg, &err.to_string()).await?; @@ -2983,15 +2990,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - msg.pre_rfc724_mid = pre_msg.rfc724_mid.clone(); } - let (mut queued_msg, side_effects) = queued_msg_pair; - - if context.get_config_bool(Config::BccSelf).await? { - smtp::add_self_recipients(context, &mut queued_msg.recipients, is_encrypted).await?; - if let Some((ref mut queued_pre_msg, _)) = queued_pre_msg_pair { - smtp::add_self_recipients(context, &mut queued_pre_msg.recipients, is_encrypted) - .await?; - } - } + let (queued_msg, side_effects) = queued_msg_pair; if needs_encryption && !is_encrypted { let addr = context.get_config(Config::ConfiguredAddr).await?; diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 6be2d8bbef..631c97f2eb 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -231,6 +231,12 @@ pub(crate) struct QueuedMail { /// Recipient addresses. pub(crate) recipients: Vec, + + /// If true, own addresses should be added to the list of recipients. + /// + /// For unencrypted messages, only the sending addresses should be added. + /// For encrypted messages, all published addresses should be added. + pub(crate) bcc_self: bool, } /// Side effects that should be applied at the same time @@ -278,6 +284,7 @@ pub(crate) fn render_queued_mail( may_compress, should_sign, recipients: _, + bcc_self: _, } = queued_mail; let mut inner_headers: Vec = Vec::new(); @@ -1346,7 +1353,11 @@ impl MimeFactory { let from_addr = context.get_primary_self_addr().await?; let public_key = key::load_self_public_key(context).await?; let secret_key = key::load_self_secret_key(context).await?; - let (queued_mail, _side_effects) = Box::pin(self.into_queued_mail(context)).await?; + + // Does not matter, we are not going to return the QueuedMail. + let bcc_self = false; // Does not matter because we are not + let (queued_mail, _side_effects) = + Box::pin(self.into_queued_mail(context, bcc_self)).await?; let rendered_mail = render_queued_mail(queued_mail, &public_key, &secret_key, from_addr)?; Ok(rendered_mail) } @@ -1357,6 +1368,7 @@ impl MimeFactory { pub(crate) async fn into_queued_mail( mut self, context: &Context, + bcc_self: bool, ) -> Result<(QueuedMail, RenderSideEffects)> { let rfc724_mid = match &self.loaded { Loaded::Message { msg, .. } => match &self.pre_message_mode { @@ -1597,6 +1609,7 @@ impl MimeFactory { should_sign, may_compress, recipients, + bcc_self, }; Ok((queued_email, side_effects)) } @@ -2504,6 +2517,9 @@ pub(crate) async fn render_symm_encrypted_securejoin_message( let raw_message = part_to_bytes(message); + // Never send a copy of SecureJoin message to self. + let bcc_self = false; + let queued_mail = QueuedMail { raw_message, display_name: String::new(), @@ -2515,6 +2531,7 @@ pub(crate) async fn render_symm_encrypted_securejoin_message( should_sign, may_compress, recipients, + bcc_self, }; Ok(queued_mail) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index eeba74c9e7..a00f1b3bf6 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -555,16 +555,12 @@ pub(crate) async fn receive_imf_inner( // // Note that messages with long recipient lists are sent out in chunks, // removing already sent recipients from the job after each chunk. - // Self recipients are added at the end so removing the job - // removes the last chunk which apparently went out fine. - let self_addr = context.get_primary_self_addr().await?; + // Self recipients are sent in the end, + // so if we received a copy, the message has been sent out + // to all recipients. context .sql - .execute( - "DELETE FROM smtp2 \ - WHERE rfc724_mid=?1 AND (recipients LIKE ?2 OR recipients LIKE ('% ' || ?2))", - (rfc724_mid_orig, &self_addr), - ) + .execute("DELETE FROM smtp2 WHERE rfc724_mid=?", (rfc724_mid_orig,)) .await?; if !msg_has_pending_smtp_job(context, msg_id).await? { msg_id.set_delivered(context).await?; diff --git a/src/smtp.rs b/src/smtp.rs index cc34d7dfdf..cbe78317bc 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -379,7 +379,16 @@ pub(crate) async fn send_msg_to_smtp( .sql .transaction_ext(true, |transaction| load_queued_mail(transaction, rowid)) .await?; - let recipients = queued_mail.recipients.clone(); + let mut recipients = queued_mail.recipients.clone(); + if queued_mail.bcc_self { + add_self_recipients( + context, + &mut recipients, + queued_mail.encryption.is_encrypted(), + ) + .await + .expect("Failed to add self recipients"); + } let public_key = key::load_self_public_key(context).await?; let secret_key = key::load_self_secret_key(context).await?; @@ -769,7 +778,8 @@ SELECT display_name, is_encrypted, shared_secret, encryption_fingerprints, - recipients + recipients, + bcc_self FROM smtp2 WHERE id = ? ", (row_id,), @@ -801,6 +811,7 @@ FROM smtp2 WHERE id = ? recipients.split(' ').map(|s| s.to_string()).collect() }; debug_assert!(!recipients.iter().any(|s| s.is_empty())); + let bcc_self: bool = row.get(10)?; let encryption = match ( is_encrypted, @@ -825,6 +836,7 @@ FROM smtp2 WHERE id = ? may_compress, should_sign, recipients, + bcc_self, }, encryption_fingerprints, )) diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 104e4ade4f..bda2fa9083 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -2624,6 +2624,7 @@ CREATE TABLE smtp2 ( should_sign INTEGER NOT NULL, msg_id INTEGER NOT NULL, recipients TEXT NOT NULL, + bcc_self INTEGER NOT NULL, is_encrypted INTEGER NOT NULL, shared_secret TEXT NOT NULL DEFAULT '', encryption_fingerprints TEXT NOT NULL DEFAULT '', diff --git a/src/test_utils.rs b/src/test_utils.rs index f3346c24b0..b6cf5ce87c 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -616,7 +616,7 @@ ORDER BY id" .await .expect("query_row_optional failed")?; let query_only = true; - let queued_mail = self + let mut queued_mail = self .ctx .sql .transaction_ext(query_only, |transaction| { @@ -624,7 +624,17 @@ ORDER BY id" }) .await .expect("Failed to load queued mail"); + if queued_mail.bcc_self { + smtp::add_self_recipients( + &self.ctx, + &mut queued_mail.recipients, + queued_mail.encryption.is_encrypted(), + ) + .await + .expect("Failed to add self recipients"); + } let recipients = queued_mail.recipients.join(" "); + debug_assert!(!recipients.starts_with(" ")); self.ctx .sql .execute("DELETE FROM smtp2 WHERE id=?;", (rowid,)) @@ -727,7 +737,7 @@ ORDER BY id" .unwrap() { let query_only = true; - let queued_mail = self + let mut queued_mail = self .ctx .sql .transaction_ext(query_only, |transaction| { @@ -735,6 +745,15 @@ ORDER BY id" }) .await .expect("Failed to load queued mail"); + if queued_mail.bcc_self { + smtp::add_self_recipients( + &self.ctx, + &mut queued_mail.recipients, + queued_mail.encryption.is_encrypted(), + ) + .await + .expect("Failed to add self recipients"); + } let recipients = queued_mail.recipients.join(" "); let rendered_mail = mimefactory::render_queued_mail( queued_mail, @@ -745,6 +764,7 @@ ORDER BY id" .expect("Failed to render queued mail"); let payload = rendered_mail.message; + debug_assert!(!recipients.starts_with(" ")); let sent_message = SentMessage { payload, sender_msg_id: msg_id,