diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index 9506ff229..9a391f714 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -47,12 +47,10 @@ pub fn dc_receive_imf( server_uid, ); - /* - if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { + if std::env::var(crate::DCC_MIME_DEBUG).unwrap_or_default() == "2" { info!(context, "dc_receive_imf: incoming message mime-body:"); println!("{}", String::from_utf8_lossy(imf_raw)); } - */ let mime_parser = MimeParser::from_bytes(context, imf_raw); let mut mime_parser = if let Err(err) = mime_parser { diff --git a/src/e2ee.rs b/src/e2ee.rs index b38ff48a0..85ad751e4 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -247,13 +247,13 @@ fn decrypt_if_autocrypt_message<'a>( // Errors are returned for failures related to decryption of AC-messages. let encrypted_data_part = match wrapmime::get_autocrypt_mime(mail) { - Err(err) => { - // not a proper autocrypt message, abort and ignore - info!(context, "Not an autocrypt message: {:?}", err); + Err(_) => { + // not an autocrypt mime message, abort and ignore return Ok(None); } Ok(res) => res, }; + info!(context, "Detected Autocrypt-mime message"); decrypt_part( context, diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 8e5df288a..2a43aef44 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; use deltachat_derive::{FromSql, ToSql}; use lettre_email::mime::{self, Mime}; -use mailparse::MailHeaderMap; +use mailparse::{DispositionType, MailHeaderMap}; use crate::aheader::Aheader; use crate::blob::BlobObject; @@ -22,6 +22,7 @@ use crate::param::*; use crate::peerstate::Peerstate; use crate::securejoin::handle_degrade_event; use crate::stock::StockMessage; +use crate::{bail, ensure}; #[derive(Debug)] pub struct MimeParser<'a> { @@ -110,7 +111,7 @@ impl<'a> MimeParser<'a> { mail_raw = raw; let decrypted_mail = mailparse::parse_mail(&mail_raw)?; if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { - info!(context, "dc_receive_imf: incoming message mime-body:"); + info!(context, "decrypted message mime-body:"); println!("{}", String::from_utf8_lossy(&mail_raw)); } @@ -509,100 +510,63 @@ impl<'a> MimeParser<'a> { let (mime_type, msg_type) = get_mime_type(mail)?; let raw_mime = mail.ctype.mimetype.to_lowercase(); + let filename = get_attachment_filename(mail); info!( self.context, - "add_single_part_if_known {:?} {:?}", mime_type, msg_type + "add_single_part_if_known {:?} {:?} {:?}", mime_type, msg_type, filename ); let old_part_count = self.parts.len(); - let is_attachment = mail - .get_content_disposition()? - .params - .iter() - .any(|(key, _value)| key.starts_with("filename")); - - // regard `Content-Transfer-Encoding:` - match mime_type.type_() { - mime::TEXT | mime::HTML if !is_attachment => { - let decoded_data = match mail.get_body() { - Ok(decoded_data) => decoded_data, - Err(err) => { - warn!(self.context, "Invalid body parsed {:?}", err); - // Note that it's not always an error - might be no data - return Ok(false); - } - }; - - // check header directly as is_send_by_messenger is not yet set up - let is_msgrmsg = self.lookup_field("Chat-Version").is_some(); - - let mut simplifier = Simplify::new(); - let simplified_txt = if decoded_data.is_empty() { - "".into() - } else { - let is_html = mime_type == mime::TEXT_HTML; - simplifier.simplify(&decoded_data, is_html, is_msgrmsg) - }; - - if !simplified_txt.is_empty() { - let mut part = Part::default(); - part.typ = Viewtype::Text; - part.mimetype = Some(mime_type); - part.msg = simplified_txt; - part.msg_raw = Some(decoded_data); - self.do_add_single_part(part); + if let Ok(filename) = filename { + self.do_add_single_file_part( + msg_type, + mime_type, + &raw_mime, + &mail.get_body_raw()?, + &filename, + ); + } else { + match mime_type.type_() { + mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => { + bail!("missing attachment"); } + mime::TEXT | mime::HTML => { + let decoded_data = match mail.get_body() { + Ok(decoded_data) => decoded_data, + Err(err) => { + warn!(self.context, "Invalid body parsed {:?}", err); + // Note that it's not always an error - might be no data + return Ok(false); + } + }; - if simplifier.is_forwarded { - self.is_forwarded = true; - } - } - mime::TEXT - | mime::HTML - | mime::IMAGE - | mime::AUDIO - | mime::VIDEO - | mime::APPLICATION => { - // try to get file name from - // `Content-Disposition: ... filename*=...` - // or `Content-Disposition: ... filename*0*=... filename*1*=... filename*2*=...` - // or `Content-Disposition: ... filename=...` + // check header directly as is_send_by_messenger is not yet set up + let is_msgrmsg = self.lookup_field("Chat-Version").is_some(); - let ct = mail.get_content_disposition()?; - let mut desired_filename = ct - .params - .iter() - .filter(|(key, _value)| key.starts_with("filename")) - .fold(String::new(), |mut acc, (_key, value)| { - acc += value; - acc - }); - - if desired_filename.is_empty() { - if let Some(param) = ct.params.get("name") { - // might be a wrongly encoded filename - desired_filename = param.to_string(); - } - } - - // if there is still no filename, guess one - if desired_filename.is_empty() { - if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) { - desired_filename = format!("file.{}", subtype,); + let mut simplifier = Simplify::new(); + let simplified_txt = if decoded_data.is_empty() { + "".into() } else { - return Ok(false); + let is_html = mime_type == mime::TEXT_HTML; + simplifier.simplify(&decoded_data, is_html, is_msgrmsg) + }; + + if !simplified_txt.is_empty() { + let mut part = Part::default(); + part.typ = Viewtype::Text; + part.mimetype = Some(mime_type); + part.msg = simplified_txt; + part.msg_raw = Some(decoded_data); + self.do_add_single_part(part); + } + + if simplifier.is_forwarded { + self.is_forwarded = true; } } - self.do_add_single_file_part( - msg_type, - mime_type, - &raw_mime, - &mail.get_body_raw()?, - &desired_filename, - ); + _ => {} } - _ => {} } // add object? (we do not add all objects, eg. signatures etc. are ignored) @@ -651,6 +615,7 @@ impl<'a> MimeParser<'a> { return; } }; + info!(self.context, "added blobfile: {:?}", blob.as_name()); /* create and register Mime part referencing the new Blob object */ let mut part = Part::default(); @@ -902,12 +867,13 @@ pub struct Part { pub param: Params, } +/// return mimetype and viewtype for a parsed mail fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> { let mimetype = mail.ctype.mimetype.parse::()?; let viewtype = match mimetype.type_() { mime::TEXT => { - if !mailmime_is_attachment_disposition(mail) { + if !is_attachment_disposition(mail) { match mimetype.subtype() { mime::PLAIN | mime::HTML => Viewtype::Text, _ => Viewtype::File, @@ -941,14 +907,62 @@ fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> { Ok((mimetype, viewtype)) } -fn mailmime_is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool { - if let Some(ct) = mail.ctype.params.get("Content-Disposition") { - return ct.to_lowercase().starts_with("attachment"); +fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool { + if let Ok(ct) = mail.get_content_disposition() { + return ct.disposition == DispositionType::Attachment + && ct + .params + .iter() + .any(|(key, _value)| key.starts_with("filename")); } false } +fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result { + // try to get file name from + // `Content-Disposition: ... filename*=...` + // or `Content-Disposition: ... filename*0*=... filename*1*=... filename*2*=...` + // or `Content-Disposition: ... filename=...` + + let ct = mail.get_content_disposition()?; + ensure!( + ct.disposition == DispositionType::Attachment, + "disposition not an attachment: {:?}", + ct.disposition + ); + + let mut desired_filename = ct + .params + .iter() + .filter(|(key, _value)| key.starts_with("filename")) + .fold(String::new(), |mut acc, (_key, value)| { + acc += value; + acc + }); + println!("get_attachment_filename1: {:?}", desired_filename); + + if desired_filename.is_empty() { + if let Some(param) = ct.params.get("name") { + // might be a wrongly encoded filename + desired_filename = param.to_string(); + } + } + println!("get_attachment_filename2: {:?}", desired_filename); + + // if there is still no filename, guess one + if desired_filename.is_empty() { + if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) { + desired_filename = format!("file.{}", subtype,); + } else { + bail!("could not determine filename: {:?}", ct.disposition); + } + } + println!("get_attachment_filename3: {:?}", desired_filename); + + Ok(desired_filename) +} + // returned addresses are normalized. fn get_recipients, T: Iterator>(headers: T) -> HashSet { let mut recipients: HashSet = Default::default(); @@ -1053,6 +1067,29 @@ mod tests { assert_eq!(recipients.len(), 2); } + #[test] + fn test_is_attachment() { + let raw = include_bytes!("../test-data/message/mail_with_cc.txt"); + let mail = mailparse::parse_mail(raw).unwrap(); + assert!(!is_attachment_disposition(&mail)); + + let raw = include_bytes!("../test-data/message/mail_attach_txt.eml"); + let mail = mailparse::parse_mail(raw).unwrap(); + assert!(!is_attachment_disposition(&mail)); + assert!(!is_attachment_disposition(&mail.subparts[0])); + assert!(is_attachment_disposition(&mail.subparts[1])); + } + + #[test] + fn test_get_attachment_filename() { + let raw = include_bytes!("../test-data/message/html_attach.eml"); + let mail = mailparse::parse_mail(raw).unwrap(); + assert!(get_attachment_filename(&mail).is_err()); + assert!(get_attachment_filename(&mail.subparts[0]).is_err()); + let filename = get_attachment_filename(&mail.subparts[1]).unwrap(); + assert_eq!(filename, "test.html") + } + #[test] fn test_mailparse_content_type() { let ctype = diff --git a/test-data/message/html_attach.eml b/test-data/message/html_attach.eml new file mode 100644 index 000000000..9a2a9a255 --- /dev/null +++ b/test-data/message/html_attach.eml @@ -0,0 +1,25 @@ +Chat-Disposition-Notification-To: tmp_6272287793210918@testrun.org +Subject: =?utf-8?q?Chat=3A_File_=E2=80=93_test=2Ehtml?= +Message-ID: Mr.XA6y3og8-az.WGbH9_dNcQx@testrun.org +Date: Sat, 07 Dec 2019 19:00:27 +0000 +X-Mailer: Delta Chat Core 1.0.0-beta.12/DcFFI +Chat-Version: 1.0 +To: +From: "=?utf-8?q??=" +Content-Type: multipart/mixed; boundary="mwkNRwaJw1M5n2xcr2ODfAqvTjcj9Z" + + +--mwkNRwaJw1M5n2xcr2ODfAqvTjcj9Z +Content-Type: text/plain; charset=utf-8 + +-- +Sent with my Delta Chat Messenger: https://delta.chat + +--mwkNRwaJw1M5n2xcr2ODfAqvTjcj9Z +Content-Type: text/html +Content-Disposition: attachment; filename="test.html" +Content-Transfer-Encoding: base64 + +PGh0bWw+PGJvZHk+dGV4dDwvYm9keT5kYXRh + +--mwkNRwaJw1M5n2xcr2ODfAqvTjcj9Z-- diff --git a/test-data/message/mail_attach_txt.eml b/test-data/message/mail_attach_txt.eml new file mode 100644 index 000000000..83fbdadc8 --- /dev/null +++ b/test-data/message/mail_attach_txt.eml @@ -0,0 +1,40 @@ +From holger@merlinux.eu Sat Dec 7 11:53:58 2019 +Return-Path: +X-Original-To: holger+test@merlinux.eu +Delivered-To: holger+test@merlinux.eu +Received: from [127.0.0.1] (localhost [127.0.0.1]) + (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) + (No client certificate requested) + by mail.merlinux.eu (Postfix) with ESMTPSA id 61825100531; + Sat, 7 Dec 2019 10:53:58 +0000 (UTC) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=merlinux.eu; + s=default; t=1575716038; + bh=xhaPssQzVOHRafcciTQqDnZ1Zi4GMwsmg9pHLH3i8P8=; + h=Date:From:To:Subject; + b=U4HxGDZ8RwLwRPFtIvRsb+x5BiyICnbbY2ZOGlZdLt12MuDTfiYi/phHiQUC402EY + GXb8dYgYr5+0PDiPBa7dyt2VQLC/h9QRfOA82tb1vpJYC+KksSAH0nYQqJvs7XrqCN + i95/jwZnsWrV7w72+xsrO5qPujIE68TmM5I9Cyec= +Received: by beto.merlinux.eu (Postfix, from userid 1000) + id 229D3820070; Sat, 7 Dec 2019 11:53:58 +0100 (CET) +Date: Sat, 7 Dec 2019 11:53:57 +0100 +From: holger krekel +To: holger+test@merlinux.eu +Subject: hello +Message-ID: <20191207105357.GA6266@beto> +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="YiEDa0DAkWCtVeE4" +Content-Disposition: inline + +--YiEDa0DAkWCtVeE4 +Content-Type: text/plain; charset=us-ascii +Content-Disposition: inline + +siehe anhang + +--YiEDa0DAkWCtVeE4 +Content-Type: text/plain; charset=us-ascii +Content-Disposition: attachment; filename="x.txt" + +hello + +--YiEDa0DAkWCtVeE4--