Files
chatmail-core/src/mimeparser.rs
2019-12-03 12:22:55 +01:00

1054 lines
38 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::collections::{HashMap, HashSet};
use deltachat_derive::{FromSql, ToSql};
use crate::blob::BlobObject;
use crate::config::Config;
use crate::constants::Viewtype;
use crate::contact::*;
use crate::context::Context;
use crate::dc_simplify::*;
use crate::dc_tools::*;
use crate::e2ee;
use crate::error::Result;
use crate::job::{job_add, Action};
use crate::location;
use crate::message;
use crate::message::MsgId;
use crate::param::*;
use crate::stock::StockMessage;
use crate::wrapmime;
#[derive(Debug)]
pub struct MimeParser<'a> {
pub context: &'a Context,
pub parts: Vec<Part>,
pub header: HashMap<String, String>,
pub subject: Option<String>,
pub is_send_by_messenger: bool,
pub decrypting_failed: bool,
pub encrypted: bool,
pub signatures: HashSet<String>,
pub gossipped_addr: HashSet<String>,
pub is_forwarded: bool,
pub is_system_message: SystemMessage,
pub location_kml: Option<location::Kml>,
pub message_kml: Option<location::Kml>,
reports: Vec<Report>,
parsed_header_protected: bool,
mdns_enabled: bool,
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql)]
#[repr(i32)]
pub enum SystemMessage {
Unknown = 0,
GroupNameChanged = 2,
GroupImageChanged = 3,
MemberAddedToGroup = 4,
MemberRemovedFromGroup = 5,
AutocryptSetupMessage = 6,
SecurejoinMessage = 7,
LocationStreamingEnabled = 8,
LocationOnly = 9,
}
impl Default for SystemMessage {
fn default() -> Self {
SystemMessage::Unknown
}
}
const DC_MIMETYPE_MP_ALTERNATIVE: i32 = 10;
const DC_MIMETYPE_MP_RELATED: i32 = 20;
const DC_MIMETYPE_MP_MIXED: i32 = 30;
const DC_MIMETYPE_MP_NOT_DECRYPTABLE: i32 = 40;
const DC_MIMETYPE_MP_REPORT: i32 = 45;
const DC_MIMETYPE_MP_SIGNED: i32 = 46;
const DC_MIMETYPE_MP_OTHER: i32 = 50;
const DC_MIMETYPE_TEXT_PLAIN: i32 = 60;
const DC_MIMETYPE_TEXT_HTML: i32 = 70;
const DC_MIMETYPE_IMAGE: i32 = 80;
const DC_MIMETYPE_AUDIO: i32 = 90;
const DC_MIMETYPE_VIDEO: i32 = 100;
const DC_MIMETYPE_FILE: i32 = 110;
const DC_MIMETYPE_AC_SETUP_FILE: i32 = 111;
impl<'a> MimeParser<'a> {
pub fn from_bytes(context: &'a Context, body: &[u8]) -> Result<Self> {
let mail = mailparse::parse_mail(body)?;
let mdns_enabled = context.get_config_bool(Config::MdnsEnabled);
let mut parser = MimeParser {
parts: Vec::new(),
header: Default::default(),
parsed_header_protected: false,
subject: None,
is_send_by_messenger: false,
decrypting_failed: false,
encrypted: false,
signatures: Default::default(),
gossipped_addr: Default::default(),
is_forwarded: false,
context,
reports: Vec::new(),
is_system_message: SystemMessage::Unknown,
location_kml: None,
message_kml: None,
mdns_enabled,
};
let mail_raw;
let mail = match e2ee::try_decrypt(parser.context, &mail) {
Ok((raw, signatures, gossipped_addr)) => {
parser.encrypted = raw.is_some();
parser.signatures = signatures;
parser.gossipped_addr = gossipped_addr;
if let Some(raw) = raw {
mail_raw = raw;
mailparse::parse_mail(&mail_raw)?
} else {
mail
}
}
Err(err) => {
// continue with the current, still encrypted, mime tree.
// unencrypted parts will be replaced by an error message
// that is added as "the message" to the chat then.
//
// if we just return here, the header is missing
// and the caller cannot display the message
// and try to assign the message to a chat
warn!(parser.context, "decryption failed: {}", err);
mail
}
};
parser.hash_header(&mail.headers);
parser.parse_mime_recursive(&mail)?;
parser.parse_headers()?;
Ok(parser)
}
fn parse_headers(&mut self) -> Result<()> {
if let Some(field) = self.lookup_field("Subject") {
self.subject = Some(field.clone());
}
if let Some(_) = self.lookup_field("Chat-Version") {
self.is_send_by_messenger = true
}
if let Some(_) = self.lookup_field("Autocrypt-Setup-Message") {
let has_setup_file = self
.parts
.iter()
.any(|p| p.mimetype == DC_MIMETYPE_AC_SETUP_FILE);
if has_setup_file {
self.is_system_message = SystemMessage::AutocryptSetupMessage;
// TODO: replace the following code with this
// once drain_filter stabilizes.
//
// See https://doc.rust-lang.org/std/vec/struct.Vec.html#method.drain_filter
// and https://github.com/rust-lang/rust/issues/43244
//
// mimeparser
// .parts
// .drain_filter(|part| part.int_mimetype != 111)
// .for_each(|part| dc_mimepart_unref(part));
let mut i = 0;
while i != self.parts.len() {
if self.parts[i].mimetype != 111 {
self.parts.remove(i);
} else {
i += 1;
}
}
}
} else if let Some(value) = self.lookup_field("Chat-Content") {
if value == "location-streaming-enabled" {
self.is_system_message = SystemMessage::LocationStreamingEnabled;
}
}
if let Some(_) = self.lookup_field("Chat-Group-Image") {
if !self.parts.is_empty() {
let textpart = &self.parts[0];
if textpart.typ == Viewtype::Text && self.parts.len() >= 2 {
let imgpart = &mut self.parts[1];
if imgpart.typ == Viewtype::Image {
imgpart.is_meta = true;
}
}
}
}
if self.is_send_by_messenger && self.parts.len() == 2 {
let need_drop = {
let textpart = &self.parts[0];
let filepart = &self.parts[1];
textpart.typ == Viewtype::Text
&& (filepart.typ == Viewtype::Image
|| filepart.typ == Viewtype::Gif
|| filepart.typ == Viewtype::Sticker
|| filepart.typ == Viewtype::Audio
|| filepart.typ == Viewtype::Voice
|| filepart.typ == Viewtype::Video
|| filepart.typ == Viewtype::File)
&& !filepart.is_meta
};
if need_drop {
let mut filepart = self.parts.swap_remove(1);
// insert new one
filepart.msg = self.parts[0].msg.as_ref().map(|s| s.to_string());
// forget the one we use now
self.parts[0].msg = None;
// swap new with old
std::mem::replace(&mut self.parts[0], filepart);
}
}
if let Some(ref subject) = self.subject {
let mut prepend_subject: libc::c_int = 1i32;
if !self.decrypting_failed {
let colon = subject.find(':');
if colon == Some(2)
|| colon == Some(3)
|| self.is_send_by_messenger
|| subject.contains("Chat:")
{
prepend_subject = 0i32
}
}
if 0 != prepend_subject {
let subj = if let Some(n) = subject.find('[') {
&subject[0..n]
} else {
subject
}
.trim();
if !subj.is_empty() {
for part in self.parts.iter_mut() {
if part.typ == Viewtype::Text {
let new_txt = format!(
"{} {}",
subj,
part.msg.as_ref().expect("missing msg part")
);
part.msg = Some(new_txt);
break;
}
}
}
}
}
if self.is_forwarded {
for part in self.parts.iter_mut() {
part.param.set_int(Param::Forwarded, 1);
}
}
if self.parts.len() == 1 {
if self.parts[0].typ == Viewtype::Audio {
if let Some(_) = self.lookup_field("Chat-Voice-Message") {
let part_mut = &mut self.parts[0];
part_mut.typ = Viewtype::Voice;
}
}
if self.parts[0].typ == Viewtype::Image {
if let Some(value) = self.lookup_field("Chat-Content") {
if value == "sticker" {
let part_mut = &mut self.parts[0];
part_mut.typ = Viewtype::Sticker;
}
}
}
let part = &self.parts[0];
if part.typ == Viewtype::Audio
|| part.typ == Viewtype::Voice
|| part.typ == Viewtype::Video
{
if let Some(field_0) = self.lookup_field("Chat-Duration") {
let duration_ms = field_0.parse().unwrap_or_default();
if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
let part_mut = &mut self.parts[0];
part_mut.param.set_int(Param::Duration, duration_ms);
}
}
}
}
if !self.decrypting_failed {
if let Some(dn_field) = self.lookup_field("Chat-Disposition-Notification-To") {
if self.get_last_nonmeta().is_some() {
let addrs = mailparse::addrparse(&dn_field).unwrap();
if let Some(dn_to_addr) = addrs.first() {
if let Some(from_field) = self.lookup_field("From") {
let value = from_field;
let from_addrs = mailparse::addrparse(&value).unwrap();
if let Some(from_addr) = from_addrs.first() {
if from_addr == dn_to_addr {
if let Some(part_4) = self.get_last_nonmeta_mut() {
part_4.param.set_int(Param::WantsMdn, 1);
}
}
}
}
}
}
}
}
/* Cleanup - and try to create at least an empty part if there are no parts yet */
if self.get_last_nonmeta().is_none() && self.reports.is_empty() {
let mut part_5 = Part::default();
part_5.typ = Viewtype::Text;
part_5.msg = Some("".into());
if let Some(ref subject) = self.subject {
if !self.is_send_by_messenger {
part_5.msg = Some(subject.to_string())
}
}
self.parts.push(part_5);
}
Ok(())
}
pub fn get_last_nonmeta(&self) -> Option<&Part> {
self.parts.iter().rev().find(|part| !part.is_meta)
}
pub fn get_last_nonmeta_mut(&mut self) -> Option<&mut Part> {
self.parts.iter_mut().rev().find(|part| !part.is_meta)
}
pub fn lookup_field(&self, field_name: &str) -> Option<&String> {
self.header.get(&field_name.to_lowercase())
}
fn parse_mime_recursive(&mut self, mail: &mailparse::ParsedMail<'_>) -> Result<bool> {
if mail.ctype.params.get("protected-headers").is_some() {
if mail.ctype.mimetype == "text/rfc822-headers" {
info!(
self.context,
"Protected headers found in text/rfc822-headers attachment: Will be ignored.",
);
return Ok(false);
}
if !self.parsed_header_protected {
// use the most outer protected header - this is typically
// created in sync with the normal, unprotected header
self.parsed_header_protected = true;
self.hash_header(&mail.headers);
} else {
info!(
self.context,
"Protected headers found in MIME header: Will be ignored as we already found an outer one."
);
}
}
// multiple = multipart/ or message/
enum MimeS {
Multiple,
Single,
Message,
}
let mimetype = mail.ctype.mimetype.to_lowercase();
let m = if mimetype.starts_with("multipart") {
if mail.ctype.params.get("boundary").is_some() {
MimeS::Multiple
} else {
MimeS::Single
}
} else if mimetype.starts_with("message") {
if mimetype == "message/rfc822" {
MimeS::Message
} else {
MimeS::Single
}
} else {
MimeS::Single
};
match m {
MimeS::Multiple => self.handle_multiple(mail),
MimeS::Message => {
let raw = mail.get_body_raw()?;
if raw.is_empty() {
return Ok(false);
}
let mail = mailparse::parse_mail(&raw).unwrap();
self.parse_mime_recursive(&mail)
}
MimeS::Single => self.add_single_part_if_known(mail),
}
}
fn handle_multiple(&mut self, mail: &mailparse::ParsedMail<'_>) -> Result<bool> {
let mut any_part_added = false;
match mailmime_get_mime_type(mail) {
/* Most times, mutlipart/alternative contains true alternatives
as text/plain and text/html. If we find a multipart/mixed
inside mutlipart/alternative, we use this (happens eg in
apple mail: "plaintext" as an alternative to "html+PDF attachment") */
(DC_MIMETYPE_MP_ALTERNATIVE, _) => {
for cur_data in &mail.subparts {
if mailmime_get_mime_type(cur_data).0 == DC_MIMETYPE_MP_MIXED {
any_part_added = self.parse_mime_recursive(cur_data)?;
break;
}
}
if !any_part_added {
/* search for text/plain and add this */
for cur_data in &mail.subparts {
if mailmime_get_mime_type(cur_data).0 == DC_MIMETYPE_TEXT_PLAIN {
any_part_added = self.parse_mime_recursive(cur_data)?;
break;
}
}
}
if !any_part_added {
/* `text/plain` not found - use the first part */
for cur_part in &mail.subparts {
if self.parse_mime_recursive(cur_part)? {
any_part_added = true;
break;
}
}
}
}
(DC_MIMETYPE_MP_RELATED, _) => {
/* add the "root part" - the other parts may be referenced which is
not interesting for us (eg. embedded images) we assume he "root part"
being the first one, which may not be always true ...
however, most times it seems okay. */
if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(first)?;
}
}
(DC_MIMETYPE_MP_NOT_DECRYPTABLE, _) => {
let mut part = Part::default();
part.typ = Viewtype::Text;
let msg_body = self.context.stock_str(StockMessage::CantDecryptMsgBody);
let txt = format!("[{}]", msg_body);
part.msg_raw = Some(txt.clone());
part.msg = Some(txt);
self.parts.push(part);
any_part_added = true;
self.decrypting_failed = true;
}
(DC_MIMETYPE_MP_SIGNED, _) => {
/* RFC 1847: "The multipart/signed content type
contains exactly two body parts. The first body
part is the body part over which the digital signature was created [...]
The second body part contains the control information necessary to
verify the digital signature." We simpliy take the first body part and
skip the rest. (see
https://k9mail.github.io/2016/11/24/OpenPGP-Considerations-Part-I.html
for background information why we use encrypted+signed) */
if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(first)?;
}
}
(DC_MIMETYPE_MP_REPORT, _) => {
/* RFC 6522: the first part is for humans, the second for machines */
if mail.subparts.len() >= 2 {
let ct = mail.get_content_disposition()?;
if let Some(report_type) = ct.params.get("report-type") {
if report_type == "disposition-notification" {
if let Some(report) = self.process_report(mail)? {
self.reports.push(report);
}
} else {
/* eg. `report-type=delivery-status`;
maybe we should show them as a little error icon */
if let Some(first) = mail.subparts.iter().next() {
any_part_added = self.parse_mime_recursive(mail)?;
}
}
}
}
}
_ => {
/* eg. DC_MIMETYPE_MP_MIXED - add all parts (in fact,
AddSinglePartIfKnown() later check if the parts are really supported)
HACK: the following lines are a hack for clients who use
multipart/mixed instead of multipart/alternative for
combined text/html messages (eg. Stock Android "Mail" does so).
So, if we detect such a message below, we skip the HTML
part. However, not sure, if there are useful situations to use
plain+html in multipart/mixed - if so, we should disable the hack. */
let mut skip_part = -1;
let mut html_part = -1;
let mut plain_cnt = 0;
let mut html_cnt = 0;
for (i, cur_data) in mail.subparts.iter().enumerate() {
match mailmime_get_mime_type(cur_data) {
(DC_MIMETYPE_TEXT_PLAIN, _) => {
plain_cnt += 1;
}
(DC_MIMETYPE_TEXT_HTML, _) => {
html_part = i as isize;
html_cnt += 1;
}
_ => {}
}
}
if plain_cnt == 1 && html_cnt == 1 {
warn!(
self.context,
"HACK: multipart/mixed message found with PLAIN and HTML, we\'ll skip the HTML part as this seems to be unwanted."
);
skip_part = html_part;
}
for (i, cur_data) in mail.subparts.iter().enumerate() {
if i as isize != skip_part {
if self.parse_mime_recursive(cur_data)? {
any_part_added = true;
}
}
}
}
}
Ok(any_part_added)
}
fn add_single_part_if_known(&mut self, mail: &mailparse::ParsedMail<'_>) -> Result<bool> {
// return true if a part was added
let (mime_type, msg_type) = mailmime_get_mime_type(mail);
let raw_mime = mail.ctype.mimetype.to_lowercase();
if !raw_mime.starts_with("text") {
// MAILMIME_DATA_FILE indicates, the data is in a file; AFAIK this is not used on parsing
return Ok(false);
}
let old_part_count = self.parts.len();
// regard `Content-Transfer-Encoding:`
match mime_type {
DC_MIMETYPE_TEXT_PLAIN | DC_MIMETYPE_TEXT_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);
}
};
// 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 == DC_MIMETYPE_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 = mime_type;
part.msg = Some(simplified_txt);
part.msg_raw = Some(decoded_data);
self.do_add_single_part(part);
}
if simplifier.is_forwarded {
self.is_forwarded = true;
}
}
DC_MIMETYPE_IMAGE
| DC_MIMETYPE_AUDIO
| DC_MIMETYPE_VIDEO
| DC_MIMETYPE_FILE
| DC_MIMETYPE_AC_SETUP_FILE => {
// 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 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('/').skip(1).next() {
desired_filename = format!("file.{}", subtype,);
} else {
return Ok(false);
}
}
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)
Ok(self.parts.len() > old_part_count)
}
fn do_add_single_file_part(
&mut self,
msg_type: Viewtype,
mime_type: libc::c_int,
raw_mime: &String,
decoded_data: &[u8],
filename: &str,
) {
if decoded_data.is_empty() {
return;
}
// treat location/message kml file attachments specially
if filename.ends_with(".kml") {
// XXX what if somebody sends eg an "location-highlights.kml"
// attachment unrelated to location streaming?
if filename.starts_with("location") || filename.starts_with("message") {
let parsed = location::Kml::parse(self.context, decoded_data)
.map_err(|err| {
warn!(self.context, "failed to parse kml part: {}", err);
})
.ok();
if filename.starts_with("location") {
self.location_kml = parsed;
} else {
self.message_kml = parsed;
}
return;
}
}
/* we have a regular file attachment,
write decoded data to new blob object */
let blob = match BlobObject::create(self.context, filename, decoded_data) {
Ok(blob) => blob,
Err(err) => {
error!(
self.context,
"Could not add blob for mime part {}, error {}", filename, err
);
return;
}
};
/* create and register Mime part referencing the new Blob object */
let mut part = Part::default();
part.typ = msg_type;
part.mimetype = mime_type;
part.bytes = decoded_data.len() as libc::c_int;
part.param.set(Param::File, blob.as_name());
part.param.set(Param::MimeType, raw_mime);
if mime_type == DC_MIMETYPE_IMAGE {
if let Ok((width, height)) = dc_get_filemeta(decoded_data) {
part.param.set_int(Param::Width, width as i32);
part.param.set_int(Param::Height, height as i32);
}
}
self.do_add_single_part(part);
}
fn do_add_single_part(&mut self, mut part: Part) {
if self.encrypted {
if self.signatures.len() > 0 {
part.param.set_int(Param::GuaranteeE2ee, 1);
} else {
// XXX if the message was encrypted but not signed
// it's not neccessarily an error we need to signal.
// we could just treat it as if it was not encrypted.
part.param.set_int(Param::ErroneousE2ee, 0x2);
}
}
self.parts.push(part);
}
pub fn is_mailinglist_message(&self) -> bool {
if let Some(_) = self.lookup_field("List-Id") {
return true;
}
if let Some(precedence) = self.lookup_field("Precedence") {
if precedence == "list" || precedence == "bulk" {
return true;
}
}
false
}
pub fn sender_equals_recipient(&self) -> bool {
/* get From: and check there is exactly one sender */
if let Some(field) = self.lookup_field("From") {
if let Ok(addrs) = mailparse::addrparse(field) {
if addrs.len() != 1 {
return false;
}
if let mailparse::MailAddr::Single(ref info) = addrs[0] {
let from_addr_norm = addr_normalize(&info.addr);
let recipients = wrapmime::mailimf_get_recipients(&self.header);
if recipients.len() == 1 && recipients.contains(from_addr_norm) {
return true;
}
}
}
}
false
}
pub fn repl_msg_by_error(&mut self, error_msg: impl AsRef<str>) {
if self.parts.is_empty() {
return;
}
let part = &mut self.parts[0];
part.typ = Viewtype::Text;
part.msg = Some(format!("[{}]", error_msg.as_ref()));
self.parts.truncate(1);
assert_eq!(self.parts.len(), 1);
}
pub fn get_rfc724_mid(&self) -> Option<String> {
// get Message-ID from header
if let Some(field) = self.lookup_field("Message-ID") {
return parse_message_id(field);
}
None
}
fn hash_header(&mut self, fields: &[mailparse::MailHeader<'_>]) {
for field in fields {
if let Ok(key) = field.get_key() {
// lowercasing all headers is technically not correct, but makes things work better
let key = key.to_lowercase();
if !self.header.contains_key(&key) || // key already exists, only overwrite known types (protected headers)
is_known(&key) || key.starts_with("chat-")
{
if let Ok(value) = field.get_value() {
self.header.insert(key, value);
}
}
}
}
}
fn process_report(&self, report: &mailparse::ParsedMail<'_>) -> Result<Option<Report>> {
use mailparse::MailHeaderMap;
let ct = report.get_content_disposition()?;
let report_type = ct.params.get("report-type");
if report_type.is_none() {
return Ok(None);
}
let report_type = report_type.unwrap();
if report_type != "disposition-notification" || report.subparts.len() < 2 {
// the first part is for humans, the second for machines
return Ok(None);
}
// to get a clear functionality, do not show incoming MDNs if the options is disabled
if !self.mdns_enabled {
return Ok(None);
}
// 1. get content
let raw = report.subparts[1].get_body_raw()?;
let report_details = mailparse::parse_mail(&raw)?;
// 2. parse as mailheaders
let report_body = report_details.get_body_raw()?;
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
// 3. retrieve information
// must be present
if let Some(_disposition) = report_fields.get_first_value("Disposition").ok().flatten() {
if let Some(original_message_id) = report_fields
.get_first_value("Original-Message-ID")
.ok()
.flatten()
.and_then(|v| parse_message_id(&v))
{
return Ok(Some(Report {
original_message_id,
}));
}
}
Ok(None)
}
// Handle reports (mainly MDNs)
pub fn handle_reports(
&self,
from_id: u32,
sent_timestamp: i64,
rr_event_to_send: &mut Vec<(u32, MsgId)>,
server_folder: impl AsRef<str>,
server_uid: u32,
) {
for report in &self.reports {
let mut mdn_consumed = false;
if let Some((chat_id, msg_id)) = message::mdn_from_ext(
self.context,
from_id,
&report.original_message_id,
sent_timestamp,
) {
rr_event_to_send.push((chat_id, msg_id));
mdn_consumed = true;
}
if self.is_send_by_messenger || mdn_consumed {
let mut param = Params::new();
param.set(Param::ServerFolder, server_folder.as_ref());
param.set_int(Param::ServerUid, server_uid as i32);
if self.is_send_by_messenger && self.context.get_config_bool(Config::MvboxMove) {
param.set_int(Param::AlsoMove, 1);
}
job_add(self.context, Action::MarkseenMdnOnImap, 0, param, 0);
}
}
}
}
#[derive(Debug)]
struct Report {
original_message_id: String,
}
fn parse_message_id(field: &str) -> Option<String> {
if let Ok(addrs) = mailparse::addrparse(field) {
// Assume the message id is a single id in the form of <id>
if let mailparse::MailAddr::Single(mailparse::SingleInfo { ref addr, .. }) = addrs[0] {
return Some(addr.clone());
}
}
None
}
fn is_known(key: &str) -> bool {
match key {
"return-path" | "date" | "from" | "sender" | "reply-to" | "to" | "cc" | "bcc"
| "message-id" | "in-reply-to" | "references" | "subject" => true,
_ => false,
}
}
#[derive(Default, Debug, Clone)]
pub struct Part {
pub typ: Viewtype,
pub is_meta: bool,
pub mimetype: i32,
pub msg: Option<String>,
pub msg_raw: Option<String>,
pub bytes: i32,
pub param: Params,
}
fn mailmime_get_mime_type(mail: &mailparse::ParsedMail<'_>) -> (libc::c_int, Viewtype) {
let unknown_type = (0, Viewtype::Unknown);
let mimetype = mail.ctype.mimetype.to_lowercase();
let mut parts = mimetype.split('/');
let typ = parts.next().expect("invalid mimetype");
let subtype = parts.next().unwrap_or_default();
match typ {
"text" => {
if !mailmime_is_attachment_disposition(mail) {
if subtype == "plain" {
return (DC_MIMETYPE_TEXT_PLAIN, Viewtype::Text);
}
if subtype == "html" {
return (DC_MIMETYPE_TEXT_HTML, Viewtype::Text);
}
}
(DC_MIMETYPE_FILE, Viewtype::File)
}
"image" => {
let msg_type = match subtype {
"gif" => Viewtype::Gif,
"svg+xml" => {
return (DC_MIMETYPE_FILE, Viewtype::File);
}
_ => Viewtype::Image,
};
(DC_MIMETYPE_IMAGE, msg_type)
}
"audio" => (DC_MIMETYPE_AUDIO, Viewtype::Audio),
"video" => (DC_MIMETYPE_VIDEO, Viewtype::Video),
"multipart" => {
let mime_type = match subtype {
"alternative" => DC_MIMETYPE_MP_ALTERNATIVE,
"related" => DC_MIMETYPE_MP_RELATED,
"encrypted" => {
// maybe try_decrypt failed to decrypt
// or it wasn't in proper Autocrypt format
DC_MIMETYPE_MP_NOT_DECRYPTABLE
}
"signed" => DC_MIMETYPE_MP_SIGNED,
"mixed" => DC_MIMETYPE_MP_MIXED,
"report" => DC_MIMETYPE_MP_REPORT,
_ => DC_MIMETYPE_MP_OTHER,
};
(mime_type, Viewtype::Unknown)
}
"message" => {
// Enacapsulated messages, see https://www.w3.org/Protocols/rfc1341/7_3_Message.html
// Also used as part "message/disposition-notification" of "multipart/report", which, however, will
// be handled separatedly.
// I've not seen any messages using this, so we do not attach these parts (maybe they're used to attach replies,
// which are unwanted at all).
// For now, we skip these parts at all; if desired, we could return DC_MIMETYPE_FILE/DC_MSG_FILE
// for selected and known subparts.
unknown_type
}
"application" => {
if subtype == "autocrypt-setup" {
return (DC_MIMETYPE_AC_SETUP_FILE, Viewtype::File);
}
(DC_MIMETYPE_FILE, Viewtype::File)
}
_ => unknown_type,
}
}
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");
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use proptest::prelude::*;
#[test]
fn test_dc_mimeparser_crash() {
let context = dummy_context();
let raw = include_bytes!("../test-data/message/issue_523.txt");
let mimeparser = MimeParser::from_bytes(&context.ctx, &raw[..]).unwrap();
assert_eq!(mimeparser.subject, None);
assert_eq!(mimeparser.parts.len(), 1);
}
proptest! {
#[ignore]
#[test]
fn test_dc_mailmime_parse_crash_fuzzy(data in "[!-~\t ]{2000,}") {
let context = dummy_context();
// parsing should error out for all these random strings
assert!(
MimeParser::from_bytes(&context.ctx, data.as_bytes()).is_err()
);
}
}
#[test]
fn test_get_rfc724_mid_exists() {
let context = dummy_context();
let raw = include_bytes!("../test-data/message/mail_with_message_id.txt");
let mimeparser = MimeParser::from_bytes(&context.ctx, &raw[..]).unwrap();
assert_eq!(
mimeparser.get_rfc724_mid(),
Some("2dfdbde7@example.org".into())
);
}
#[test]
fn test_get_rfc724_mid_not_exists() {
let context = dummy_context();
let raw = include_bytes!("../test-data/message/issue_523.txt");
let mimeparser = MimeParser::from_bytes(&context.ctx, &raw[..]).unwrap();
assert_eq!(mimeparser.get_rfc724_mid(), None);
}
#[test]
fn test_mimeparser_with_context() {
let context = dummy_context();
let raw = b"From: hello\n\
Content-Type: multipart/mixed; boundary=\"==break==\";\n\
Subject: outer-subject\n\
X-Special-A: special-a\n\
Foo: Bar\nChat-Version: 0.0\n\
\n\
--==break==\n\
Content-Type: text/plain; protected-headers=\"v1\";\n\
Subject: inner-subject\n\
X-Special-B: special-b\n\
Foo: Xy\n\
Chat-Version: 1.0\n\
\n\
test1\n\
\n\
--==break==--\n\
\n\
\x00";
let mimeparser = MimeParser::from_bytes(&context.ctx, &raw[..]).unwrap();
assert_eq!(mimeparser.subject, Some("inner-subject".into()));
let of = mimeparser.lookup_field("X-Special-A").unwrap();
assert_eq!(of, "special-a");
let of = mimeparser.lookup_field("Foo").unwrap();
assert_eq!(of, "Bar");
let of = mimeparser.lookup_field("Chat-Version").unwrap();
assert_eq!(of, "1.0");
assert_eq!(mimeparser.parts.len(), 1);
}
}