diff --git a/src/chat.rs b/src/chat.rs index e68ae7c61f..280af205da 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_MSG_ATTACHMENT_SIZE_THRESHOLD, PRE_MSG_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; @@ -2733,6 +2735,52 @@ 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() + && mimefactory.will_be_encrypted() // unencrypted is likely email, we don't want to spam by sending multiple messages + && 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 @@ -2804,13 +2852,14 @@ 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 { - Ok(res) => Ok(res), - Err(err) => { - message::set_msg_failed(context, msg, &err.to_string()).await?; - Err(err) - } - }?; + 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) + } + }?; if needs_encryption && !rendered_msg.is_encrypted { /* unrecoverable */ @@ -2870,12 +2919,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, + ), + )?; + 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, - recipients_chunk, + &recipients_chunk, &rendered_msg.message, msg.id, ), diff --git a/src/download.rs b/src/download.rs index bf54662175..fa075886ef 100644 --- a/src/download.rs +++ b/src/download.rs @@ -18,6 +18,22 @@ 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. +// 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. +/// This limit defines what messages are fully fetched in the background. +/// 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; + +/// 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_MSG_SIZE_WARNING_THRESHOLD: usize = 150_000; + /// Download state of the message. #[derive( Debug, @@ -193,12 +209,17 @@ impl Session { #[cfg(test)] mod tests { + use mailparse::MailHeaderMap; use num_traits::FromPrimitive; + use tokio::fs; use super::*; - use crate::chat::send_msg; + use crate::chat::{self, create_group, send_msg}; + use crate::config::Config; + use crate::headerdef::{HeaderDef, HeaderDefMap}; + use crate::message::Viewtype; use crate::receive_imf::receive_imf_from_inbox; - use crate::test_utils::TestContext; + use crate::test_utils::{self, TestContext, TestContextManager}; #[test] fn test_downloadstate_values() { @@ -295,4 +316,327 @@ mod tests { Ok(()) } + /// 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)] + 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 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, "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).await?.unwrap() > PRE_MSG_ATTACHMENT_SIZE_THRESHOLD); + + 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 + // and test that correct headers are present on both messages + assert_eq!(smtp_rows.len(), 2); + 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_parsed + .headers + .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) + .is_none() + ); + assert!( + full_message_parsed + .headers + .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) + .is_some() + ); + + assert_eq!( + 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_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 = 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 = bob.parse_msg(pre_message).await; + assert_eq!( + decrypted_pre_message + .get_header(HeaderDef::ChatFullMessageId) + .map(String::from), + full_message_parsed + .headers + .get_header_value(HeaderDef::MessageId) + ); + assert!( + pre_message_parsed + .headers + .get_header_value(HeaderDef::ChatFullMessageId) + .is_none(), + "no Chat-Full-Message-ID header in unprotected headers of Pre-Message" + ); + + 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_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 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, "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).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, group_id, &mut msg).await?; + let smtp_rows = alice.get_smtp_rows_for_msg(msg_id).await; + + assert_eq!(smtp_rows.len(), 2); + 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 = bob.parse_msg(pre_message).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 = bob.parse_msg(full_message).await; + assert!( + full_message_parsed + .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(()) + } + + #[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 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 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") + .payload + .as_bytes(); + let message = mailparse::parse_mail(message_bytes)?; + assert!( + message + .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<()> { + 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); + msg.set_text("test".to_owned()); + 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"); + + let msg = smtp_rows.first().expect("first element exists"); + let mail = mailparse::parse_mail(msg.payload.as_bytes())?; + + assert!( + mail.headers + .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) + .is_none(), + "no 'Chat-Is-Full-Message'-header should be present" + ); + assert!( + mail.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 = bob.parse_msg(msg).await; + assert!( + !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, 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 msg = smtp_rows.first().expect("first element exists"); + let mail = mailparse::parse_mail(msg.payload.as_bytes())?; + + assert!( + mail.headers + .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) + .is_none() + ); + assert!( + mail.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 = bob.parse_msg(msg).await; + assert!( + !decrypted_message.header_exists(HeaderDef::ChatFullMessageId), + "no 'Chat-Full-Message-ID'-header should be present" + ); + Ok(()) + } + + /// 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 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); + 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).await?.unwrap() < PRE_MSG_ATTACHMENT_SIZE_THRESHOLD); + + 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 + assert_eq!(smtp_rows.len(), 1); + + let msg = smtp_rows.first().expect("first element exists"); + let mail = mailparse::parse_mail(msg.payload.as_bytes())?; + + assert!( + mail.headers + .get_first_header(HeaderDef::ChatIsFullMessage.get_headername()) + .is_none() + ); + assert!( + mail.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 = bob.parse_msg(msg).await; + assert!( + !decrypted_message.header_exists(HeaderDef::ChatFullMessageId), + "no 'Chat-Full-Message-ID'-header should be present" + ); + + 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_MSG_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(()) + } } diff --git a/src/headerdef.rs b/src/headerdef.rs index 32c2281b58..510c2d9e29 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -102,6 +102,17 @@ pub enum HeaderDef { /// used to encrypt and decrypt messages. /// This secret is sent to a new member in the member-addition message. ChatBroadcastSecret, + /// 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 is preceded by a pre-message + /// and thus this message can be skipped while fetching messages. + /// This is a cleartext / unproteced header. + ChatIsFullMessage, /// [Autocrypt](https://autocrypt.org/) header. Autocrypt, @@ -144,6 +155,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() } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 3c7a2df638..cf71bb329c 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -58,6 +58,15 @@ pub enum Loaded { }, } +#[derive(Debug, Clone, PartialEq)] +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 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(( @@ -895,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 @@ -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(( + "Chat-Is-Full-Message", + mail_builder::headers::raw::Raw::new("1").into(), + )); + } else if let Some(PreMessageMode::PreMessage { + full_msg_rfc724_mid, + }) = self.pre_message_mode.clone() + { + protected_headers.push(( + "Chat-Full-Message-ID", + mail_builder::headers::message_id::MessageId::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,6 +1144,10 @@ impl MimeFactory { for (addr, key) in &encryption_pubkeys { let fingerprint = key.dc_fingerprint().hex(); let cmd = msg.param.get_cmd(); + if self.pre_message_mode == Some(PreMessageMode::FullMessage) { + continue; + } + let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup || cmd == SystemMessage::SecurejoinMessage || multiple_recipients && { @@ -1837,8 +1874,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,6 +1924,8 @@ impl MimeFactory { } } + 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 { @@ -1952,6 +1995,20 @@ 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); + } + + 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> { diff --git a/src/test_utils.rs b/src/test_utils.rs index 73e9875f13..f397ad1d5a 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -697,6 +697,32 @@ impl TestContext { }) } + 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 _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)) + }, + ) + .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. /// /// This retrieves and removes a sync message which has been scheduled to send from the jobs