Do not treat non-failed DSNs as NDNs

This commit is contained in:
link2xt
2022-07-14 20:01:45 +00:00
parent d09be1f7e3
commit 294d8862e4
5 changed files with 197 additions and 23 deletions

View File

@@ -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

View File

@@ -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<String>,
) -> 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<()> {

View File

@@ -73,7 +73,7 @@ pub struct MimeMessage {
pub(crate) user_avatar: Option<AvatarAction>,
pub(crate) group_avatar: Option<AvatarAction>,
pub(crate) mdn_reports: Vec<Report>,
pub(crate) failure_report: Option<FailureReport>,
pub(crate) delivery_report: Option<DeliveryReport>,
/// Standard USENET signature, if any.
pub(crate) footer: Option<String>,
@@ -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<Option<FailureReport>> {
) -> Result<Option<DeliveryReport>> {
// 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<regex::Regex> =
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<String>,
}
/// 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<String>,
pub failure: bool,
}
#[allow(clippy::indexing_slicing)]

View File

@@ -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()));
}

View File

@@ -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 <anon_1@posteo.de>; 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 <anon_1@posteo.de>; 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 <anon_1@posteo.de>; 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 <anon_1@posteo.at>; 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
<anon_2@gmx.at>: 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: <anon_1@posteo.at>
Received: from mout02.posteo.de (unknown [10.0.0.66])
by mout02.posteo.de (Postfix) with ESMTPS id 56E6D1A007F
for <anon_2@gmx.at>; 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 <anon_2@gmx.at>; 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 <anon_2@gmx.at>; 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 <anon_1@posteo.at>
To: Anon_2 <anon_2@gmx.at>
Subject: Hallo
Message-ID: <8b7b1a9d0c8cc588c7bcac47f5687634@posteo.de>
Posteo-User: anon_1@posteo.de
Posteo-Dkim: ok
--56E6D1A007F.1623487327/mout02.posteo.de--