diff --git a/src/chat.rs b/src/chat.rs index 509f1b16b..8bb134321 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1597,6 +1597,46 @@ pub fn set_gossiped_timestamp(context: &Context, chat_id: u32, timestamp: i64) { } } +pub fn shall_attach_selfavatar(context: &Context, chat_id: u32) -> Result { + let resend_every_days = 14; + let timestamp_some_days_ago = time() - resend_every_days * 24 * 60 * 60; + let needs_attach = context.sql.query_map( + "SELECT c.selfavatar_sent + FROM chats_contacts cc + LEFT JOIN contacts c ON c.id=cc.contact_id + WHERE cc.chat_id=? AND cc.contact_id!=?;", + params![chat_id, DC_CONTACT_ID_SELF], + |row| Ok(row.get::<_, i64>(0)), + |rows| { + let mut needs_attach = false; + for row in rows { + if let Ok(selfavatar_sent) = row { + let selfavatar_sent = selfavatar_sent?; + if selfavatar_sent < timestamp_some_days_ago { + needs_attach = true; + } + } + } + Ok(needs_attach) + }, + )?; + Ok(needs_attach) +} + +pub fn set_selfavatar_timestamp( + context: &Context, + chat_id: u32, + timestamp: i64, +) -> Result<(), Error> { + context.sql.execute( + "UPDATE contacts + SET selfavatar_sent=? + WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=?);", + params![timestamp, chat_id], + )?; + Ok(()) +} + pub fn remove_contact_from_chat( context: &Context, chat_id: u32, @@ -2435,4 +2475,19 @@ mod tests { assert_eq!(chat2.name, chat.name); } + + #[test] + fn test_shall_attach_selfavatar() { + let t = dummy_context(); + let chat_id = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "foo").unwrap(); + assert!(!shall_attach_selfavatar(&t.ctx, chat_id).unwrap()); + + let (contact_id, _) = + Contact::add_or_lookup(&t.ctx, "", "foo@bar.org", Origin::IncomingUnknownTo).unwrap(); + add_contact_to_chat(&t.ctx, chat_id, contact_id); + assert!(shall_attach_selfavatar(&t.ctx, chat_id).unwrap()); + + assert!(set_selfavatar_timestamp(&t.ctx, chat_id, time()).is_ok()); + assert!(!shall_attach_selfavatar(&t.ctx, chat_id).unwrap()); + } } diff --git a/src/job.rs b/src/job.rs index 506ad861b..6115de4c9 100644 --- a/src/job.rs +++ b/src/job.rs @@ -633,7 +633,15 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<(), Error> { /* create message */ let needs_encryption = msg.param.get_int(Param::GuaranteeE2ee).unwrap_or_default(); - let mimefactory = MimeFactory::from_msg(context, &msg)?; + let attach_selfavatar = match chat::shall_attach_selfavatar(context, msg.chat_id) { + Ok(attach_selfavatar) => attach_selfavatar, + Err(err) => { + warn!(context, "job: cannot get selfavatar-state: {}", err); + false + } + }; + + let mimefactory = MimeFactory::from_msg(context, &msg, attach_selfavatar)?; let mut rendered_msg = mimefactory.render().map_err(|err| { message::set_msg_failed(context, msg_id, Some(err.to_string())); err @@ -672,6 +680,7 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<(), Error> { if rendered_msg.is_gossiped { chat::set_gossiped_timestamp(context, msg.chat_id, time()); } + if 0 != rendered_msg.last_added_location_id { if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()) { error!(context, "Failed to set kml sent_timestamp: {:?}", err); @@ -684,6 +693,13 @@ pub fn job_send_msg(context: &Context, msg_id: MsgId) -> Result<(), Error> { } } } + + if attach_selfavatar { + if let Err(err) = chat::set_selfavatar_timestamp(context, msg.chat_id, time()) { + error!(context, "Failed to set selfavatar timestamp: {:?}", err); + } + } + if rendered_msg.is_encrypted && needs_encryption == 0 { msg.param.set_int(Param::GuaranteeE2ee, 1); msg.save_param_to_disk(context); diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 65956e841..8f06e73e4 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1,6 +1,7 @@ use chrono::TimeZone; use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder}; +use crate::blob::BlobObject; use crate::chat::{self, Chat}; use crate::config::Config; use crate::constants::*; @@ -41,6 +42,7 @@ pub struct MimeFactory<'a, 'b> { pub req_mdn: bool, pub context: &'a Context, last_added_location_id: u32, + attach_selfavatar: bool, } /// Result of rendering a message, ready to be submitted to a send job. @@ -62,7 +64,11 @@ pub struct RenderedEmail { } impl<'a, 'b> MimeFactory<'a, 'b> { - pub fn from_msg(context: &'a Context, msg: &'b Message) -> Result, Error> { + pub fn from_msg( + context: &'a Context, + msg: &'b Message, + add_selfavatar: bool, + ) -> Result, Error> { let chat = Chat::load_from_db(context, msg.chat_id)?; let mut factory = MimeFactory { @@ -84,6 +90,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { references: String::default(), req_mdn: false, last_added_location_id: 0, + attach_selfavatar: add_selfavatar, context, }; @@ -210,6 +217,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { references: String::default(), req_mdn: false, last_added_location_id: 0, + attach_selfavatar: false, }) } @@ -873,6 +881,21 @@ impl<'a, 'b> MimeFactory<'a, 'b> { } } + if self.attach_selfavatar { + match context.get_config(Config::Selfavatar) { + Some(path) => match build_selfavatar_file(context, path) { + Ok((part, filename)) => { + parts.push(part); + protected_headers.push(Header::new("Chat-Profile-Image".into(), filename)) + } + Err(err) => warn!(context, "mimefactory: cannot attach selfavatar: {}", err), + }, + None => { + protected_headers.push(Header::new("Chat-Profile-Image".into(), "0".into())) + } + } + } + // Single part, render as regular message. if parts.len() == 1 { return Ok(parts.pop().unwrap()); @@ -1024,6 +1047,31 @@ fn build_body_file( Ok((mail, filename_to_send)) } +fn build_selfavatar_file(context: &Context, path: String) -> Result<(PartBuilder, String), Error> { + let blob = BlobObject::from_path(context, path)?; + let filename_to_send = match blob.suffix() { + Some(suffix) => format!("avatar.{}", suffix), + None => "avatar".to_string(), + }; + let mimetype = match message::guess_msgtype_from_suffix(blob.as_rel_path()) { + Some(res) => res.1.parse()?, + None => mime::APPLICATION_OCTET_STREAM, + }; + let body = std::fs::read(blob.to_abs_path())?; + let encoded_body = base64::encode(&body); + + let part = PartBuilder::new() + .content_type(&mimetype) + .header(( + "Content-Disposition", + format!("attachment; filename=\"{}\"", &filename_to_send), + )) + .header(("Content-Transfer-Encoding", "base64")) + .body(encoded_body); + + Ok((part, filename_to_send)) +} + pub(crate) fn vec_contains_lowercase(vec: &[String], part: &str) -> bool { let partlc = part.to_lowercase(); for cur in vec.iter() {