From 40c6647db83c5137b79c9bec233972a8a78aeb76 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Fri, 9 Dec 2022 14:06:20 +0200 Subject: Fix multipart/related with main text/html part not displayed correctly --- melib/src/email/attachment_types.rs | 46 +++++++++------- melib/src/email/attachments.rs | 103 +++++++++++++++++++++++++++++------- melib/src/email/compose.rs | 24 ++++++++- melib/src/email/pgp.rs | 1 + src/components/mail/compose.rs | 3 +- src/components/mail/pgp.rs | 8 +-- src/components/mail/view.rs | 5 +- 7 files changed, 147 insertions(+), 43 deletions(-) diff --git a/melib/src/email/attachment_types.rs b/melib/src/email/attachment_types.rs index fbd303f5..b00f5a34 100644 --- a/melib/src/email/attachment_types.rs +++ b/melib/src/email/attachment_types.rs @@ -262,6 +262,7 @@ pub enum ContentType { }, Multipart { boundary: Vec, + parameters: Vec<(Vec, Vec)>, kind: MultipartType, parts: Vec, }, @@ -271,9 +272,11 @@ pub enum ContentType { Other { tag: Vec, name: Option, + parameters: Vec<(Vec, Vec)>, }, OctetStream { name: Option, + parameters: Vec<(Vec, Vec)>, }, } @@ -287,75 +290,79 @@ impl Default for ContentType { } } -impl PartialEq<&str> for ContentType { - fn eq(&self, other: &&str) -> bool { +impl PartialEq<&[u8]> for ContentType { + fn eq(&self, other: &&[u8]) -> bool { match (self, *other) { ( ContentType::Text { kind: Text::Plain, .. }, - "text/plain", + b"text/plain", ) => true, ( ContentType::Text { kind: Text::Html, .. }, - "text/html", + b"text/html", ) => true, ( ContentType::Multipart { kind: MultipartType::Alternative, .. }, - "multipart/alternative", + b"multipart/alternative", ) => true, ( ContentType::Multipart { kind: MultipartType::Digest, .. }, - "multipart/digest", + b"multipart/digest", ) => true, ( ContentType::Multipart { kind: MultipartType::Encrypted, .. }, - "multipart/encrypted", + b"multipart/encrypted", ) => true, ( ContentType::Multipart { kind: MultipartType::Mixed, .. }, - "multipart/mixed", + b"multipart/mixed", ) => true, ( ContentType::Multipart { kind: MultipartType::Related, .. }, - "multipart/related", + b"multipart/related", ) => true, ( ContentType::Multipart { kind: MultipartType::Signed, .. }, - "multipart/signed", + b"multipart/signed", ) => true, - (ContentType::PGPSignature, "application/pgp-signature") => true, - (ContentType::CMSSignature, "application/pkcs7-signature") => true, - (ContentType::MessageRfc822, "message/rfc822") => true, - (ContentType::Other { tag, .. }, _) => { - other.eq_ignore_ascii_case(&String::from_utf8_lossy(tag)) - } - (ContentType::OctetStream { .. }, "application/octet-stream") => true, + (ContentType::PGPSignature, b"application/pgp-signature") => true, + (ContentType::CMSSignature, b"application/pkcs7-signature") => true, + (ContentType::MessageRfc822, b"message/rfc822") => true, + (ContentType::Other { tag, .. }, _) => other.eq_ignore_ascii_case(tag), + (ContentType::OctetStream { .. }, b"application/octet-stream") => true, _ => false, } } } +impl PartialEq<&str> for ContentType { + fn eq(&self, other: &&str) -> bool { + self.eq(&other.as_bytes()) + } +} + impl Display for ContentType { fn fmt(&self, f: &mut Formatter) -> FmtResult { match self { @@ -424,7 +431,10 @@ impl ContentType { pub fn name(&self) -> Option<&str> { match self { ContentType::Other { ref name, .. } => name.as_ref().map(|n| n.as_ref()), - ContentType::OctetStream { ref name } => name.as_ref().map(|n| n.as_ref()), + ContentType::OctetStream { + ref name, + parameters: _, + } => name.as_ref().map(|n| n.as_ref()), _ => None, } } diff --git a/melib/src/email/attachments.rs b/melib/src/email/attachments.rs index 723d4ed3..c02d1a60 100644 --- a/melib/src/email/attachments.rs +++ b/melib/src/email/attachments.rs @@ -142,7 +142,7 @@ impl AttachmentBuilder { Ok((_, (ct, cst, params))) => { if ct.eq_ignore_ascii_case(b"multipart") { let mut boundary = None; - for (n, v) in params { + for (n, v) in ¶ms { if n.eq_ignore_ascii_case(b"boundary") { boundary = Some(v); break; @@ -155,6 +155,10 @@ impl AttachmentBuilder { self.content_type = ContentType::Multipart { boundary, kind: MultipartType::from(cst), + parameters: params + .into_iter() + .map(|(kb, vb)| (kb.to_vec(), vb.to_vec())) + .collect::, Vec)>>(), parts, }; } else { @@ -208,7 +212,7 @@ impl AttachmentBuilder { self.content_type = ContentType::CMSSignature; } else { let mut name: Option = None; - for (n, v) in params { + for (n, v) in ¶ms { if n.eq_ignore_ascii_case(b"name") { if let Ok(v) = crate::email::parser::encodings::phrase(v.trim(), false) .as_ref() @@ -225,7 +229,14 @@ impl AttachmentBuilder { tag.extend(ct); tag.push(b'/'); tag.extend(cst); - self.content_type = ContentType::Other { tag, name }; + self.content_type = ContentType::Other { + tag, + name, + parameters: params + .into_iter() + .map(|(kb, vb)| (kb.to_vec(), vb.to_vec())) + .collect::, Vec)>>(), + }; } } Err(e) => { @@ -556,11 +567,18 @@ impl Attachment { text.extend(self.decode(Default::default())); } ContentType::Multipart { - ref kind, + kind: MultipartType::Related, ref parts, + ref parameters, .. - } => match kind { - MultipartType::Alternative => { + } => { + if let Some(main_attachment) = parameters + .iter() + .find_map(|(k, v)| if k == b"type" { Some(v) } else { None }) + .and_then(|t| parts.iter().find(|a| a.content_type == t.as_slice())) + { + main_attachment.get_text_recursive(text); + } else { for a in parts { if a.content_disposition.kind.is_inline() { if let ContentType::Text { @@ -573,14 +591,33 @@ impl Attachment { } } } - _ => { - for a in parts { - if a.content_disposition.kind.is_inline() { + } + ContentType::Multipart { + kind: MultipartType::Alternative, + ref parts, + .. + } => { + for a in parts { + if a.content_disposition.kind.is_inline() { + if let ContentType::Text { + kind: Text::Plain, .. + } = a.content_type + { a.get_text_recursive(text); + break; } } } - }, + } + ContentType::Multipart { + kind: _, ref parts, .. + } => { + for a in parts { + if a.content_disposition.kind.is_inline() { + a.get_text_recursive(text); + } + } + } _ => {} } } @@ -646,7 +683,10 @@ impl Attachment { ref parts, .. } => parts.iter().all(Attachment::is_html), - + ContentType::Multipart { + kind: MultipartType::Related, + .. + } => false, ContentType::Multipart { ref parts, .. } => parts.iter().any(Attachment::is_html), _ => false, } @@ -709,12 +749,25 @@ impl Attachment { boundary, kind, parts, + parameters, } => { let boundary = String::from_utf8_lossy(boundary); ret.push_str(&format!("Content-Type: {}; boundary={}", kind, boundary)); if *kind == MultipartType::Signed { ret.push_str("; micalg=pgp-sha512; protocol=\"application/pgp-signature\""); } + for (n, v) in parameters { + ret.push_str("; "); + ret.push_str(&String::from_utf8_lossy(n)); + ret.push('='); + if v.contains(&b' ') { + ret.push('"'); + } + ret.push_str(&String::from_utf8_lossy(v)); + if v.contains(&b' ') { + ret.push('"'); + } + } ret.push_str("\r\n"); let boundary_start = format!("\r\n--{}\r\n", boundary); @@ -732,15 +785,25 @@ impl Attachment { ret.push_str(&format!("Content-Type: {}\r\n\r\n", a.content_type)); ret.push_str(&String::from_utf8_lossy(a.body())); } - ContentType::OctetStream { ref name } => { + ContentType::OctetStream { name, parameters } => { if let Some(name) = name { - ret.push_str(&format!( - "Content-Type: {}; name={}\r\n\r\n", - a.content_type, name - )); + ret.push_str(&format!("Content-Type: {}; name={}", a.content_type, name)); } else { - ret.push_str(&format!("Content-Type: {}\r\n\r\n", a.content_type)); + ret.push_str(&format!("Content-Type: {}", a.content_type)); + } + for (n, v) in parameters { + ret.push_str("; "); + ret.push_str(&String::from_utf8_lossy(n)); + ret.push('='); + if v.contains(&b' ') { + ret.push('"'); + } + ret.push_str(&String::from_utf8_lossy(v)); + if v.contains(&b' ') { + ret.push('"'); + } } + ret.push_str("\r\n\r\n"); ret.push_str(BASE64_MIME.encode(a.body()).trim()); } _ => { @@ -803,7 +866,10 @@ impl Attachment { match self.content_type { ContentType::Other { .. } => Vec::new(), ContentType::Text { .. } => self.decode_helper(options), - ContentType::OctetStream { ref name } => name + ContentType::OctetStream { + ref name, + parameters: _, + } => name .clone() .unwrap_or_else(|| self.mime_type()) .into_bytes(), @@ -820,6 +886,7 @@ impl Attachment { ContentType::Multipart { ref kind, ref parts, + parameters: _, .. } => match kind { MultipartType::Alternative => { diff --git a/melib/src/email/compose.rs b/melib/src/email/compose.rs index 41585515..924431b8 100644 --- a/melib/src/email/compose.rs +++ b/melib/src/email/compose.rs @@ -335,14 +335,19 @@ impl Draft { parts.push(body_attachment); } parts.extend(attachments.into_iter()); - build_multipart(&mut ret, MultipartType::Mixed, parts); + build_multipart(&mut ret, MultipartType::Mixed, &[], parts); } Ok(ret) } } -fn build_multipart(ret: &mut String, kind: MultipartType, parts: Vec) { +fn build_multipart( + ret: &mut String, + kind: MultipartType, + parameters: &[(Vec, Vec)], + parts: Vec, +) { let boundary = ContentType::make_boundary(&parts); ret.push_str(&format!( r#"Content-Type: {}; charset="utf-8"; boundary="{}""#, @@ -351,6 +356,18 @@ fn build_multipart(ret: &mut String, kind: MultipartType, parts: Vec { build_multipart( ret, kind, + ¶meters, parts .into_iter() .map(|s| s.into()) @@ -578,6 +597,7 @@ where } else { b"application/octet-stream".to_vec() }, + parameters: vec![], }); Ok(attachment) diff --git a/melib/src/email/pgp.rs b/melib/src/email/pgp.rs index c28ac0b9..d17e7259 100644 --- a/melib/src/email/pgp.rs +++ b/melib/src/email/pgp.rs @@ -93,6 +93,7 @@ pub fn verify_signature(a: &Attachment) -> Result<(Vec, &Attachment)> { kind: MultipartType::Signed, ref parts, boundary: _, + parameters: _, } => { if parts.len() != 2 { return Err(Error::new(format!( diff --git a/src/components/mail/compose.rs b/src/components/mail/compose.rs index b2362964..331ce968 100644 --- a/src/components/mail/compose.rs +++ b/src/components/mail/compose.rs @@ -2305,9 +2305,10 @@ pub fn send_draft_async( boundary: boundary.into_bytes(), kind: MultipartType::Mixed, parts: parts.into_iter().map(|a| a.into()).collect::>(), + parameters: vec![], }, Default::default(), - Vec::new(), + vec![], ) .into(); } diff --git a/src/components/mail/pgp.rs b/src/components/mail/pgp.rs index 7fce3f7f..ccdadff0 100644 --- a/src/components/mail/pgp.rs +++ b/src/components/mail/pgp.rs @@ -71,9 +71,10 @@ pub fn sign_filter( boundary: boundary.into_bytes(), kind: MultipartType::Signed, parts: parts.into_iter().map(|a| a.into()).collect::>(), + parameters: vec![], }, Default::default(), - Vec::new(), + vec![], ) .into()) }) @@ -100,7 +101,7 @@ pub fn encrypt_filter( let sig_attachment = { let mut a = Attachment::new( - ContentType::OctetStream { name: None }, + ContentType::OctetStream { name: None, parameters: vec![] }, Default::default(), ctx.encrypt(sign_keys, encrypt_keys, data)?.await?, ); @@ -117,9 +118,10 @@ pub fn encrypt_filter( boundary: boundary.into_bytes(), kind: MultipartType::Encrypted, parts: parts.into_iter().map(|a| a.into()).collect::>(), + parameters: vec![], }, Default::default(), - Vec::new(), + vec![], ) .into()) }) diff --git a/src/components/mail/view.rs b/src/components/mail/view.rs index 84e56228..9efb2e4c 100644 --- a/src/components/mail/view.rs +++ b/src/components/mail/view.rs @@ -2263,7 +2263,10 @@ impl Component for MailView { )); } } - ContentType::OctetStream { ref name } => { + ContentType::OctetStream { + ref name, + parameters: _, + } => { context.replies.push_back(UIEvent::StatusEvent( StatusEvent::DisplayMessage(format!( "Failed to open {}. application/octet-stream isn't supported yet", -- cgit v1.2.3