From c0f8bc1aedb31c32e92348a07fe5f9532cc7de8c Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sun, 9 Aug 2020 09:49:32 +0300 Subject: [PATCH] melib/email/attachments: add Content-Disposition --- melib/src/email/attachment_types.rs | 49 ++++++++++++++++++++++ melib/src/email/attachments.rs | 65 +++++++++++++++++++++++++---- melib/src/email/parser.rs | 55 +++++++++++++++++++++++- 3 files changed, 158 insertions(+), 11 deletions(-) diff --git a/melib/src/email/attachment_types.rs b/melib/src/email/attachment_types.rs index 788927ebd..cd9ff8175 100644 --- a/melib/src/email/attachment_types.rs +++ b/melib/src/email/attachment_types.rs @@ -336,3 +336,52 @@ impl From<&[u8]> for ContentTransferEncoding { } } } + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct ContentDisposition { + pub kind: ContentDispositionKind, + pub filename: Option, + pub creation_date: Option, + pub modification_date: Option, + pub read_date: Option, + pub size: Option, + pub parameter: Vec, +} + +#[derive(Clone, Debug, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub enum ContentDispositionKind { + Inline, + Attachment, +} + +impl ContentDispositionKind { + pub fn is_inline(&self) -> bool { + *self == ContentDispositionKind::Inline + } + + pub fn is_attachment(&self) -> bool { + *self == ContentDispositionKind::Attachment + } +} + +impl Default for ContentDispositionKind { + fn default() -> Self { + ContentDispositionKind::Inline + } +} + +impl Display for ContentDispositionKind { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + match *self { + ContentDispositionKind::Inline => write!(f, "inline"), + ContentDispositionKind::Attachment => write!(f, "attachment"), + } + } +} +impl From<&[u8]> for ContentDisposition { + fn from(val: &[u8]) -> ContentDisposition { + crate::email::parser::attachments::content_disposition(val) + .map(|(_, v)| v) + .unwrap_or_default() + } +} diff --git a/melib/src/email/attachments.rs b/melib/src/email/attachments.rs index ea85ca1ed..758f89e4f 100644 --- a/melib/src/email/attachments.rs +++ b/melib/src/email/attachments.rs @@ -32,6 +32,7 @@ pub use crate::email::attachment_types::*; pub struct AttachmentBuilder { pub content_type: ContentType, pub content_transfer_encoding: ContentTransferEncoding, + pub content_disposition: ContentDisposition, pub raw: Vec, pub body: StrBuilder, @@ -50,6 +51,7 @@ impl AttachmentBuilder { return AttachmentBuilder { content_type: Default::default(), content_transfer_encoding: ContentTransferEncoding::_7Bit, + content_disposition: ContentDisposition::default(), raw: content.to_vec(), body: StrBuilder { length: content.len(), @@ -74,6 +76,8 @@ impl AttachmentBuilder { builder.set_content_type_from_bytes(value); } else if name.eq_ignore_ascii_case(b"content-transfer-encoding") { builder.set_content_transfer_encoding(ContentTransferEncoding::from(value)); + } else if name.eq_ignore_ascii_case(b"content-disposition") { + builder.set_content_disposition(ContentDisposition::from(value)); } } builder @@ -116,6 +120,15 @@ impl AttachmentBuilder { self } + pub fn set_content_disposition(&mut self, val: ContentDisposition) -> &mut Self { + self.content_disposition = val; + self + } + + pub fn content_disposition(&self) -> &ContentDisposition { + &self.content_disposition + } + pub fn content_transfer_encoding(&self) -> &ContentTransferEncoding { &self.content_transfer_encoding } @@ -218,6 +231,7 @@ impl AttachmentBuilder { Attachment { content_type: self.content_type, content_transfer_encoding: self.content_transfer_encoding, + content_disposition: self.content_disposition, raw: self.raw, body: self.body, } @@ -257,6 +271,8 @@ impl AttachmentBuilder { builder.set_content_transfer_encoding(ContentTransferEncoding::from( value, )); + } else if name.eq_ignore_ascii_case(b"content-disposition") { + builder.set_content_disposition(ContentDisposition::from(value)); } } vec.push(builder.build()); @@ -280,12 +296,14 @@ impl From for AttachmentBuilder { fn from(val: Attachment) -> Self { let Attachment { content_type, + content_disposition, content_transfer_encoding, raw, body, } = val; AttachmentBuilder { content_type, + content_disposition, content_transfer_encoding, raw, body, @@ -298,12 +316,14 @@ impl From for Attachment { let AttachmentBuilder { content_type, content_transfer_encoding, + content_disposition, raw, body, } = val; Attachment { content_type, content_transfer_encoding, + content_disposition, raw, body, } @@ -315,6 +335,7 @@ impl From for Attachment { pub struct Attachment { pub content_type: ContentType, pub content_transfer_encoding: ContentTransferEncoding, + pub content_disposition: ContentDisposition, pub raw: Vec, pub body: StrBuilder, @@ -395,6 +416,7 @@ impl Attachment { ) -> Self { Attachment { content_type, + content_disposition: ContentDisposition::default(), content_transfer_encoding, body: StrBuilder { length: raw.len(), @@ -478,18 +500,22 @@ impl Attachment { } => match kind { MultipartType::Alternative => { for a in parts { - if let ContentType::Text { - kind: Text::Plain, .. - } = a.content_type - { - a.get_text_recursive(text); - break; + if a.content_disposition.kind.is_inline() { + if let ContentType::Text { + kind: Text::Plain, .. + } = a.content_type + { + a.get_text_recursive(text); + break; + } } } } _ => { for a in parts { - a.get_text_recursive(text) + if a.content_disposition.kind.is_inline() { + a.get_text_recursive(text); + } } } }, @@ -499,7 +525,7 @@ impl Attachment { pub fn text(&self) -> String { let mut text = Vec::with_capacity(self.body.length); self.get_text_recursive(&mut text); - String::from_utf8_lossy(text.as_slice().trim()).into() + String::from_utf8_lossy(text.as_slice()).into() } pub fn mime_type(&self) -> String { @@ -674,6 +700,25 @@ impl Attachment { ret } + + pub fn filename(&self) -> Option { + if self.content_disposition.kind.is_attachment() { + self.content_disposition.filename.clone() + } else { + None + } + .or_else(|| match &self.content_type { + ContentType::Text { parameters, .. } => parameters + .iter() + .find(|(h, _)| { + h.eq_ignore_ascii_case(b"name") | h.eq_ignore_ascii_case(b"filename") + }) + .map(|(_, v)| String::from_utf8_lossy(v).to_string()), + ContentType::Other { name, .. } | ContentType::OctetStream { name, .. } => name.clone(), + _ => None, + }) + .map(|n| n.replace(|c| std::path::is_separator(c) || c.is_ascii_control(), "_")) + } } pub fn interpret_format_flowed(_t: &str) -> String { @@ -729,7 +774,9 @@ fn decode_rec_helper<'a>(a: &'a Attachment, filter: &mut Option>) -> _ => { let mut vec = Vec::new(); for a in parts { - vec.extend(decode_rec_helper(a, filter)); + if a.content_disposition.kind.is_inline() { + vec.extend(decode_rec_helper(a, filter)); + } } vec } diff --git a/melib/src/email/parser.rs b/melib/src/email/parser.rs index 6f29fb8bb..27ab853dc 100644 --- a/melib/src/email/parser.rs +++ b/melib/src/email/parser.rs @@ -643,6 +643,7 @@ pub mod headers { pub mod attachments { use super::*; use crate::email::address::*; + use crate::email::attachment_types::{ContentDisposition, ContentDispositionKind}; pub fn attachment(input: &[u8]) -> IResult<&[u8], (std::vec::Vec<(&[u8], &[u8])>, &[u8])> { separated_pair( many0(headers::header), @@ -810,7 +811,7 @@ pub mod attachments { /* Caution: values should be passed through phrase() */ pub fn content_type_parameter(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> { - let (input, _) = tag(";")(input)?; + let (input, _) = tag(";")(input.ltrim())?; let (input, name) = terminated(take_until("="), tag("="))(input.ltrim())?; let (input, value) = alt(( delimited(tag("\""), take_until("\""), tag("\"")), @@ -821,7 +822,7 @@ pub mod attachments { } pub fn content_type(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8], Vec<(&[u8], &[u8])>)> { - let (input, _type) = take_until("/")(input)?; + let (input, _type) = take_until("/")(input.ltrim())?; let (input, _) = tag("/")(input)?; let (input, _subtype) = is_not(";")(input)?; let (input, parameters) = many0(content_type_parameter)(input)?; @@ -838,6 +839,56 @@ pub mod attachments { )); */ } + + /* Caution: values should be passed through phrase() */ + pub fn content_disposition_parameter(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> { + let (input, _) = tag(";")(input.ltrim())?; + let (input, name) = terminated(take_until("="), tag("="))(input.ltrim())?; + let (input, value) = alt(( + delimited(tag("\""), take_until("\""), tag("\"")), + is_not(";"), + ))(input.ltrim())?; + + Ok((input, (name, value))) + } + + pub fn content_disposition(input: &[u8]) -> IResult<&[u8], ContentDisposition> { + let (input, kind) = alt((take_until(";"), take_while(|_| true)))(input.trim())?; + let mut ret = ContentDisposition { + kind: if kind.trim().eq_ignore_ascii_case(b"attachment") { + ContentDispositionKind::Attachment + } else { + ContentDispositionKind::Inline + }, + ..ContentDisposition::default() + }; + if input.is_empty() { + return Ok((input, ret)); + } + let (input, parameters) = many0(content_disposition_parameter)(input.ltrim())?; + for (k, v) in parameters { + if k.eq_ignore_ascii_case(b"filename") { + ret.filename = + Some(String::from_utf8_lossy(&super::encodings::phrase(v, false)?.1).into()); + } else if k.eq_ignore_ascii_case(b"size") { + ret.size = + Some(String::from_utf8_lossy(&super::encodings::phrase(v, false)?.1).into()); + } else if k.eq_ignore_ascii_case(b"creation-date") { + ret.creation_date = + Some(String::from_utf8_lossy(&super::encodings::phrase(v, false)?.1).into()); + } else if k.eq_ignore_ascii_case(b"modification-date") { + ret.modification_date = + Some(String::from_utf8_lossy(&super::encodings::phrase(v, false)?.1).into()); + } else if k.eq_ignore_ascii_case(b"read-date") { + ret.read_date = + Some(String::from_utf8_lossy(&super::encodings::phrase(v, false)?.1).into()); + } else { + ret.parameter + .push(String::from_utf8_lossy(&super::encodings::phrase(v, false)?.1).into()); + } + } + Ok((input, ret)) + } } pub mod encodings {