mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 17:36:29 +03:00
- test and fix receiving text/html attachment in multipart/mixed situations
They are now preserved as attachment, instead of diving into parsing-html and simplifying. - adapt mime-debugging
This commit is contained in:
@@ -47,12 +47,10 @@ pub fn dc_receive_imf(
|
|||||||
server_uid,
|
server_uid,
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
if std::env::var(crate::DCC_MIME_DEBUG).unwrap_or_default() == "2" {
|
||||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
|
||||||
info!(context, "dc_receive_imf: incoming message mime-body:");
|
info!(context, "dc_receive_imf: incoming message mime-body:");
|
||||||
println!("{}", String::from_utf8_lossy(imf_raw));
|
println!("{}", String::from_utf8_lossy(imf_raw));
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
let mime_parser = MimeParser::from_bytes(context, imf_raw);
|
let mime_parser = MimeParser::from_bytes(context, imf_raw);
|
||||||
let mut mime_parser = if let Err(err) = mime_parser {
|
let mut mime_parser = if let Err(err) = mime_parser {
|
||||||
|
|||||||
@@ -247,13 +247,13 @@ fn decrypt_if_autocrypt_message<'a>(
|
|||||||
// Errors are returned for failures related to decryption of AC-messages.
|
// Errors are returned for failures related to decryption of AC-messages.
|
||||||
|
|
||||||
let encrypted_data_part = match wrapmime::get_autocrypt_mime(mail) {
|
let encrypted_data_part = match wrapmime::get_autocrypt_mime(mail) {
|
||||||
Err(err) => {
|
Err(_) => {
|
||||||
// not a proper autocrypt message, abort and ignore
|
// not an autocrypt mime message, abort and ignore
|
||||||
info!(context, "Not an autocrypt message: {:?}", err);
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
};
|
};
|
||||||
|
info!(context, "Detected Autocrypt-mime message");
|
||||||
|
|
||||||
decrypt_part(
|
decrypt_part(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet};
|
|||||||
|
|
||||||
use deltachat_derive::{FromSql, ToSql};
|
use deltachat_derive::{FromSql, ToSql};
|
||||||
use lettre_email::mime::{self, Mime};
|
use lettre_email::mime::{self, Mime};
|
||||||
use mailparse::MailHeaderMap;
|
use mailparse::{DispositionType, MailHeaderMap};
|
||||||
|
|
||||||
use crate::aheader::Aheader;
|
use crate::aheader::Aheader;
|
||||||
use crate::blob::BlobObject;
|
use crate::blob::BlobObject;
|
||||||
@@ -22,6 +22,7 @@ use crate::param::*;
|
|||||||
use crate::peerstate::Peerstate;
|
use crate::peerstate::Peerstate;
|
||||||
use crate::securejoin::handle_degrade_event;
|
use crate::securejoin::handle_degrade_event;
|
||||||
use crate::stock::StockMessage;
|
use crate::stock::StockMessage;
|
||||||
|
use crate::{bail, ensure};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct MimeParser<'a> {
|
pub struct MimeParser<'a> {
|
||||||
@@ -110,7 +111,7 @@ impl<'a> MimeParser<'a> {
|
|||||||
mail_raw = raw;
|
mail_raw = raw;
|
||||||
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
|
let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
|
||||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
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));
|
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 (mime_type, msg_type) = get_mime_type(mail)?;
|
||||||
let raw_mime = mail.ctype.mimetype.to_lowercase();
|
let raw_mime = mail.ctype.mimetype.to_lowercase();
|
||||||
|
|
||||||
|
let filename = get_attachment_filename(mail);
|
||||||
info!(
|
info!(
|
||||||
self.context,
|
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 old_part_count = self.parts.len();
|
||||||
|
|
||||||
let is_attachment = mail
|
if let Ok(filename) = filename {
|
||||||
.get_content_disposition()?
|
self.do_add_single_file_part(
|
||||||
.params
|
msg_type,
|
||||||
.iter()
|
mime_type,
|
||||||
.any(|(key, _value)| key.starts_with("filename"));
|
&raw_mime,
|
||||||
|
&mail.get_body_raw()?,
|
||||||
// regard `Content-Transfer-Encoding:`
|
&filename,
|
||||||
match mime_type.type_() {
|
);
|
||||||
mime::TEXT | mime::HTML if !is_attachment => {
|
} else {
|
||||||
let decoded_data = match mail.get_body() {
|
match mime_type.type_() {
|
||||||
Ok(decoded_data) => decoded_data,
|
mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
|
||||||
Err(err) => {
|
bail!("missing attachment");
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
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 {
|
// check header directly as is_send_by_messenger is not yet set up
|
||||||
self.is_forwarded = true;
|
let is_msgrmsg = self.lookup_field("Chat-Version").is_some();
|
||||||
}
|
|
||||||
}
|
|
||||||
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=...`
|
|
||||||
|
|
||||||
let ct = mail.get_content_disposition()?;
|
let mut simplifier = Simplify::new();
|
||||||
let mut desired_filename = ct
|
let simplified_txt = if decoded_data.is_empty() {
|
||||||
.params
|
"".into()
|
||||||
.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,);
|
|
||||||
} else {
|
} 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)
|
// add object? (we do not add all objects, eg. signatures etc. are ignored)
|
||||||
@@ -651,6 +615,7 @@ impl<'a> MimeParser<'a> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
info!(self.context, "added blobfile: {:?}", blob.as_name());
|
||||||
|
|
||||||
/* create and register Mime part referencing the new Blob object */
|
/* create and register Mime part referencing the new Blob object */
|
||||||
let mut part = Part::default();
|
let mut part = Part::default();
|
||||||
@@ -902,12 +867,13 @@ pub struct Part {
|
|||||||
pub param: Params,
|
pub param: Params,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// return mimetype and viewtype for a parsed mail
|
||||||
fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
|
fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
|
||||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||||
|
|
||||||
let viewtype = match mimetype.type_() {
|
let viewtype = match mimetype.type_() {
|
||||||
mime::TEXT => {
|
mime::TEXT => {
|
||||||
if !mailmime_is_attachment_disposition(mail) {
|
if !is_attachment_disposition(mail) {
|
||||||
match mimetype.subtype() {
|
match mimetype.subtype() {
|
||||||
mime::PLAIN | mime::HTML => Viewtype::Text,
|
mime::PLAIN | mime::HTML => Viewtype::Text,
|
||||||
_ => Viewtype::File,
|
_ => Viewtype::File,
|
||||||
@@ -941,14 +907,62 @@ fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
|
|||||||
Ok((mimetype, viewtype))
|
Ok((mimetype, viewtype))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mailmime_is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
|
fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
|
||||||
if let Some(ct) = mail.ctype.params.get("Content-Disposition") {
|
if let Ok(ct) = mail.get_content_disposition() {
|
||||||
return ct.to_lowercase().starts_with("attachment");
|
return ct.disposition == DispositionType::Attachment
|
||||||
|
&& ct
|
||||||
|
.params
|
||||||
|
.iter()
|
||||||
|
.any(|(key, _value)| key.starts_with("filename"));
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_attachment_filename(mail: &mailparse::ParsedMail) -> Result<String> {
|
||||||
|
// 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.
|
// returned addresses are normalized.
|
||||||
fn get_recipients<S: AsRef<str>, T: Iterator<Item = (S, S)>>(headers: T) -> HashSet<String> {
|
fn get_recipients<S: AsRef<str>, T: Iterator<Item = (S, S)>>(headers: T) -> HashSet<String> {
|
||||||
let mut recipients: HashSet<String> = Default::default();
|
let mut recipients: HashSet<String> = Default::default();
|
||||||
@@ -1053,6 +1067,29 @@ mod tests {
|
|||||||
assert_eq!(recipients.len(), 2);
|
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]
|
#[test]
|
||||||
fn test_mailparse_content_type() {
|
fn test_mailparse_content_type() {
|
||||||
let ctype =
|
let ctype =
|
||||||
|
|||||||
25
test-data/message/html_attach.eml
Normal file
25
test-data/message/html_attach.eml
Normal file
@@ -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: <tmp_5890965001269692@testrun.org>
|
||||||
|
From: "=?utf-8?q??=" <tmp_6272287793210918@testrun.org>
|
||||||
|
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--
|
||||||
40
test-data/message/mail_attach_txt.eml
Normal file
40
test-data/message/mail_attach_txt.eml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
From holger@merlinux.eu Sat Dec 7 11:53:58 2019
|
||||||
|
Return-Path: <holger@merlinux.eu>
|
||||||
|
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 <holger@merlinux.eu>
|
||||||
|
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--
|
||||||
Reference in New Issue
Block a user