From f73dbd22440a27694322f1c5e9f70dcd3d2a4f8c Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 14 Feb 2021 14:48:55 +0300 Subject: [PATCH 1/7] Better comments for protected_headers and unprotected_headers Make it clear that protected_headers are protected only opportunistically and will go into IMF header section if the message is not encrypted. --- src/mimefactory.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 82171e1c01..e4a8d964c3 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -409,13 +409,22 @@ impl<'a> MimeFactory<'a> { } pub async fn render(mut self, context: &Context) -> Result { - // Headers that are encrypted - // - Chat-*, except Chat-Version - // - Secure-Join* - // - Subject + // Opportunistically protected headers. + // + // These headers are placed into encrypted part *if* the message is encrypted. Place headers + // which are not needed before decryption (e.g. Chat-Group-Name) or are not interesting if + // the message cannot be decrypted (e.g. Chat-Disposition-Notification-To) here. + // + // If the message is not encrypted, these headers are placed into IMF header section, so + // make sure that the message will be encrypted if you place any sensitive information here. let mut protected_headers: Vec
= Vec::new(); - // All other headers + // Headers that must go into IMF header section. + // + // These are standard headers such as Date, In-Reply-To, References, which cannot be placed + // anywhere else according to the standard. Placing headers here also allows them to be + // fetched individually over IMAP without downloading the message body. This is why + // Chat-Version is placed here. let mut unprotected_headers: Vec
= Vec::new(); let from = Address::new_mailbox_with_name( From 2d8dfff7f0e4f2e1b862739cb4a2a1646aff994d Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 14 Feb 2021 16:57:48 +0300 Subject: [PATCH 2/7] mimefactory: create MessageHeaders structure --- src/mimefactory.rs | 182 ++++++++++++++++++++++++++++----------------- 1 file changed, 112 insertions(+), 70 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index e4a8d964c3..514cb3042c 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -83,6 +83,27 @@ pub struct RenderedEmail { pub subject: String, } +#[derive(Debug, Clone, Default)] +struct MessageHeaders { + /// Opportunistically protected headers. + /// + /// These headers are placed into encrypted part *if* the message is encrypted. Place headers + /// which are not needed before decryption (e.g. Chat-Group-Name) or are not interesting if the + /// message cannot be decrypted (e.g. Chat-Disposition-Notification-To) here. + /// + /// If the message is not encrypted, these headers are placed into IMF header section, so make + /// sure that the message will be encrypted if you place any sensitive information here. + pub protected: Vec
, + + /// Headers that must go into IMF header section. + /// + /// These are standard headers such as Date, In-Reply-To, References, which cannot be placed + /// anywhere else according to the standard. Placing headers here also allows them to be fetched + /// individually over IMAP without downloading the message body. This is why Chat-Version is + /// placed here. + pub unprotected: Vec
, +} + impl<'a> MimeFactory<'a> { pub async fn from_msg( context: &Context, @@ -409,23 +430,7 @@ impl<'a> MimeFactory<'a> { } pub async fn render(mut self, context: &Context) -> Result { - // Opportunistically protected headers. - // - // These headers are placed into encrypted part *if* the message is encrypted. Place headers - // which are not needed before decryption (e.g. Chat-Group-Name) or are not interesting if - // the message cannot be decrypted (e.g. Chat-Disposition-Notification-To) here. - // - // If the message is not encrypted, these headers are placed into IMF header section, so - // make sure that the message will be encrypted if you place any sensitive information here. - let mut protected_headers: Vec
= Vec::new(); - - // Headers that must go into IMF header section. - // - // These are standard headers such as Date, In-Reply-To, References, which cannot be placed - // anywhere else according to the standard. Placing headers here also allows them to be - // fetched individually over IMAP without downloading the message body. This is why - // Chat-Version is placed here. - let mut unprotected_headers: Vec
= Vec::new(); + let mut headers: MessageHeaders = Default::default(); let from = Address::new_mailbox_with_name( self.from_displayname.to_string(), @@ -448,14 +453,20 @@ impl<'a> MimeFactory<'a> { to.push(from.clone()); } - unprotected_headers.push(Header::new("MIME-Version".into(), "1.0".into())); + headers + .unprotected + .push(Header::new("MIME-Version".into(), "1.0".into())); if !self.references.is_empty() { - unprotected_headers.push(Header::new("References".into(), self.references.clone())); + headers + .unprotected + .push(Header::new("References".into(), self.references.clone())); } if !self.in_reply_to.is_empty() { - unprotected_headers.push(Header::new("In-Reply-To".into(), self.in_reply_to.clone())); + headers + .unprotected + .push(Header::new("In-Reply-To".into(), self.in_reply_to.clone())); } let date = chrono::Utc @@ -463,12 +474,14 @@ impl<'a> MimeFactory<'a> { .unwrap() .to_rfc2822(); - unprotected_headers.push(Header::new("Date".into(), date)); + headers.unprotected.push(Header::new("Date".into(), date)); - unprotected_headers.push(Header::new("Chat-Version".to_string(), "1.0".to_string())); + headers + .unprotected + .push(Header::new("Chat-Version".to_string(), "1.0".to_string())); if let Loaded::Mdn { .. } = self.loaded { - unprotected_headers.push(Header::new( + headers.unprotected.push(Header::new( "Auto-Submitted".to_string(), "auto-replied".to_string(), )); @@ -478,7 +491,7 @@ impl<'a> MimeFactory<'a> { // we use "Chat-Disposition-Notification-To" // because replies to "Disposition-Notification-To" are weird in many cases // eg. are just freetext and/or do not follow any standard. - protected_headers.push(Header::new( + headers.protected.push(Header::new( "Chat-Disposition-Notification-To".into(), self.from_addr.clone(), )); @@ -506,10 +519,14 @@ impl<'a> MimeFactory<'a> { if !skip_autocrypt { // unless determined otherwise we add the Autocrypt header let aheader = encrypt_helper.get_aheader().to_string(); - unprotected_headers.push(Header::new("Autocrypt".into(), aheader)); + headers + .unprotected + .push(Header::new("Autocrypt".into(), aheader)); } - protected_headers.push(Header::new("Subject".into(), encoded_subject)); + headers + .protected + .push(Header::new("Subject".into(), encoded_subject)); let rfc724_mid = match self.loaded { Loaded::Message { .. } => self.msg.rfc724_mid.clone(), @@ -518,23 +535,28 @@ impl<'a> MimeFactory<'a> { let ephemeral_timer = self.msg.chat_id.get_ephemeral_timer(context).await?; if let EphemeralTimer::Enabled { duration } = ephemeral_timer { - protected_headers.push(Header::new( + headers.protected.push(Header::new( "Ephemeral-Timer".to_string(), duration.to_string(), )); } - unprotected_headers.push(Header::new( + headers.unprotected.push(Header::new( "Message-ID".into(), render_rfc724_mid(&rfc724_mid), )); - unprotected_headers.push(Header::new_with_value("To".into(), to).unwrap()); - unprotected_headers.push(Header::new_with_value("From".into(), vec![from]).unwrap()); + headers + .unprotected + .push(Header::new_with_value("To".into(), to).unwrap()); + headers + .unprotected + .push(Header::new_with_value("From".into(), vec![from]).unwrap()); if let Some(sender_displayname) = &self.sender_displayname { let sender = Address::new_mailbox_with_name(sender_displayname.clone(), self.from_addr.clone()); - unprotected_headers + headers + .unprotected .push(Header::new_with_value("Sender".into(), vec![sender]).unwrap()); } @@ -542,13 +564,8 @@ impl<'a> MimeFactory<'a> { let (main_part, parts) = match self.loaded { Loaded::Message { .. } => { - self.render_message( - context, - &mut protected_headers, - &mut unprotected_headers, - &grpimage, - ) - .await? + self.render_message(context, &mut headers, &grpimage) + .await? } Loaded::Mdn { .. } => (self.render_mdn(context).await?, Vec::new()), }; @@ -572,7 +589,8 @@ impl<'a> MimeFactory<'a> { }; // Store protected headers in the inner message. - let mut message = protected_headers + let mut message = headers + .protected .into_iter() .fold(message, |message, header| message.header(header)); @@ -611,7 +629,8 @@ impl<'a> MimeFactory<'a> { )); // Store the unprotected headers on the outer message. - let outer_message = unprotected_headers + let outer_message = headers + .unprotected .into_iter() .fold(outer_message, |message, header| message.header(header)); @@ -649,7 +668,8 @@ impl<'a> MimeFactory<'a> { ) .header(("Subject".to_string(), "...".to_string())) } else { - unprotected_headers + headers + .unprotected .into_iter() .fold(message, |message, header| message.header(header)) }; @@ -714,8 +734,7 @@ impl<'a> MimeFactory<'a> { async fn render_message( &mut self, context: &Context, - protected_headers: &mut Vec
, - unprotected_headers: &mut Vec
, + headers: &mut MessageHeaders, grpimage: &Option, ) -> Result<(PartBuilder, Vec)> { let chat = match &self.loaded { @@ -727,20 +746,26 @@ impl<'a> MimeFactory<'a> { let mut meta_part = None; if chat.is_protected() { - protected_headers.push(Header::new("Chat-Verified".to_string(), "1".to_string())); + headers + .protected + .push(Header::new("Chat-Verified".to_string(), "1".to_string())); } if chat.typ == Chattype::Group { - protected_headers.push(Header::new("Chat-Group-ID".into(), chat.grpid.clone())); + headers + .protected + .push(Header::new("Chat-Group-ID".into(), chat.grpid.clone())); let encoded = encode_words(&chat.name); - protected_headers.push(Header::new("Chat-Group-Name".into(), encoded)); + headers + .protected + .push(Header::new("Chat-Group-Name".into(), encoded)); match command { SystemMessage::MemberRemovedFromGroup => { let email_to_remove = self.msg.param.get(Param::Arg).unwrap_or_default(); if !email_to_remove.is_empty() { - protected_headers.push(Header::new( + headers.protected.push(Header::new( "Chat-Group-Member-Removed".into(), email_to_remove.into(), )); @@ -749,7 +774,7 @@ impl<'a> MimeFactory<'a> { SystemMessage::MemberAddedToGroup => { let email_to_add = self.msg.param.get(Param::Arg).unwrap_or_default(); if !email_to_add.is_empty() { - protected_headers.push(Header::new( + headers.protected.push(Header::new( "Chat-Group-Member-Added".into(), email_to_add.into(), )); @@ -762,7 +787,7 @@ impl<'a> MimeFactory<'a> { "sending secure-join message \'{}\' >>>>>>>>>>>>>>>>>>>>>>>>>", "vg-member-added", ); - protected_headers.push(Header::new( + headers.protected.push(Header::new( "Secure-Join".to_string(), "vg-member-added".to_string(), )); @@ -770,18 +795,18 @@ impl<'a> MimeFactory<'a> { } SystemMessage::GroupNameChanged => { let old_name = self.msg.param.get(Param::Arg).unwrap_or_default(); - protected_headers.push(Header::new( + headers.protected.push(Header::new( "Chat-Group-Name-Changed".into(), maybe_encode_words(old_name), )); } SystemMessage::GroupImageChanged => { - protected_headers.push(Header::new( + headers.protected.push(Header::new( "Chat-Content".to_string(), "group-avatar-changed".to_string(), )); if grpimage.is_none() { - protected_headers.push(Header::new( + headers.protected.push(Header::new( "Chat-Group-Avatar".to_string(), "0".to_string(), )); @@ -793,13 +818,13 @@ impl<'a> MimeFactory<'a> { match command { SystemMessage::LocationStreamingEnabled => { - protected_headers.push(Header::new( + headers.protected.push(Header::new( "Chat-Content".into(), "location-streaming-enabled".into(), )); } SystemMessage::EphemeralTimerChanged => { - protected_headers.push(Header::new( + headers.protected.push(Header::new( "Chat-Content".to_string(), "ephemeral-timer-changed".to_string(), )); @@ -813,13 +838,14 @@ impl<'a> MimeFactory<'a> { // Adding this header without encryption leaks some // information about the message contents, but it can // already be easily guessed from message timing and size. - unprotected_headers.push(Header::new( + headers.unprotected.push(Header::new( "Auto-Submitted".to_string(), "auto-generated".to_string(), )); } SystemMessage::AutocryptSetupMessage => { - unprotected_headers + headers + .unprotected .push(Header::new("Autocrypt-Setup-Message".into(), "v1".into())); placeholdertext = Some(stock_str::ac_setup_msg_body(context).await); @@ -832,11 +858,13 @@ impl<'a> MimeFactory<'a> { context, "sending secure-join message \'{}\' >>>>>>>>>>>>>>>>>>>>>>>>>", step, ); - protected_headers.push(Header::new("Secure-Join".into(), step.into())); + headers + .protected + .push(Header::new("Secure-Join".into(), step.into())); let param2 = msg.param.get(Param::Arg2).unwrap_or_default(); if !param2.is_empty() { - protected_headers.push(Header::new( + headers.protected.push(Header::new( if step == "vg-request-with-auth" || step == "vc-request-with-auth" { "Secure-Join-Auth".into() } else { @@ -848,24 +876,26 @@ impl<'a> MimeFactory<'a> { let fingerprint = msg.param.get(Param::Arg3).unwrap_or_default(); if !fingerprint.is_empty() { - protected_headers.push(Header::new( + headers.protected.push(Header::new( "Secure-Join-Fingerprint".into(), fingerprint.into(), )); } if let Some(id) = msg.param.get(Param::Arg4) { - protected_headers.push(Header::new("Secure-Join-Group".into(), id.into())); + headers + .protected + .push(Header::new("Secure-Join-Group".into(), id.into())); }; } } SystemMessage::ChatProtectionEnabled => { - protected_headers.push(Header::new( + headers.protected.push(Header::new( "Chat-Content".to_string(), "protection-enabled".to_string(), )); } SystemMessage::ChatProtectionDisabled => { - protected_headers.push(Header::new( + headers.protected.push(Header::new( "Chat-Content".to_string(), "protection-disabled".to_string(), )); @@ -883,17 +913,21 @@ impl<'a> MimeFactory<'a> { let (mail, filename_as_sent) = build_body_file(context, &meta, "group-image").await?; meta_part = Some(mail); - protected_headers.push(Header::new("Chat-Group-Avatar".into(), filename_as_sent)); + headers + .protected + .push(Header::new("Chat-Group-Avatar".into(), filename_as_sent)); } if self.msg.viewtype == Viewtype::Sticker { - protected_headers.push(Header::new("Chat-Content".into(), "sticker".into())); + headers + .protected + .push(Header::new("Chat-Content".into(), "sticker".into())); } else if self.msg.viewtype == Viewtype::VideochatInvitation { - protected_headers.push(Header::new( + headers.protected.push(Header::new( "Chat-Content".into(), "videochat-invitation".into(), )); - protected_headers.push(Header::new( + headers.protected.push(Header::new( "Chat-Webrtc-Room".into(), self.msg .param @@ -908,12 +942,16 @@ impl<'a> MimeFactory<'a> { || self.msg.viewtype == Viewtype::Video { if self.msg.viewtype == Viewtype::Voice { - protected_headers.push(Header::new("Chat-Voice-Message".into(), "1".into())); + headers + .protected + .push(Header::new("Chat-Voice-Message".into(), "1".into())); } let duration_ms = self.msg.param.get_int(Param::Duration).unwrap_or_default(); if duration_ms > 0 { let dur = duration_ms.to_string(); - protected_headers.push(Header::new("Chat-Duration".into(), dur)); + headers + .protected + .push(Header::new("Chat-Duration".into(), dur)); } } @@ -1026,11 +1064,15 @@ impl<'a> MimeFactory<'a> { Some(path) => match build_selfavatar_file(context, &path) { Ok((part, filename)) => { parts.push(part); - protected_headers.push(Header::new("Chat-User-Avatar".into(), filename)) + headers + .protected + .push(Header::new("Chat-User-Avatar".into(), filename)) } Err(err) => warn!(context, "mimefactory: cannot attach selfavatar: {}", err), }, - None => protected_headers.push(Header::new("Chat-User-Avatar".into(), "0".into())), + None => headers + .protected + .push(Header::new("Chat-User-Avatar".into(), "0".into())), } } From ff8ca3808d442da8c3a0f9bf1be225640d73fa65 Mon Sep 17 00:00:00 2001 From: link2xt Date: Sun, 14 Feb 2021 18:45:01 +0300 Subject: [PATCH 3/7] mimefactory: implement hidden headers --- src/mimefactory.rs | 53 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 514cb3042c..8a9b0b2450 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -102,6 +102,12 @@ struct MessageHeaders { /// individually over IMAP without downloading the message body. This is why Chat-Version is /// placed here. pub unprotected: Vec
, + + /// Headers that MUST NOT go into IMF header section. + /// + /// These are large headers which may hit the header section size limit on the server, such as + /// Chat-User-Avatar with a base64-encoded image inside. + pub hidden: Vec
, } impl<'a> MimeFactory<'a> { @@ -588,13 +594,19 @@ impl<'a> MimeFactory<'a> { ) }; - // Store protected headers in the inner message. - let mut message = headers - .protected - .into_iter() - .fold(message, |message, header| message.header(header)); - let outer_message = if is_encrypted { + // Store protected headers in the inner message. + let message = headers + .protected + .into_iter() + .fold(message, |message, header| message.header(header)); + + // Add hidden headers to encrypted payload. + let mut message = headers + .hidden + .into_iter() + .fold(message, |message, header| message.header(header)); + // Add gossip headers in chats with multiple recipients if peerstates.len() > 1 && self.should_do_gossip(context).await? { for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) { @@ -628,12 +640,6 @@ impl<'a> MimeFactory<'a> { "multipart/encrypted; protocol=\"application/pgp-encrypted\"".to_string(), )); - // Store the unprotected headers on the outer message. - let outer_message = headers - .unprotected - .into_iter() - .fold(outer_message, |message, header| message.header(header)); - if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { info!(context, "mimefactory: outgoing message mime:"); let raw_message = message.clone().build().as_string(); @@ -668,12 +674,33 @@ impl<'a> MimeFactory<'a> { ) .header(("Subject".to_string(), "...".to_string())) } else { + let message = if headers.hidden.is_empty() { + message + } else { + // Store hidden headers in the inner unencrypted message. + let message = headers + .hidden + .into_iter() + .fold(message, |message, header| message.header(header)); + + PartBuilder::new() + .message_type(MimeMultipartType::Mixed) + .child(message.build()) + }; + + // Store protected headers in the outer message. headers - .unprotected + .protected .into_iter() .fold(message, |message, header| message.header(header)) }; + // Store the unprotected headers on the outer message. + let outer_message = headers + .unprotected + .into_iter() + .fold(outer_message, |message, header| message.header(header)); + let MimeFactory { last_added_location_id, .. From 5248929aef84a7ab2594d4a98f8df28d7eff93bf Mon Sep 17 00:00:00 2001 From: link2xt Date: Tue, 16 Feb 2021 21:55:18 +0300 Subject: [PATCH 4/7] mimeparser: parse hidden headers --- src/mimeparser.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 33edf152bf..23bcc07896 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -149,7 +149,7 @@ impl MimeMessage { let mut from = Default::default(); let mut chat_disposition_notification_to = None; - // init known headers with what mailparse provided us + // Parse IMF headers. MimeMessage::merge_headers( context, &mut headers, @@ -159,6 +159,21 @@ impl MimeMessage { &mail.headers, ); + // Parse hidden headers. + let mimetype = mail.ctype.mimetype.parse::()?; + if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "mixed" { + if let Some(part) = mail.subparts.first() { + for field in &part.headers { + let key = field.get_key().to_lowercase(); + + // For now only Chat-User-Avatar can be hidden. + if !headers.contains_key(&key) && key == "chat-user-avatar" { + headers.insert(key.to_string(), field.get_value()); + } + } + } + } + // remove headers that are allowed _only_ in the encrypted part headers.remove("secure-join-fingerprint"); headers.remove("chat-verified"); From 901ea0c89a2ddb30d8f0c0fe1a5f10605971fc4f Mon Sep 17 00:00:00 2001 From: link2xt Date: Wed, 17 Feb 2021 01:28:33 +0300 Subject: [PATCH 5/7] Process Chat-User-Avatar headers with embedded base64 images --- src/mimeparser.rs | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 23bcc07896..14582d12c3 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -278,7 +278,7 @@ impl MimeMessage { parser.maybe_remove_bad_parts(); parser.maybe_remove_inline_mailinglist_footer(); parser.heuristically_parse_ndn(context).await; - parser.parse_headers(context); + parser.parse_headers(context).await; if warn_empty_signature && parser.signatures.is_empty() { for part in parser.parts.iter_mut() { @@ -325,13 +325,13 @@ impl MimeMessage { } /// Parses avatar action headers. - fn parse_avatar_headers(&mut self) { + async fn parse_avatar_headers(&mut self, context: &Context) { if let Some(header_value) = self.get(HeaderDef::ChatGroupAvatar).cloned() { - self.group_avatar = self.avatar_action_from_header(header_value); + self.group_avatar = self.avatar_action_from_header(context, header_value).await; } if let Some(header_value) = self.get(HeaderDef::ChatUserAvatar).cloned() { - self.user_avatar = self.avatar_action_from_header(header_value); + self.user_avatar = self.avatar_action_from_header(context, header_value).await; } } @@ -421,9 +421,9 @@ impl MimeMessage { } } - fn parse_headers(&mut self, context: &Context) { + async fn parse_headers(&mut self, context: &Context) { self.parse_system_message_headers(context); - self.parse_avatar_headers(); + self.parse_avatar_headers(context).await; self.parse_videochat_headers(); self.squash_attachment_parts(); @@ -500,10 +500,37 @@ impl MimeMessage { } } - fn avatar_action_from_header(&mut self, header_value: String) -> Option { + async fn avatar_action_from_header( + &mut self, + context: &Context, + header_value: String, + ) -> Option { if header_value == "0" { Some(AvatarAction::Delete) + } else if let Some(avatar) = header_value + .split_ascii_whitespace() + .collect::() + .strip_prefix("base64:") + .map(base64::decode) + { + // Avatar sent directly in the header as base64. + if let Ok(decoded_data) = avatar { + match BlobObject::create(context, "avatar", &decoded_data).await { + Ok(blob) => Some(AvatarAction::Change(blob.as_name().to_string())), + Err(err) => { + warn!( + context, + "Could not save decoded avatar to blob file: {}", err + ); + None + } + } + } else { + None + } } else { + // Avatar sent in attachment, as previous versions of Delta Chat did. + let mut i = 0; while let Some(part) = self.parts.get_mut(i) { if let Some(part_filename) = &part.org_filename { From 6b93b5a1352f25a3874d2b167fce8ef990a18219 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 18 Feb 2021 21:14:21 +0300 Subject: [PATCH 6/7] base64-encode avatar into the hidden header --- src/mimefactory.rs | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 8a9b0b2450..846233d725 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1089,12 +1089,10 @@ impl<'a> MimeFactory<'a> { if self.attach_selfavatar { match context.get_config(Config::Selfavatar).await? { Some(path) => match build_selfavatar_file(context, &path) { - Ok((part, filename)) => { - parts.push(part); - headers - .protected - .push(Header::new("Chat-User-Avatar".into(), filename)) - } + Ok(avatar) => headers.hidden.push(Header::new( + "Chat-User-Avatar".into(), + format!("base64:{}", avatar), + )), Err(err) => warn!(context, "mimefactory: cannot attach selfavatar: {}", err), }, None => headers @@ -1279,29 +1277,11 @@ async fn build_body_file( Ok((mail, filename_to_send)) } -fn build_selfavatar_file(context: &Context, path: &str) -> Result<(PartBuilder, String)> { +fn build_selfavatar_file(context: &Context, path: &str) -> Result { 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 = wrapped_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)) + Ok(encoded_body) } fn recipients_contain_addr(recipients: &[(String, String)], addr: &str) -> bool { From 357eb8d3fabb3b917b5f2fa596decfdbe2a8f9e6 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Wed, 21 Apr 2021 18:03:12 +0200 Subject: [PATCH 7/7] test that the correct headers are moved --- src/mimefactory.rs | 59 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 846233d725..33492cc942 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1345,14 +1345,16 @@ fn maybe_encode_words(words: &str) -> String { #[cfg(test)] mod tests { use super::*; - use crate::chat::ChatId; + use async_std::prelude::*; + use crate::chat::ChatId; use crate::contact::Origin; use crate::dc_receive_imf::dc_receive_imf; use crate::mimeparser::MimeMessage; use crate::test_utils::TestContext; use crate::{chatlist::Chatlist, test_utils::get_chat_msg}; + use async_std::fs::File; use pretty_assertions::assert_eq; #[test] @@ -1903,4 +1905,59 @@ mod tests { assert!(!headers.lines().any(|l| l.trim().is_empty())); } + + #[async_std::test] + async fn test_selfavatar_unencrypted() -> anyhow::Result<()> { + // create chat with bob, set selfavatar + let t = TestContext::new_alice().await; + let chat = t.create_chat_with_contact("bob", "bob@example.org").await; + + let file = t.dir.path().join("avatar.png"); + let bytes = include_bytes!("../test-data/image/avatar64x64.png"); + File::create(&file).await?.write_all(bytes).await?; + t.set_config(Config::Selfavatar, Some(file.to_str().unwrap())) + .await?; + + // send message to bob: that should get multipart/mixed because of the avatar moved to inner header; + // make sure, `Subject:` stays in the outer header (imf header) + let mut msg = Message::new(Viewtype::Text); + msg.set_text(Some("this is the text!".to_string())); + + let payload = t.send_msg(chat.id, &mut msg).await.payload(); + let mut payload = payload.splitn(3, "\r\n\r\n"); + let outer = payload.next().unwrap(); + let inner = payload.next().unwrap(); + let body = payload.next().unwrap(); + + assert_eq!(outer.match_indices("multipart/mixed").count(), 1); + assert_eq!(outer.match_indices("Subject:").count(), 1); + assert_eq!(outer.match_indices("Autocrypt:").count(), 1); + assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0); + + assert_eq!(inner.match_indices("text/plain").count(), 1); + assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 1); + assert_eq!(inner.match_indices("Subject:").count(), 0); + + assert_eq!(body.match_indices("this is the text!").count(), 1); + + // if another message is sent, that one must not contain the avatar + // and no artificial multipart/mixed nesting + let payload = t.send_msg(chat.id, &mut msg).await.payload(); + let mut payload = payload.splitn(2, "\r\n\r\n"); + let outer = payload.next().unwrap(); + let body = payload.next().unwrap(); + + assert_eq!(outer.match_indices("text/plain").count(), 1); + assert_eq!(outer.match_indices("Subject:").count(), 1); + assert_eq!(outer.match_indices("Autocrypt:").count(), 1); + assert_eq!(outer.match_indices("multipart/mixed").count(), 0); + assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0); + + assert_eq!(body.match_indices("this is the text!").count(), 1); + assert_eq!(body.match_indices("text/plain").count(), 0); + assert_eq!(body.match_indices("Chat-User-Avatar:").count(), 0); + assert_eq!(body.match_indices("Subject:").count(), 0); + + Ok(()) + } }