diff --git a/CHANGELOG.md b/CHANGELOG.md index f49b4b6e1..55555622d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Fixes - don't squash text parts of NDN into attachments #3497 +- do not treat non-failed DSNs as NDNs #3506 ## 1.89.0 diff --git a/src/message.rs b/src/message.rs index 5e8eaf18a..346ece3f5 100644 --- a/src/message.rs +++ b/src/message.rs @@ -19,7 +19,7 @@ use crate::download::DownloadState; use crate::ephemeral::{start_ephemeral_timers_msgids, Timer as EphemeralTimer}; use crate::events::EventType; use crate::imap::markseen_on_imap_table; -use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage}; +use crate::mimeparser::{parse_message_id, DeliveryReport, SystemMessage}; use crate::param::{Param, Params}; use crate::pgp::split_armored_data; use crate::scheduler::InterruptInfo; @@ -1532,7 +1532,7 @@ pub async fn handle_mdn( /// Where appropriate, also adds an info message telling the user which of the recipients of a group message failed. pub(crate) async fn handle_ndn( context: &Context, - failed: &FailureReport, + failed: &DeliveryReport, error: Option, ) -> Result<()> { if failed.rfc724_mid.is_empty() { @@ -1588,7 +1588,7 @@ pub(crate) async fn handle_ndn( async fn ndn_maybe_add_info_msg( context: &Context, - failed: &FailureReport, + failed: &DeliveryReport, chat_id: ChatId, chat_type: Chattype, ) -> Result<()> { diff --git a/src/mimeparser.rs b/src/mimeparser.rs index bf682961c..ee7ab5365 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -73,7 +73,7 @@ pub struct MimeMessage { pub(crate) user_avatar: Option, pub(crate) group_avatar: Option, pub(crate) mdn_reports: Vec, - pub(crate) failure_report: Option, + pub(crate) delivery_report: Option, /// Standard USENET signature, if any. pub(crate) footer: Option, @@ -332,7 +332,7 @@ impl MimeMessage { webxdc_status_update: None, user_avatar: None, group_avatar: None, - failure_report: None, + delivery_report: None, footer: None, is_mime_modified: false, decoded_data: Vec::new(), @@ -535,7 +535,7 @@ impl MimeMessage { self.parse_system_message_headers(context); self.parse_avatar_headers(context).await; self.parse_videochat_headers(); - if self.failure_report.is_none() { + if self.delivery_report.is_none() { self.squash_attachment_parts(); } @@ -869,7 +869,7 @@ impl MimeMessage { // Some providers, e.g. Tiscali, forget to set the report-type. So, if it's None, assume that it might be delivery-status Some("delivery-status") | None => { if let Some(report) = self.process_delivery_status(context, mail)? { - self.failure_report = Some(report); + self.delivery_report = Some(report); } // Add all parts (we need another part, preferably text/plain, to show as an error message) @@ -1280,9 +1280,46 @@ impl MimeMessage { &self, context: &Context, report: &mailparse::ParsedMail<'_>, - ) -> Result> { + ) -> Result> { + // Assume failure. + let mut failure = true; + + if let Some(status_part) = report.subparts.get(1) { + // RFC 3464 defines `message/delivery-status` + // RFC 6533 defines `message/global-delivery-status` + if status_part.ctype.mimetype != "message/delivery-status" + && status_part.ctype.mimetype != "message/global-delivery-status" + { + warn!(context, "Second part of Delivery Status Notification is not message/delivery-status or message/global-delivery-status, ignoring"); + return Ok(None); + } + + let status_body = status_part.get_body_raw()?; + + // Skip per-message fields. + let (_, sz) = mailparse::parse_headers(&status_body)?; + + // Parse first set of per-recipient fields + if let Some(status_body) = status_body.get(sz..) { + let (status_fields, _) = mailparse::parse_headers(status_body)?; + if let Some(action) = status_fields.get_first_value("action") { + if action != "failed" { + info!(context, "DSN with {:?} action", action); + failure = false; + } + } else { + warn!(context, "DSN without action"); + } + } else { + warn!(context, "DSN without per-recipient fields"); + } + } else { + // No message/delivery-status part. + return Ok(None); + } + // parse as mailheaders - if let Some(original_msg) = report.subparts.iter().find(|p| { + if let Some(original_msg) = report.subparts.get(2).filter(|p| { p.ctype.mimetype.contains("rfc822") || p.ctype.mimetype == "message/global" || p.ctype.mimetype == "message/global-headers" @@ -1303,9 +1340,10 @@ impl MimeMessage { None // We do not know which recipient failed }; - return Ok(Some(FailureReport { + return Ok(Some(DeliveryReport { rfc724_mid: original_message_id, failed_recipient: to.map(|s| s.addr), + failure, })); } @@ -1386,7 +1424,7 @@ impl MimeMessage { } else { false }; - if maybe_ndn && self.failure_report.is_none() { + if maybe_ndn && self.delivery_report.is_none() { static RE: Lazy = Lazy::new(|| regex::Regex::new(r"Message-ID:(.*)").unwrap()); for captures in self @@ -1400,9 +1438,10 @@ impl MimeMessage { if let Ok(Some(_)) = message::rfc724_mid_exists(context, &original_message_id).await { - self.failure_report = Some(FailureReport { + self.delivery_report = Some(DeliveryReport { rfc724_mid: original_message_id, failed_recipient: None, + failure: true, }) } } @@ -1440,13 +1479,15 @@ impl MimeMessage { } } - if let Some(failure_report) = &self.failure_report { - let error = parts - .iter() - .find(|p| p.typ == Viewtype::Text) - .map(|p| p.msg.clone()); - if let Err(e) = message::handle_ndn(context, failure_report, error).await { - warn!(context, "Could not handle ndn: {}", e); + if let Some(delivery_report) = &self.delivery_report { + if delivery_report.failure { + let error = parts + .iter() + .find(|p| p.typ == Viewtype::Text) + .map(|p| p.msg.clone()); + if let Err(e) = message::handle_ndn(context, delivery_report, error).await { + warn!(context, "Could not handle ndn: {}", e); + } } } } @@ -1526,6 +1567,7 @@ async fn update_gossip_peerstates( Ok(gossiped_addr) } +/// Message Disposition Notification (RFC 8098) #[derive(Debug)] pub(crate) struct Report { /// Original-Message-ID header @@ -1537,10 +1579,12 @@ pub(crate) struct Report { additional_message_ids: Vec, } +/// Delivery Status Notification (RFC 3464, RFC 6533) #[derive(Debug)] -pub(crate) struct FailureReport { +pub(crate) struct DeliveryReport { pub rfc724_mid: String, pub failed_recipient: Option, + pub failure: bool, } #[allow(clippy::indexing_slicing)] diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 3dc0a353f..ef46f265a 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -497,9 +497,9 @@ async fn add_parts( ChatIdBlocked::lookup_by_contact(context, from_id).await? }; - if chat_id.is_none() && mime_parser.failure_report.is_some() { + if chat_id.is_none() && mime_parser.delivery_report.is_some() { chat_id = Some(DC_CHAT_ID_TRASH); - info!(context, "Message belongs to an NDN (TRASH)",); + info!(context, "Message is a DSN (TRASH)",); } if chat_id.is_none() { @@ -2749,6 +2749,19 @@ mod tests { .await; } + /// Test that DSN is not treated as NDN if Action: is not "failed" + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_parse_dsn_relayed() { + test_parse_ndn( + "anon_1@posteo.de", + "anon_2@gmx.at", + "8b7b1a9d0c8cc588c7bcac47f5687634@posteo.de", + include_bytes!("../test-data/message/dsn_relayed.eml"), + None, + ) + .await; + } + // ndn = Non Delivery Notification async fn test_parse_ndn( self_addr: &str, @@ -2798,7 +2811,14 @@ mod tests { receive_imf(&t, raw_ndn, false).await.unwrap(); let msg = Message::load_from_db(&t, msg_id).await.unwrap(); - assert_eq!(msg.state, MessageState::OutFailed); + assert_eq!( + msg.state, + if error_msg.is_some() { + MessageState::OutFailed + } else { + MessageState::OutDelivered + } + ); assert_eq!(msg.error(), error_msg.map(|error| error.to_string())); } diff --git a/test-data/message/dsn_relayed.eml b/test-data/message/dsn_relayed.eml new file mode 100644 index 000000000..fae69a451 --- /dev/null +++ b/test-data/message/dsn_relayed.eml @@ -0,0 +1,109 @@ +Return-Path: <> +Delivered-To: anon_1@posteo.de +Received: from proxy02.posteo.name ([127.0.0.1]) + by dovecot16.posteo.name (Dovecot) with LMTP id 4LJTJKBpxGClSAAAchYRkQ + for ; Sat, 12 Jun 2021 10:42:09 +0200 +Received: from proxy02.posteo.de ([127.0.0.1]) + by proxy02.posteo.name (Dovecot) with LMTP id 0NENMHNXxGDI4AIAGFAyLg + ; Sat, 12 Jun 2021 10:42:09 +0200 +Received: from mailin02.posteo.de (unknown [10.0.0.62]) + by proxy02.posteo.de (Postfix) with ESMTPS id 4G2B686dbVz11xc + for ; Sat, 12 Jun 2021 10:42:08 +0200 (CEST) +Received: from mx01.posteo.de (mailin02.posteo.de [127.0.0.1]) + by mailin02.posteo.de (Postfix) with ESMTPS id AC2472152F + for ; Sat, 12 Jun 2021 10:42:08 +0200 (CEST) +X-Virus-Scanned: amavisd-new at posteo.de +X-Spam-Flag: NO +X-Spam-Score: -1 +X-Spam-Level: +X-Spam-Status: No, score=-1 tagged_above=-1000 required=7 + tests=[ALL_TRUSTED=-1] autolearn=disabled +X-Posteo-Antispam-Signature: v=1; e=base64; a=aes-256-gcm; d=7/8PYiypR3F6lmk8rQGNxZgmuPRJI9wU2IwnCWX1fg/nFdbPrDu9pCFSVsnrK1SjAWJJ9HtJVYECbeMxMhq9tOMxZf1nSN2cM/XXzeH6ELaaQfOWfQbBff3ZIe+rix/CF1uWX164 +Authentication-Results: posteo.de; dmarc=none (p=none dis=none) header.from=mout02.posteo.de +X-Posteo-TLS-Received-Status: TLSv1.3 +Received: from mout02.posteo.de (mout02.posteo.de [185.67.36.66]) + by mx01.posteo.de (Postfix) with ESMTPS id 4G2B676wGBz10Wt + for ; Sat, 12 Jun 2021 10:42:07 +0200 (CEST) +Received: by mout02.posteo.de (Postfix) + id A9F481A0089; Sat, 12 Jun 2021 10:42:07 +0200 (CEST) +Date: Sat, 12 Jun 2021 10:42:07 +0200 (CEST) +From: MAILER-DAEMON@mout02.posteo.de (Mail Delivery System) +Subject: Successful Mail Delivery Report +To: anon_1@posteo.at +Auto-Submitted: auto-replied +MIME-Version: 1.0 +Content-Type: multipart/report; report-type=delivery-status; + boundary="56E6D1A007F.1623487327/mout02.posteo.de" +Content-Transfer-Encoding: 7bit +Message-Id: <20210612084207.A9F481A0089@mout02.posteo.de> + +This is a MIME-encapsulated message. + +--56E6D1A007F.1623487327/mout02.posteo.de +Content-Description: Notification +Content-Type: text/plain; charset=us-ascii + +This is the mail system at host mout02.posteo.de. + +Your message was successfully delivered to the destination(s) +listed below. If the message was delivered to mailbox you will +receive no further notifications. Otherwise you may still receive +notifications of mail delivery errors from other systems. + + The mail system + +: delivery via mx00.emig.gmx.net[212.227.15.9]:25: 250 + Requested mail action okay, completed: id=1M9ohD-1lvXys2NFd-005r3O + +--56E6D1A007F.1623487327/mout02.posteo.de +Content-Description: Delivery report +Content-Type: message/delivery-status + +Reporting-MTA: dns; mout02.posteo.de +X-Postfix-Queue-ID: 56E6D1A007F +X-Postfix-Sender: rfc822; anon_1@posteo.at +Arrival-Date: Sat, 12 Jun 2021 10:42:07 +0200 (CEST) + +Final-Recipient: rfc822; anon_2@gmx.at +Original-Recipient: rfc822;anon_2@gmx.at +Action: relayed +Status: 2.0.0 +Remote-MTA: dns; mx00.emig.gmx.net +Diagnostic-Code: smtp; 250 Requested mail action okay, completed: + id=1M9ohD-1lvXys2NFd-005r3O + +--56E6D1A007F.1623487327/mout02.posteo.de +Content-Description: Message Headers +Content-Type: text/rfc822-headers + +Return-Path: +Received: from mout02.posteo.de (unknown [10.0.0.66]) + by mout02.posteo.de (Postfix) with ESMTPS id 56E6D1A007F + for ; Sat, 12 Jun 2021 10:42:07 +0200 (CEST) +Received: from submission-encrypt01.posteo.de (unknown [10.0.0.75]) + by mout02.posteo.de (Postfix) with ESMTPS id 1C39E2400FD + for ; Sat, 12 Jun 2021 10:42:07 +0200 (CEST) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=posteo.at; s=2017; + t=1623487327; bh=+ZIKEoFCh8N5xYBj6tMbfqiyHmay76uM4H4bfme6VyU=; + h=Date:From:To:Subject:From; + b=QK6HwDU2YEzzTgHN2PRT2lPaf5uwC7ZJ1Y0QMSUrEyvJxwPj6+z6OoEqRDcgQcGVo + biAO2aKyBX+YCFwM5a6CaJotv8DaL+hn/XLk3RKqxGKTu5cBLQXJc0gjfRMel7LnBg + i0UxTeOqoTw2anWTonH2GnseUPtVAhi23UICVD6gC6DchuNYF/YloMltns5HMGthQh + z279J05txneSKgpbU/R3fN2v5ACEve7X6GoxM0hDZRNmAur0HAxAREc9xIaHwQ3zXM + dEGFyO53s+UzLlOFnY4vhGVI3AiyOZUProq6vX40g9e4TkrIJMGd1pyKG4NdajauuY + KTIwbUiR5Y2Xw== +Received: from customer (localhost [127.0.0.1]) + by submission (posteo.de) with ESMTPSA id 4G2B665xBPz6tmH + for ; Sat, 12 Jun 2021 10:42:06 +0200 (CEST) +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="=_d0190a7dc3b70a1dcf12785779aad292" +Date: Sat, 12 Jun 2021 08:42:06 +0000 +From: Anon_1 +To: Anon_2 +Subject: Hallo +Message-ID: <8b7b1a9d0c8cc588c7bcac47f5687634@posteo.de> +Posteo-User: anon_1@posteo.de +Posteo-Dkim: ok + +--56E6D1A007F.1623487327/mout02.posteo.de--