diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 789be5a040..89e792715c 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -6649,6 +6649,8 @@ void dc_event_unref(dc_event_t* event); /// /// Used as message text of outgoing read receipts. /// - %1$s will be replaced by the subject of the displayed message +/// +/// @deprecated Deprecated 2024-06-23, use DC_STR_READRCPT_MAILBODY2 instead. #define DC_STR_READRCPT_MAILBODY 32 /// @deprecated Deprecated, this string is no longer needed. @@ -7367,6 +7369,11 @@ void dc_event_unref(dc_event_t* event); /// Used as info message. #define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191 +/// "The message is a receipt notification." +/// +/// Used as message text of outgoing read receipts. +#define DC_STR_READRCPT_MAILBODY2 192 + /// "Contact". Deprecated, currently unused. #define DC_STR_CONTACT 200 diff --git a/node/constants.js b/node/constants.js index afdd5c99e7..3f3fe3c63b 100644 --- a/node/constants.js +++ b/node/constants.js @@ -266,6 +266,7 @@ module.exports = { DC_STR_REACTED_BY: 177, DC_STR_READRCPT: 31, DC_STR_READRCPT_MAILBODY: 32, + DC_STR_READRCPT_MAILBODY2: 192, DC_STR_REMOVE_MEMBER_BY_OTHER: 131, DC_STR_REMOVE_MEMBER_BY_YOU: 130, DC_STR_REPLY_NOUN: 90, diff --git a/node/lib/constants.ts b/node/lib/constants.ts index 4689757b13..7297bf386f 100644 --- a/node/lib/constants.ts +++ b/node/lib/constants.ts @@ -266,6 +266,7 @@ export enum C { DC_STR_REACTED_BY = 177, DC_STR_READRCPT = 31, DC_STR_READRCPT_MAILBODY = 32, + DC_STR_READRCPT_MAILBODY2 = 192, DC_STR_REMOVE_MEMBER_BY_OTHER = 131, DC_STR_REMOVE_MEMBER_BY_YOU = 130, DC_STR_REPLY_NOUN = 90, diff --git a/src/chat.rs b/src/chat.rs index 50b6361287..c659504cdb 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2908,7 +2908,7 @@ async fn prepare_send_msg( /// 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 needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default(); - let mimefactory = MimeFactory::from_msg(context, msg).await?; + let mimefactory = MimeFactory::from_msg(context, msg.clone()).await?; let attach_selfavatar = mimefactory.attach_selfavatar; let mut recipients = mimefactory.recipients(); diff --git a/src/mimefactory.rs b/src/mimefactory.rs index cbac942c12..00b42884bd 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; -use anyhow::{bail, ensure, Context as _, Result}; +use anyhow::{bail, Context as _, Result}; use base64::Engine as _; use chrono::TimeZone; use email::Mailbox; @@ -40,13 +40,19 @@ pub const RECOMMENDED_FILE_SIZE: u64 = 24 * 1024 * 1024 / 4 * 3; #[derive(Debug, Clone)] pub enum Loaded { - Message { chat: Chat }, - Mdn { additional_msg_ids: Vec }, + Message { + chat: Chat, + msg: Message, + }, + Mdn { + rfc724_mid: String, + additional_msg_ids: Vec, + }, } /// Helper to construct mime messages. #[derive(Debug, Clone)] -pub struct MimeFactory<'a> { +pub struct MimeFactory { from_addr: String, from_displayname: String, @@ -65,7 +71,6 @@ pub struct MimeFactory<'a> { timestamp: i64, loaded: Loaded, - msg: &'a Message, in_reply_to: String, /// Space-separated list of Message-IDs for `References` header. @@ -139,10 +144,10 @@ struct MessageHeaders { pub hidden: Vec
, } -impl<'a> MimeFactory<'a> { - pub async fn from_msg(context: &Context, msg: &'a Message) -> Result> { +impl MimeFactory { + pub async fn from_msg(context: &Context, msg: Message) -> Result { let chat = Chat::load_from_db(context, msg.chat_id).await?; - let attach_profile_data = Self::should_attach_profile_data(msg); + let attach_profile_data = Self::should_attach_profile_data(&msg); let from_addr = context.get_primary_self_addr().await?; let config_displayname = context @@ -236,7 +241,7 @@ impl<'a> MimeFactory<'a> { .unwrap_or_default(), false => "".to_string(), }; - let attach_selfavatar = Self::should_attach_selfavatar(context, msg).await; + let attach_selfavatar = Self::should_attach_selfavatar(context, &msg).await; let factory = MimeFactory { from_addr, @@ -245,8 +250,7 @@ impl<'a> MimeFactory<'a> { selfstatus, recipients, timestamp: msg.timestamp_sort, - loaded: Loaded::Message { chat }, - msg, + loaded: Loaded::Message { msg, chat }, in_reply_to, references, req_mdn, @@ -259,24 +263,25 @@ impl<'a> MimeFactory<'a> { pub async fn from_mdn( context: &Context, - msg: &'a Message, + from_id: ContactId, + rfc724_mid: String, additional_msg_ids: Vec, - ) -> Result> { - ensure!(!msg.chat_id.is_special(), "Invalid chat id"); - - let contact = Contact::get_by_id(context, msg.from_id).await?; + ) -> Result { + let contact = Contact::get_by_id(context, from_id).await?; let from_addr = context.get_primary_self_addr().await?; let timestamp = create_smeared_timestamp(context); - let res = MimeFactory::<'a> { + let res = MimeFactory { from_addr, from_displayname: "".to_string(), sender_displayname: None, selfstatus: "".to_string(), recipients: vec![("".to_string(), contact.get_addr().to_string())], timestamp, - loaded: Loaded::Mdn { additional_msg_ids }, - msg, + loaded: Loaded::Mdn { + rfc724_mid, + additional_msg_ids, + }, in_reply_to: String::default(), references: String::default(), req_mdn: false, @@ -308,21 +313,15 @@ impl<'a> MimeFactory<'a> { fn is_e2ee_guaranteed(&self) -> bool { match &self.loaded { - Loaded::Message { chat } => { + Loaded::Message { chat, msg } => { if chat.is_protected() { return true; } - !self - .msg - .param + !msg.param .get_bool(Param::ForcePlaintext) .unwrap_or_default() - && self - .msg - .param - .get_bool(Param::GuaranteeE2ee) - .unwrap_or_default() + && msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default() } Loaded::Mdn { .. } => false, } @@ -330,9 +329,9 @@ impl<'a> MimeFactory<'a> { fn verified(&self) -> bool { match &self.loaded { - Loaded::Message { chat } => { + Loaded::Message { chat, msg } => { if chat.is_protected() { - if self.msg.get_info_type() == SystemMessage::SecurejoinMessage { + if msg.get_info_type() == SystemMessage::SecurejoinMessage { // Securejoin messages are supposed to verify a key. // In order to do this, it is necessary that they can be sent // to a key that is not yet verified. @@ -351,9 +350,8 @@ impl<'a> MimeFactory<'a> { fn should_force_plaintext(&self) -> bool { match &self.loaded { - Loaded::Message { chat } => { - self.msg - .param + Loaded::Message { chat, msg } => { + msg.param .get_bool(Param::ForcePlaintext) .unwrap_or_default() || chat.typ == Chattype::Broadcast @@ -364,19 +362,17 @@ impl<'a> MimeFactory<'a> { fn should_skip_autocrypt(&self) -> bool { match &self.loaded { - Loaded::Message { .. } => self - .msg - .param - .get_bool(Param::SkipAutocrypt) - .unwrap_or_default(), + Loaded::Message { msg, .. } => { + msg.param.get_bool(Param::SkipAutocrypt).unwrap_or_default() + } Loaded::Mdn { .. } => true, } } async fn should_do_gossip(&self, context: &Context, multiple_recipients: bool) -> Result { match &self.loaded { - Loaded::Message { chat } => { - let cmd = self.msg.param.get_cmd(); + Loaded::Message { chat, msg } => { + let cmd = msg.param.get_cmd(); if cmd == SystemMessage::MemberAddedToGroup || cmd == SystemMessage::SecurejoinMessage { @@ -429,21 +425,20 @@ impl<'a> MimeFactory<'a> { fn grpimage(&self) -> Option { match &self.loaded { - Loaded::Message { chat } => { - let cmd = self.msg.param.get_cmd(); + Loaded::Message { chat, msg } => { + let cmd = msg.param.get_cmd(); match cmd { SystemMessage::MemberAddedToGroup => { return chat.param.get(Param::ProfileImage).map(Into::into); } SystemMessage::GroupImageChanged => { - return self.msg.param.get(Param::Arg).map(Into::into) + return msg.param.get(Param::Arg).map(Into::into) } _ => {} } - if self - .msg + if msg .param .get_bool(Param::AttachGroupImage) .unwrap_or_default() @@ -457,13 +452,13 @@ impl<'a> MimeFactory<'a> { } } - async fn subject_str(&self, context: &Context) -> anyhow::Result { - let quoted_msg_subject = self.msg.quoted_message(context).await?.map(|m| m.subject); + async fn subject_str(&self, context: &Context) -> Result { + let subject = match &self.loaded { + Loaded::Message { ref chat, msg } => { + let quoted_msg_subject = msg.quoted_message(context).await?.map(|m| m.subject); - let subject = match self.loaded { - Loaded::Message { ref chat } => { - if !self.msg.subject.is_empty() { - return Ok(self.msg.subject.clone()); + if !msg.subject.is_empty() { + return Ok(msg.subject.clone()); } if (chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast) @@ -486,7 +481,7 @@ impl<'a> MimeFactory<'a> { return Ok(format!("Re: {}", remove_subject_prefix(last_subject))); } - let self_name = match Self::should_attach_profile_data(self.msg) { + let self_name = match Self::should_attach_profile_data(msg) { true => context.get_config(Config::Displayname).await?, false => None, }; @@ -520,7 +515,7 @@ impl<'a> MimeFactory<'a> { ); let undisclosed_recipients = match &self.loaded { - Loaded::Message { chat } => chat.typ == Chattype::Broadcast, + Loaded::Message { chat, .. } => chat.typ == Chattype::Broadcast, Loaded::Mdn { .. } => false, }; @@ -531,12 +526,16 @@ impl<'a> MimeFactory<'a> { Vec::new(), )); } else { - let email_to_remove = - if self.msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup { - self.msg.param.get(Param::Arg) - } else { - None - }; + let email_to_remove = match &self.loaded { + Loaded::Message { msg, .. } => { + if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup { + msg.param.get(Param::Arg) + } else { + None + } + } + Loaded::Mdn { .. } => None, + }; for (name, addr) in &self.recipients { if let Some(email_to_remove) = email_to_remove { @@ -596,8 +595,8 @@ impl<'a> MimeFactory<'a> { .to_rfc2822(); headers.unprotected.push(Header::new("Date".into(), date)); - let rfc724_mid = match self.loaded { - Loaded::Message { .. } => self.msg.rfc724_mid.clone(), + let rfc724_mid = match &self.loaded { + Loaded::Message { msg, .. } => msg.rfc724_mid.clone(), Loaded::Mdn { .. } => create_outgoing_rfc724_mid(), }; let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid); @@ -630,7 +629,7 @@ impl<'a> MimeFactory<'a> { )); } - if let Loaded::Message { chat } = &self.loaded { + if let Loaded::Message { chat, .. } = &self.loaded { if chat.typ == Chattype::Broadcast { let encoded_chat_name = encode_words(&chat.name); headers.protected.push(Header::new( @@ -670,12 +669,17 @@ impl<'a> MimeFactory<'a> { .push(Header::new("Autocrypt".into(), aheader)); } - let ephemeral_timer = self.msg.chat_id.get_ephemeral_timer(context).await?; - if let EphemeralTimer::Enabled { duration } = ephemeral_timer { - headers.protected.push(Header::new( - "Ephemeral-Timer".to_string(), - duration.to_string(), - )); + // Add ephemeral timer for non-MDN messages. + // For MDNs it does not matter because they are not visible + // and ignored by the receiver. + if let Loaded::Message { msg, .. } = &self.loaded { + let ephemeral_timer = msg.chat_id.get_ephemeral_timer(context).await?; + if let EphemeralTimer::Enabled { duration } = ephemeral_timer { + headers.protected.push(Header::new( + "Ephemeral-Timer".to_string(), + duration.to_string(), + )); + } } // MIME header . @@ -718,8 +722,13 @@ impl<'a> MimeFactory<'a> { .unwrap(), ); } - if is_encrypted && verified || self.msg.param.get_cmd() == SystemMessage::SecurejoinMessage - { + + let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded { + msg.param.get_cmd() == SystemMessage::SecurejoinMessage + } else { + false + }; + if is_encrypted && verified || is_securejoin_message { headers.unprotected.insert( 0, Header::new_with_value( @@ -732,38 +741,39 @@ impl<'a> MimeFactory<'a> { headers.unprotected.insert(0, from_header); } - let (main_part, parts) = match self.loaded { - Loaded::Message { .. } => { - self.render_message(context, &mut headers, &grpimage, is_encrypted) - .await? - } - Loaded::Mdn { .. } => (self.render_mdn(context).await?, Vec::new()), - }; - - let message = if parts.is_empty() { - // Single part, render as regular message. - main_part - } else { - // Multiple parts, render as multipart. - let part_holder = if self.msg.param.get_cmd() == SystemMessage::MultiDeviceSync { - PartBuilder::new().header(( - "Content-Type".to_string(), - "multipart/report; report-type=multi-device-sync".to_string(), - )) - } else if self.msg.param.get_cmd() == SystemMessage::WebxdcStatusUpdate { - PartBuilder::new().header(( - "Content-Type".to_string(), - "multipart/report; report-type=status-update".to_string(), - )) - } else { - PartBuilder::new().message_type(MimeMultipartType::Mixed) - }; + let message = match &self.loaded { + Loaded::Message { msg, .. } => { + let msg = msg.clone(); + let (main_part, parts) = self + .render_message(context, &mut headers, &grpimage, is_encrypted) + .await?; + if parts.is_empty() { + // Single part, render as regular message. + main_part + } else { + // Multiple parts, render as multipart. + let part_holder = if msg.param.get_cmd() == SystemMessage::MultiDeviceSync { + PartBuilder::new().header(( + "Content-Type".to_string(), + "multipart/report; report-type=multi-device-sync".to_string(), + )) + } else if msg.param.get_cmd() == SystemMessage::WebxdcStatusUpdate { + PartBuilder::new().header(( + "Content-Type".to_string(), + "multipart/report; report-type=status-update".to_string(), + )) + } else { + PartBuilder::new().message_type(MimeMultipartType::Mixed) + }; - parts - .into_iter() - .fold(part_holder.child(main_part.build()), |message, part| { - message.child(part.build()) - }) + parts + .into_iter() + .fold(part_holder.child(main_part.build()), |message, part| { + message.child(part.build()) + }) + } + } + Loaded::Mdn { .. } => self.render_mdn(context).await?, }; let get_content_type_directives_header = || { @@ -825,7 +835,12 @@ impl<'a> MimeFactory<'a> { // Disable compression for SecureJoin to ensure // there are no compression side channels // leaking information about the tokens. - let compress = self.msg.param.get_cmd() != SystemMessage::SecurejoinMessage; + let compress = match &self.loaded { + Loaded::Message { msg, .. } => { + msg.param.get_cmd() != SystemMessage::SecurejoinMessage + } + Loaded::Mdn { .. } => true, + }; let encrypted = encrypt_helper .encrypt(context, verified, message, peerstates, compress) .await?; @@ -955,10 +970,14 @@ impl<'a> MimeFactory<'a> { /// Returns MIME part with a `message.kml` attachment. fn get_message_kml_part(&self) -> Option { - let latitude = self.msg.param.get_float(Param::SetLatitude)?; - let longitude = self.msg.param.get_float(Param::SetLongitude)?; + let Loaded::Message { msg, .. } = &self.loaded else { + return None; + }; + + let latitude = msg.param.get_float(Param::SetLatitude)?; + let longitude = msg.param.get_float(Param::SetLongitude)?; - let kml_file = location::get_message_kml(self.msg.timestamp_sort, latitude, longitude); + let kml_file = location::get_message_kml(msg.timestamp_sort, latitude, longitude); let part = PartBuilder::new() .content_type( &"application/vnd.google-earth.kml+xml" @@ -975,8 +994,12 @@ impl<'a> MimeFactory<'a> { /// Returns MIME part with a `location.kml` attachment. async fn get_location_kml_part(&mut self, context: &Context) -> Result> { + let Loaded::Message { msg, .. } = &self.loaded else { + return Ok(None); + }; + let Some((kml_content, last_added_location_id)) = - location::get_kml(context, self.msg.chat_id).await? + location::get_kml(context, msg.chat_id).await? else { return Ok(None); }; @@ -992,7 +1015,7 @@ impl<'a> MimeFactory<'a> { "attachment; filename=\"location.kml\"", )) .body(kml_content); - if !self.msg.param.exists(Param::SetLatitude) { + if !msg.param.exists(Param::SetLatitude) { // otherwise, the independent location is already filed self.last_added_location_id = Some(last_added_location_id); } @@ -1017,11 +1040,12 @@ impl<'a> MimeFactory<'a> { grpimage: &Option, is_encrypted: bool, ) -> Result<(PartBuilder, Vec)> { - let chat = match &self.loaded { - Loaded::Message { chat } => chat, - Loaded::Mdn { .. } => bail!("Attempt to render MDN as a message"), + let Loaded::Message { chat, msg } = &self.loaded else { + bail!("Attempt to render MDN as a message"); }; - let command = self.msg.param.get_cmd(); + let chat = chat.clone(); + let msg = msg.clone(); + let command = msg.param.get_cmd(); let mut placeholdertext = None; let send_verified_headers = match chat.typ { @@ -1052,7 +1076,7 @@ impl<'a> MimeFactory<'a> { match command { SystemMessage::MemberRemovedFromGroup => { - let email_to_remove = self.msg.param.get(Param::Arg).unwrap_or_default(); + let email_to_remove = msg.param.get(Param::Arg).unwrap_or_default(); if email_to_remove == context @@ -1074,7 +1098,7 @@ impl<'a> MimeFactory<'a> { } } SystemMessage::MemberAddedToGroup => { - let email_to_add = self.msg.param.get(Param::Arg).unwrap_or_default(); + let email_to_add = msg.param.get(Param::Arg).unwrap_or_default(); placeholdertext = Some(stock_str::msg_add_member_remote(context, email_to_add).await); @@ -1084,9 +1108,7 @@ impl<'a> MimeFactory<'a> { email_to_add.into(), )); } - if 0 != self.msg.param.get_int(Param::Arg2).unwrap_or_default() - & DC_FROM_HANDSHAKE - { + if 0 != msg.param.get_int(Param::Arg2).unwrap_or_default() & DC_FROM_HANDSHAKE { info!( context, "Sending secure-join message {:?}.", "vg-member-added", @@ -1098,7 +1120,7 @@ impl<'a> MimeFactory<'a> { } } SystemMessage::GroupNameChanged => { - let old_name = self.msg.param.get(Param::Arg).unwrap_or_default(); + let old_name = msg.param.get(Param::Arg).unwrap_or_default(); headers.protected.push(Header::new( "Chat-Group-Name-Changed".into(), maybe_encode_words(old_name), @@ -1157,7 +1179,6 @@ impl<'a> MimeFactory<'a> { placeholdertext = Some(stock_str::ac_setup_msg_body(context).await); } SystemMessage::SecurejoinMessage => { - let msg = &self.msg; let step = msg.param.get(Param::Arg).unwrap_or_default(); if !step.is_empty() { info!(context, "Sending secure-join message {step:?}."); @@ -1229,35 +1250,31 @@ impl<'a> MimeFactory<'a> { )); } - if self.msg.viewtype == Viewtype::Sticker { + if msg.viewtype == Viewtype::Sticker { headers .protected .push(Header::new("Chat-Content".into(), "sticker".into())); - } else if self.msg.viewtype == Viewtype::VideochatInvitation { + } else if msg.viewtype == Viewtype::VideochatInvitation { headers.protected.push(Header::new( "Chat-Content".into(), "videochat-invitation".into(), )); headers.protected.push(Header::new( "Chat-Webrtc-Room".into(), - self.msg - .param - .get(Param::WebrtcRoom) - .unwrap_or_default() - .into(), + msg.param.get(Param::WebrtcRoom).unwrap_or_default().into(), )); } - if self.msg.viewtype == Viewtype::Voice - || self.msg.viewtype == Viewtype::Audio - || self.msg.viewtype == Viewtype::Video + if msg.viewtype == Viewtype::Voice + || msg.viewtype == Viewtype::Audio + || msg.viewtype == Viewtype::Video { - if self.msg.viewtype == Viewtype::Voice { + if msg.viewtype == Viewtype::Voice { headers .protected .push(Header::new("Chat-Voice-Message".into(), "1".into())); } - let duration_ms = self.msg.param.get_int(Param::Duration).unwrap_or_default(); + let duration_ms = msg.param.get_int(Param::Duration).unwrap_or_default(); if duration_ms > 0 { let dur = duration_ms.to_string(); headers @@ -1271,7 +1288,7 @@ impl<'a> MimeFactory<'a> { // - we can add "forward hints" this way // - it looks better - let afwd_email = self.msg.param.exists(Param::Forwarded); + let afwd_email = msg.param.exists(Param::Forwarded); let fwdhint = if afwd_email { Some( "---------- Forwarded message ----------\r\n\ @@ -1283,19 +1300,12 @@ impl<'a> MimeFactory<'a> { None }; - let final_text = placeholdertext.as_deref().unwrap_or(&self.msg.text); + let final_text = placeholdertext.as_deref().unwrap_or(&msg.text); - let mut quoted_text = self - .msg + let mut quoted_text = msg .quoted_text() .map(|quote| format_flowed_quote("e) + "\r\n\r\n"); - if !is_encrypted - && self - .msg - .param - .get_bool(Param::ProtectQuote) - .unwrap_or_default() - { + if !is_encrypted && msg.param.get_bool(Param::ProtectQuote).unwrap_or_default() { // Message is not encrypted but quotes encrypted message. quoted_text = Some("> ...\r\n\r\n".to_string()); } @@ -1306,7 +1316,7 @@ impl<'a> MimeFactory<'a> { } let flowed_text = format_flowed(final_text); - let is_reaction = self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0; + let is_reaction = msg.param.get_int(Param::Reaction).unwrap_or_default() != 0; let footer = if is_reaction { "" } else { &self.selfstatus }; @@ -1338,13 +1348,13 @@ impl<'a> MimeFactory<'a> { // add HTML-part, this is needed only if a HTML-message from a non-delta-client is forwarded; // for simplificity and to avoid conversion errors, we're generating the HTML-part from the original message. - if self.msg.has_html() { - let html = if let Some(orig_msg_id) = self.msg.param.get_int(Param::Forwarded) { + if msg.has_html() { + let html = if let Some(orig_msg_id) = msg.param.get_int(Param::Forwarded) { MsgId::new(orig_msg_id.try_into()?) .get_html(context) .await? } else { - self.msg.param.get(Param::SendHtml).map(|s| s.to_string()) + msg.param.get(Param::SendHtml).map(|s| s.to_string()) }; if let Some(html) = html { main_part = PartBuilder::new() @@ -1355,8 +1365,8 @@ impl<'a> MimeFactory<'a> { } // add attachment part - if self.msg.viewtype.has_file() { - let (file_part, _) = build_body_file(context, self.msg, "").await?; + if msg.viewtype.has_file() { + let (file_part, _) = build_body_file(context, &msg, "").await?; parts.push(file_part); } @@ -1364,7 +1374,7 @@ impl<'a> MimeFactory<'a> { parts.push(msg_kml_part); } - if location::is_sending_locations_to_chat(context, Some(self.msg.chat_id)).await? { + if location::is_sending_locations_to_chat(context, Some(msg.chat_id)).await? { if let Some(part) = self.get_location_kml_part(context).await? { parts.push(part); } @@ -1373,20 +1383,20 @@ impl<'a> MimeFactory<'a> { // we do not piggyback sync-files to other self-sent-messages // to not risk files becoming too larger and being skipped by download-on-demand. if command == SystemMessage::MultiDeviceSync && self.is_e2ee_guaranteed() { - let json = self.msg.param.get(Param::Arg).unwrap_or_default(); - let ids = self.msg.param.get(Param::Arg2).unwrap_or_default(); + let json = msg.param.get(Param::Arg).unwrap_or_default(); + let ids = msg.param.get(Param::Arg2).unwrap_or_default(); parts.push(context.build_sync_part(json.to_string())); self.sync_ids_to_delete = Some(ids.to_string()); } else if command == SystemMessage::WebxdcStatusUpdate { - let json = self.msg.param.get(Param::Arg).unwrap_or_default(); + let json = msg.param.get(Param::Arg).unwrap_or_default(); parts.push(context.build_status_update_part(json)); - } else if self.msg.viewtype == Viewtype::Webxdc { + } else if msg.viewtype == Viewtype::Webxdc { let topic = peer_channels::create_random_topic(); headers .protected - .push(create_iroh_header(context, topic, self.msg.id).await?); + .push(create_iroh_header(context, topic, msg.id).await?); if let Some(json) = context - .render_webxdc_status_update_object(self.msg.id, None) + .render_webxdc_status_update_object(msg.id, None) .await? { parts.push(context.build_status_update_part(&json)); @@ -1425,11 +1435,12 @@ impl<'a> MimeFactory<'a> { // are forwarded for any reasons (eg. gmail always forwards to IMAP), we have no chance to decrypt them; // this issue is fixed with 0.9.4 - let additional_msg_ids = match &self.loaded { - Loaded::Message { .. } => bail!("Attempt to render a message as MDN"), - Loaded::Mdn { - additional_msg_ids, .. - } => additional_msg_ids, + let Loaded::Mdn { + rfc724_mid, + additional_msg_ids, + } = &self.loaded + else { + bail!("Attempt to render a message as MDN"); }; let mut message = PartBuilder::new().header(( @@ -1438,23 +1449,10 @@ impl<'a> MimeFactory<'a> { )); // first body part: always human-readable, always REQUIRED by RFC 6522 - let p1 = if 0 - != self - .msg - .param - .get_int(Param::GuaranteeE2ee) - .unwrap_or_default() - { - stock_str::encrypted_msg(context).await - } else { - self.msg - .get_summary(context, None) - .await? - .truncated_text(32) - .to_string() - }; - let p2 = stock_str::read_rcpt_mail_body(context, &p1).await; - let message_text = format!("{}\r\n", format_flowed(&p2)); + let message_text = format!( + "{}\r\n", + format_flowed(&stock_str::read_rcpt_mail_body(context).await) + ); let text_part = PartBuilder::new().header(( "Content-Type".to_string(), "text/plain; charset=utf-8; format=flowed; delsp=no".to_string(), @@ -1468,7 +1466,7 @@ impl<'a> MimeFactory<'a> { Final-Recipient: rfc822;{}\r\n\ Original-Message-ID: <{}>\r\n\ Disposition: manual-action/MDN-sent-automatically; displayed\r\n", - self.from_addr, self.from_addr, self.msg.rfc724_mid + self.from_addr, self.from_addr, rfc724_mid ); let extension_fields = if additional_msg_ids.is_empty() { @@ -1930,7 +1928,7 @@ mod tests { Original-Message-ID: <2893@example.com>\n\ Disposition: manual-action/MDN-sent-automatically; displayed\n\ \n", &t).await; - let mf = MimeFactory::from_msg(&t, &new_msg).await.unwrap(); + let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap(); // The subject string should not be "Re: message opened" assert_eq!("Re: Hello, Bob", mf.subject_str(&t).await.unwrap()); } @@ -1956,7 +1954,8 @@ mod tests { let rcvd = bob.recv_msg(&sent).await; message::markseen_msgs(&bob, vec![rcvd.id]).await?; - let mimefactory = MimeFactory::from_mdn(&bob, &rcvd, vec![]).await?; + let mimefactory = + MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?; let rendered_msg = mimefactory.render(&bob).await?; assert!(!rendered_msg.is_encrypted); @@ -1968,7 +1967,8 @@ mod tests { let rcvd = tcm.send_recv(&alice, &bob, "Heyho").await; message::markseen_msgs(&bob, vec![rcvd.id]).await?; - let mimefactory = MimeFactory::from_mdn(&bob, &rcvd, vec![]).await?; + let mimefactory = + MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?; let rendered_msg = mimefactory.render(&bob).await?; // When encrypted, the MDN should be encrypted as well @@ -2078,7 +2078,7 @@ mod tests { new_msg.chat_id = chat_id; chat::prepare_msg(&t, chat_id, &mut new_msg).await.unwrap(); - let mf = MimeFactory::from_msg(&t, &new_msg).await.unwrap(); + let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap(); mf.subject_str(&t).await.unwrap() } @@ -2163,7 +2163,7 @@ mod tests { new_msg.set_quote(&t, Some(&incoming_msg)).await.unwrap(); } - let mf = MimeFactory::from_msg(&t, &new_msg).await.unwrap(); + let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap(); mf.subject_str(&t).await.unwrap() } @@ -2211,7 +2211,7 @@ mod tests { ) .await; - let mimefactory = MimeFactory::from_msg(&t, &msg).await.unwrap(); + let mimefactory = MimeFactory::from_msg(&t, msg).await.unwrap(); let recipients = mimefactory.recipients(); assert_eq!(recipients, vec!["charlie@example.com"]); diff --git a/src/receive_imf/tests.rs b/src/receive_imf/tests.rs index a7ca9579bf..208027c06e 100644 --- a/src/receive_imf/tests.rs +++ b/src/receive_imf/tests.rs @@ -2800,8 +2800,13 @@ async fn test_read_receipts_dont_create_chats() -> Result<()> { assert_eq!(chats.len(), 0); // Bob sends a read receipt. - let mdn_mimefactory = - crate::mimefactory::MimeFactory::from_mdn(&bob, &received_msg, vec![]).await?; + let mdn_mimefactory = crate::mimefactory::MimeFactory::from_mdn( + &bob, + received_msg.from_id, + received_msg.rfc724_mid, + vec![], + ) + .await?; let rendered_mdn = mdn_mimefactory.render(&bob).await?; let mdn_body = rendered_mdn.message; @@ -2830,8 +2835,13 @@ async fn test_read_receipts_dont_unmark_bots() -> Result<()> { let received_msg = bob.get_last_msg().await; // Bob sends a read receipt. - let mdn_mimefactory = - crate::mimefactory::MimeFactory::from_mdn(bob, &received_msg, vec![]).await?; + let mdn_mimefactory = crate::mimefactory::MimeFactory::from_mdn( + bob, + received_msg.from_id, + received_msg.rfc724_mid, + vec![], + ) + .await?; let rendered_mdn = mdn_mimefactory.render(bob).await?; let mdn_body = rendered_mdn.message; diff --git a/src/smtp.rs b/src/smtp.rs index 0683059188..af7e388737 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -361,7 +361,7 @@ pub(crate) async fn smtp_send( recipients: &[async_smtp::EmailAddress], message: &str, smtp: &mut Smtp, - msg_id: MsgId, + msg_id: Option, ) -> SendResult { if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { info!(context, "SMTP-sending out mime message:\n{message}"); @@ -489,19 +489,22 @@ pub(crate) async fn smtp_send( }; if let SendResult::Failure(err) = &status { - // We couldn't send the message, so mark it as failed - match Message::load_from_db(context, msg_id).await { - Ok(mut msg) => { - if let Err(err) = message::set_msg_failed(context, &mut msg, &err.to_string()).await - { - error!(context, "Failed to mark {msg_id} as failed: {err:#}."); + if let Some(msg_id) = msg_id { + // We couldn't send the message, so mark it as failed + match Message::load_from_db(context, msg_id).await { + Ok(mut msg) => { + if let Err(err) = + message::set_msg_failed(context, &mut msg, &err.to_string()).await + { + error!(context, "Failed to mark {msg_id} as failed: {err:#}."); + } + } + Err(err) => { + error!( + context, + "Failed to load {msg_id} to mark it as failed: {err:#}." + ); } - } - Err(err) => { - error!( - context, - "Failed to load {msg_id} to mark it as failed: {err:#}." - ); } } } @@ -577,7 +580,7 @@ pub(crate) async fn send_msg_to_smtp( ) .collect::>(); - let status = smtp_send(context, &recipients_list, body.as_str(), smtp, msg_id).await; + let status = smtp_send(context, &recipients_list, body.as_str(), smtp, Some(msg_id)).await; match status { SendResult::Retry => {} @@ -706,7 +709,7 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp) Ok(()) } -/// Tries to send MDN for message `msg_id` to `contact_id`. +/// Tries to send MDN for message identified by `rfc724_mdn` to `contact_id`. /// /// Attempts to aggregate additional MDNs for `contact_id` into sent MDN. /// @@ -715,9 +718,9 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp) /// points to non-existent message or contact. /// /// Returns true on success, false on temporary error. -async fn send_mdn_msg_id( +async fn send_mdn_rfc724_mid( context: &Context, - msg_id: MsgId, + rfc724_mid: &str, contact_id: ContactId, smtp: &mut Smtp, ) -> Result { @@ -727,26 +730,30 @@ async fn send_mdn_msg_id( } // Try to aggregate additional MDNs into this MDN. - let (additional_msg_ids, additional_rfc724_mids): (Vec, Vec) = context + let additional_rfc724_mids: Vec = context .sql .query_map( - "SELECT msg_id, rfc724_mid + "SELECT rfc724_mid FROM smtp_mdns - WHERE from_id=? AND msg_id!=?", - (contact_id, msg_id), + WHERE from_id=? AND rfc724_mid!=?", + (contact_id, &rfc724_mid), |row| { - let msg_id: MsgId = row.get(0)?; - let rfc724_mid: String = row.get(1)?; - Ok((msg_id, rfc724_mid)) + let rfc724_mid: String = row.get(0)?; + Ok(rfc724_mid) }, |rows| rows.collect::, _>>().map_err(Into::into), ) .await? .into_iter() - .unzip(); + .collect(); - let msg = Message::load_from_db(context, msg_id).await?; - let mimefactory = MimeFactory::from_mdn(context, &msg, additional_rfc724_mids).await?; + let mimefactory = MimeFactory::from_mdn( + context, + contact_id, + rfc724_mid.to_string(), + additional_rfc724_mids.clone(), + ) + .await?; let rendered_msg = mimefactory.render(context).await?; let body = rendered_msg.message; @@ -755,21 +762,21 @@ async fn send_mdn_msg_id( .map_err(|err| format_err!("invalid recipient: {} {:?}", addr, err))?; let recipients = vec![recipient]; - match smtp_send(context, &recipients, &body, smtp, msg_id).await { + match smtp_send(context, &recipients, &body, smtp, None).await { SendResult::Success => { - info!(context, "Successfully sent MDN for {msg_id}."); + info!(context, "Successfully sent MDN for {rfc724_mid}."); context .sql - .execute("DELETE FROM smtp_mdns WHERE msg_id = ?", (msg_id,)) + .execute("DELETE FROM smtp_mdns WHERE rfc724_mid = ?", (rfc724_mid,)) .await?; - if !additional_msg_ids.is_empty() { + if !additional_rfc724_mids.is_empty() { let q = format!( - "DELETE FROM smtp_mdns WHERE msg_id IN({})", - sql::repeat_vars(additional_msg_ids.len()) + "DELETE FROM smtp_mdns WHERE rfc724_mid IN({})", + sql::repeat_vars(additional_rfc724_mids.len()) ); context .sql - .execute(&q, rusqlite::params_from_iter(additional_msg_ids)) + .execute(&q, rusqlite::params_from_iter(additional_rfc724_mids)) .await?; } Ok(true) @@ -777,7 +784,7 @@ async fn send_mdn_msg_id( SendResult::Retry => { info!( context, - "Temporary SMTP failure while sending an MDN for {msg_id}." + "Temporary SMTP failure while sending an MDN for {rfc724_mid}." ); Ok(false) } @@ -802,40 +809,40 @@ async fn send_mdn(context: &Context, smtp: &mut Smtp) -> Result { let Some(msg_row) = context .sql .query_row_optional( - "SELECT msg_id, from_id FROM smtp_mdns ORDER BY retries LIMIT 1", + "SELECT rfc724_mid, from_id FROM smtp_mdns ORDER BY retries LIMIT 1", [], |row| { - let msg_id: MsgId = row.get(0)?; + let rfc724_mid: String = row.get(0)?; let from_id: ContactId = row.get(1)?; - Ok((msg_id, from_id)) + Ok((rfc724_mid, from_id)) }, ) .await? else { return Ok(false); }; - let (msg_id, contact_id) = msg_row; + let (rfc724_mid, contact_id) = msg_row; context .sql .execute( - "UPDATE smtp_mdns SET retries=retries+1 WHERE msg_id=?", - (msg_id,), + "UPDATE smtp_mdns SET retries=retries+1 WHERE rfc724_mid=?", + (rfc724_mid.clone(),), ) .await .context("Failed to update MDN retries count")?; - match send_mdn_msg_id(context, msg_id, contact_id, smtp).await { + match send_mdn_rfc724_mid(context, &rfc724_mid, contact_id, smtp).await { Err(err) => { // If there is an error, for example there is no message corresponding to the msg_id in the // database, do not try to send this MDN again. warn!( context, - "Error sending MDN for {msg_id}, removing it: {err:#}." + "Error sending MDN for {rfc724_mid}, removing it: {err:#}." ); context .sql - .execute("DELETE FROM smtp_mdns WHERE msg_id = ?", (msg_id,)) + .execute("DELETE FROM smtp_mdns WHERE rfc724_mid = ?", (rfc724_mid,)) .await?; Err(err) } diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 9546fceba9..807685c623 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -578,7 +578,7 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid); if dbversion < 90 { sql.execute_migration( r#"CREATE TABLE smtp_mdns ( - msg_id INTEGER NOT NULL, -- id of the message in msgs table which requested MDN + msg_id INTEGER NOT NULL, -- id of the message in msgs table which requested MDN (DEPRECATED 2024-06-21) from_id INTEGER NOT NULL, -- id of the contact that sent the message, MDN destination rfc724_mid TEXT NOT NULL, -- Message-ID header retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send MDN diff --git a/src/stock_str.rs b/src/stock_str.rs index 984a658f82..0b5908cde4 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -83,9 +83,6 @@ pub enum StockMessage { #[strum(props(fallback = "Return receipt"))] ReadRcpt = 31, - #[strum(props(fallback = "This is a return receipt for the message \"%1$s\"."))] - ReadRcptMailBody = 32, - #[strum(props(fallback = "End-to-end encryption preferred"))] E2ePreferred = 34, @@ -443,6 +440,9 @@ pub enum StockMessage { fallback = "Could not yet establish guaranteed end-to-end encryption, but you may already send a message." ))] SecurejoinWaitTimeout = 191, + + #[strum(props(fallback = "This message is a receipt notification."))] + ReadRcptMailBody = 192, } impl StockMessage { @@ -770,11 +770,6 @@ pub(crate) async fn gif(context: &Context) -> String { translated(context, StockMessage::Gif).await } -/// Stock string: `Encrypted message`. -pub(crate) async fn encrypted_msg(context: &Context) -> String { - translated(context, StockMessage::EncryptedMsg).await -} - /// Stock string: `End-to-end encryption available.`. pub(crate) async fn e2e_available(context: &Context) -> String { translated(context, StockMessage::E2eAvailable).await @@ -805,11 +800,9 @@ pub(crate) async fn read_rcpt(context: &Context) -> String { translated(context, StockMessage::ReadRcpt).await } -/// Stock string: `This is a return receipt for the message "%1$s".`. -pub(crate) async fn read_rcpt_mail_body(context: &Context, message: &str) -> String { - translated(context, StockMessage::ReadRcptMailBody) - .await - .replace1(message) +/// Stock string: `This message is a receipt notification.`. +pub(crate) async fn read_rcpt_mail_body(context: &Context) -> String { + translated(context, StockMessage::ReadRcptMailBody).await } /// Stock string: `Group image deleted.`. diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs index a13ce5d79e..33d1179a61 100644 --- a/src/tests/verified_chats.rs +++ b/src/tests/verified_chats.rs @@ -542,7 +542,7 @@ async fn test_mdn_doesnt_disable_verification() -> Result<()> { let rcvd = tcm.send_recv_accept(&alice, &bob, "Heyho").await; message::markseen_msgs(&bob, vec![rcvd.id]).await?; - let mimefactory = MimeFactory::from_mdn(&bob, &rcvd, vec![]).await?; + let mimefactory = MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?; let rendered_msg = mimefactory.render(&bob).await?; let body = rendered_msg.message; receive_imf(&alice, body.as_bytes(), false).await.unwrap();