diff --git a/src/chat.rs b/src/chat.rs index 015a82000c..9db1f5cf2f 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2839,7 +2839,6 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - return Err(err); } }; - let attach_selfavatar = mimefactory.attach_selfavatar; let mut recipients = mimefactory.recipients(); let from = context.get_primary_self_addr().await?; @@ -2926,15 +2925,20 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) - let now = time(); - if rendered_msg.last_added_location_id.is_some() - && let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await - { - error!(context, "Failed to set kml sent_timestamp: {err:#}."); + if let Some(last_added_location_timestamp) = rendered_msg.last_added_location_timestamp { + location::set_kml_sent_timestamp(context, msg.chat_id, last_added_location_timestamp) + .await?; } - if attach_selfavatar && let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await + if rendered_msg.avatar_is_attached + || rendered_pre_msg + .as_ref() + .is_some_and(|msg| msg.avatar_is_attached) { - error!(context, "Failed to set selfavatar timestamp: {err:#}."); + msg.chat_id + .set_selfavatar_timestamp(context, now) + .await + .context("Failed to set selfavatar timestamp")?; } if rendered_msg.is_encrypted { diff --git a/src/location.rs b/src/location.rs index 85ce22c386..cd575ac237 100644 --- a/src/location.rs +++ b/src/location.rs @@ -521,10 +521,9 @@ pub(crate) async fn delete_orphaned_poi(context: &Context) -> Result<()> { Ok(()) } -/// Returns `location.kml` contents. -#[expect(clippy::arithmetic_side_effects)] -pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result> { - let mut last_added_location_id = 0; +/// Returns `location.kml` contents and the largest location timestamp, if any. +pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result> { + let mut last_added_location_timestamp: Option = None; let self_addr = context.get_primary_self_addr().await?; @@ -540,7 +539,6 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result Result=? \ - AND (timestamp>=? OR \ + AND (timestamp>? OR \ timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \ AND independent=0 \ GROUP BY timestamp \ @@ -566,25 +564,24 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result\ - {timestamp}\ + {kml_timestamp}\ {longitude},{latitude}\ \n" ); - location_count += 1; - last_added_location_id = location_id as u32; + last_added_location_timestamp = std::cmp::max(last_added_location_timestamp, Some(timestamp)); } Ok(()) }, @@ -593,8 +590,8 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result 0 { - Ok(Some((ret, last_added_location_id))) + if let Some(last_added_location_timestamp) = last_added_location_timestamp { + Ok(Some((ret, last_added_location_timestamp))) } else { Ok(None) } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 07a899e7ab..53c192cca1 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -142,16 +142,8 @@ pub struct MimeFactory { /// using `Chat-Disposition-Notification-To` header. req_mdn: bool, - last_added_location_id: Option, - - /// If the created mime-structure contains sync-items, - /// the IDs of these items are listed here. - /// The IDs are returned via `RenderedEmail` - /// and must be deleted if the message is actually queued for sending. - sync_ids_to_delete: Option, - /// True if the avatar should be attached. - pub attach_selfavatar: bool, + attach_selfavatar: bool, /// This field is used to sustain the topic id of webxdcs needed for peer channels. webxdc_topic: Option, @@ -160,12 +152,35 @@ pub struct MimeFactory { pre_message_mode: PreMessageMode, } +/// Result of rendering non-MDN message. +pub struct RenderedMessage { + main_part: MimePart<'static>, + + parts: Vec>, + + /// Largest timestamp of the location sent in `location.kml` in this message. + last_added_location_timestamp: Option, + + /// True if the avatar is attached to the message. + avatar_is_attached: bool, + + /// If the created mime-structure contains sync-items, + /// the IDs of these items are listed here. + /// The IDs are returned via `RenderedEmail` + /// and must be deleted if the message is actually queued for sending. + sync_ids_to_delete: Option, +} + /// Result of rendering a message, ready to be submitted to a send job. #[derive(Debug, Clone)] pub struct RenderedEmail { pub message: String, pub is_encrypted: bool, - pub last_added_location_id: Option, + + /// Largest timestamp of the location sent in `location.kml` in this message. + pub last_added_location_timestamp: Option, + + pub avatar_is_attached: bool, /// A comma-separated string of sync-IDs that are used by the rendered email and must be deleted /// from `multi_device_sync` once the message is actually queued for sending. @@ -555,8 +570,6 @@ impl MimeFactory { in_reply_to, references, req_mdn, - last_added_location_id: None, - sync_ids_to_delete: None, attach_selfavatar, webxdc_topic, pre_message_mode: PreMessageMode::None, @@ -606,8 +619,6 @@ impl MimeFactory { in_reply_to: String::default(), references: Vec::new(), req_mdn: false, - last_added_location_id: None, - sync_ids_to_delete: None, attach_selfavatar: false, webxdc_topic: None, pre_message_mode: PreMessageMode::None, @@ -742,7 +753,22 @@ impl MimeFactory { pub async fn render(mut self, context: &Context) -> Result { let mut headers = Vec::<(&'static str, HeaderType<'static>)>::new(); - let from = new_address_with_name(&self.from_displayname, self.from_addr.clone()); + let is_encrypted = self.will_be_encrypted(); + let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded { + msg.param.get_cmd() == SystemMessage::SecurejoinMessage + } else { + false + }; + + let from = new_address_with_name( + if is_securejoin_message && !is_encrypted { + // Unencrypted securejoin messages should _not_ include the display name. + "" + } else { + &self.from_displayname + }, + self.from_addr.clone(), + ); let mut to: Vec> = Vec::new(); for (name, addr) in &self.to { @@ -972,8 +998,6 @@ impl MimeFactory { )); } - 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 // and ignored by the receiver. @@ -987,18 +1011,24 @@ impl MimeFactory { } } - let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded { - msg.param.get_cmd() == SystemMessage::SecurejoinMessage - } else { - false - }; - + let last_added_location_timestamp; + let avatar_is_attached; + let sync_ids_to_delete; let message: MimePart<'static> = match &self.loaded { Loaded::Message { msg, .. } => { let msg = msg.clone(); - let (main_part, mut parts) = self + let RenderedMessage { + main_part, + mut parts, + last_added_location_timestamp: tmp_last_added_location_timestamp, + avatar_is_attached: tmp_avatar_is_attached, + sync_ids_to_delete: tmp_sync_ids_to_delete, + } = self .render_message(context, &mut headers, &grpimage, is_encrypted) .await?; + last_added_location_timestamp = tmp_last_added_location_timestamp; + avatar_is_attached = tmp_avatar_is_attached; + sync_ids_to_delete = tmp_sync_ids_to_delete; if parts.is_empty() { // Single part, render as regular message. main_part @@ -1015,20 +1045,19 @@ impl MimeFactory { } } } - Loaded::Mdn { .. } => self.render_mdn()?, + Loaded::Mdn { .. } => { + last_added_location_timestamp = None; + avatar_is_attached = false; + sync_ids_to_delete = None; + self.render_mdn()? + } }; let HeadersByConfidentiality { mut unprotected_headers, hidden_headers, protected_headers, - } = group_headers_by_confidentiality( - headers, - &self.from_addr, - self.timestamp, - is_encrypted, - is_securejoin_message, - ); + } = group_headers_by_confidentiality(headers, &self.from_addr, self.timestamp); let outer_message = if let Some(encryption_pubkeys) = self.encryption_pubkeys { let mut message = add_headers_to_encrypted_part( @@ -1223,18 +1252,14 @@ impl MimeFactory { message }; - let MimeFactory { - last_added_location_id, - .. - } = self; - let message = render_outer_message(unprotected_headers, outer_message); Ok(RenderedEmail { message, is_encrypted, - last_added_location_id, - sync_ids_to_delete: self.sync_ids_to_delete, + last_added_location_timestamp, + avatar_is_attached, + sync_ids_to_delete, rfc724_mid, subject: subject_str, }) @@ -1255,16 +1280,17 @@ impl MimeFactory { Some(part) } - /// Returns MIME part with a `location.kml` attachment. + /// Returns MIME part with a `location.kml` attachment + /// and the timestamp of the latest location timestamp. async fn get_location_kml_part( - &mut self, + &self, context: &Context, - ) -> Result>> { + ) -> Result, i64)>> { let Loaded::Message { msg, .. } = &self.loaded else { return Ok(None); }; - let Some((kml_content, last_added_location_id)) = + let Some((kml_content, last_added_location_timestamp)) = location::get_kml(context, msg.chat_id).await? else { return Ok(None); @@ -1272,20 +1298,16 @@ impl MimeFactory { let part = MimePart::new("application/vnd.google-earth.kml+xml", kml_content) .attachment("location.kml"); - if !msg.param.exists(Param::SetLatitude) { - // otherwise, the independent location is already filed - self.last_added_location_id = Some(last_added_location_id); - } - Ok(Some(part)) + Ok(Some((part, last_added_location_timestamp))) } async fn render_message( - &mut self, + &self, context: &Context, headers: &mut Vec<(&'static str, HeaderType<'static>)>, grpimage: &Option, is_encrypted: bool, - ) -> Result<(MimePart<'static>, Vec>)> { + ) -> Result { let Loaded::Message { chat, msg } = &self.loaded else { bail!("Attempt to render MDN as a message"); }; @@ -1771,23 +1793,31 @@ impl MimeFactory { } } - if let Some(msg_kml_part) = self.get_message_kml_part() { + if !matches!(self.pre_message_mode, PreMessageMode::Pre { .. }) + && let Some(msg_kml_part) = self.get_message_kml_part() + { parts.push(msg_kml_part); } - if location::is_sending_to_chat(context, msg.chat_id).await? - && let Some(part) = self.get_location_kml_part(context).await? - { - parts.push(part); - } + let last_added_location_timestamp = + if !matches!(self.pre_message_mode, PreMessageMode::Pre { .. }) + && location::is_sending_to_chat(context, msg.chat_id).await? + && let Some((part, timestamp)) = self.get_location_kml_part(context).await? + { + parts.push(part); + Some(timestamp) + } else { + None + }; + let mut sync_ids_to_delete = None; // 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 { 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()); + sync_ids_to_delete = Some(ids.to_string()); } else if command == SystemMessage::WebxdcStatusUpdate { let json = msg.param.get(Param::Arg).unwrap_or_default(); parts.push(context.build_status_update_part(json)); @@ -1814,9 +1844,9 @@ impl MimeFactory { } } - self.attach_selfavatar = + let avatar_is_attached = self.attach_selfavatar && self.pre_message_mode != PreMessageMode::Post; - if self.attach_selfavatar { + if avatar_is_attached { match context.get_config(Config::Selfavatar).await? { Some(path) => match build_avatar_file(context, &path).await { Ok(avatar) => headers.push(( @@ -1832,7 +1862,13 @@ impl MimeFactory { } } - Ok((main_part, parts)) + Ok(RenderedMessage { + main_part, + parts, + last_added_location_timestamp, + avatar_is_attached, + sync_ids_to_delete, + }) } /// Render an MDN @@ -2022,8 +2058,6 @@ fn group_headers_by_confidentiality( headers: Vec<(&'static str, HeaderType<'static>)>, from_addr: &str, timestamp: i64, - is_encrypted: bool, - is_securejoin_message: bool, ) -> HeadersByConfidentiality { let mut unprotected_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new(); let mut hidden_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new(); @@ -2043,27 +2077,17 @@ fn group_headers_by_confidentiality( } else if is_hidden(&header_name) { hidden_headers.push(header.clone()); } else if header_name == "from" { - // Unencrypted securejoin messages should _not_ include the display name: - if is_encrypted || !is_securejoin_message { - protected_headers.push(header.clone()); - } - + protected_headers.push(header.clone()); unprotected_headers.push(( original_header_name, Address::new_address(None::<&'static str>, from_addr.to_string()).into(), )); } else if header_name == "to" { protected_headers.push(header.clone()); - if is_encrypted { - unprotected_headers.push(("To", hidden_recipients().into())); - } else { - unprotected_headers.push(header.clone()); - } + unprotected_headers.push(("To", hidden_recipients().into())); } else if header_name == "chat-broadcast-secret" { - if is_encrypted { - protected_headers.push(header.clone()); - } - } else if is_encrypted && header_name == "date" { + protected_headers.push(header.clone()); + } else if header_name == "date" { protected_headers.push(header.clone()); // Randomized date goes to unprotected header. @@ -2097,7 +2121,7 @@ fn group_headers_by_confidentiality( "Date", mail_builder::headers::raw::Raw::new(unprotected_date).into(), )); - } else if is_encrypted { + } else { protected_headers.push(header.clone()); match header_name.as_str() { @@ -2114,8 +2138,6 @@ fn group_headers_by_confidentiality( // Other headers are removed from unprotected part. } } - } else { - unprotected_headers.push(header.clone()) } } HeadersByConfidentiality { @@ -2300,19 +2322,11 @@ pub(crate) async fn render_symm_encrypted_securejoin_message( let message: MimePart<'static> = MimePart::new("text/plain", "Secure-Join"); - let is_encrypted = true; - let is_securejoin_message = true; let HeadersByConfidentiality { unprotected_headers, hidden_headers, protected_headers, - } = group_headers_by_confidentiality( - headers, - &from_addr, - timestamp, - is_encrypted, - is_securejoin_message, - ); + } = group_headers_by_confidentiality(headers, &from_addr, timestamp); let outer_message = { let message = add_headers_to_encrypted_part(