kumo_log_types/
rfc3464.rs

1//! This module parses out RFC3464 delivery status reports
2//! from an email message
3use crate::rfc5965::{
4    extract_headers, extract_single, extract_single_conv, extract_single_req, DateTimeRfc2822,
5};
6use crate::{JsonLogRecord, RecordType};
7use anyhow::{anyhow, Context};
8use bstr::{BStr, BString, ByteSlice};
9use chrono::{DateTime, Utc};
10use mailparsing::MimePart;
11use rfc5321::parser::EnvelopeAddress;
12use serde::{Deserialize, Serialize};
13use std::collections::BTreeMap;
14use std::str::FromStr;
15
16#[derive(Debug, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)]
17#[serde(rename_all = "lowercase")]
18pub enum ReportAction {
19    Failed,
20    Delayed,
21    Delivered,
22    Relayed,
23    Expanded,
24}
25
26impl std::fmt::Display for ReportAction {
27    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
28        let label = match self {
29            Self::Failed => "failed",
30            Self::Delayed => "delayed",
31            Self::Delivered => "delivered",
32            Self::Relayed => "relayed",
33            Self::Expanded => "expanded",
34        };
35        write!(fmt, "{label}")
36    }
37}
38
39impl FromStr for ReportAction {
40    type Err = anyhow::Error;
41    fn from_str(input: &str) -> anyhow::Result<Self> {
42        Ok(match input {
43            "failed" => Self::Failed,
44            "delayed" => Self::Delayed,
45            "delivered" => Self::Delivered,
46            "relayed" => Self::Relayed,
47            "expanded" => Self::Expanded,
48            _ => anyhow::bail!("invalid action type {input}"),
49        })
50    }
51}
52
53#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
54pub struct ReportStatus {
55    pub class: u8,
56    pub subject: u16,
57    pub detail: u16,
58    pub comment: Option<String>,
59}
60
61impl std::fmt::Display for ReportStatus {
62    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
63        write!(fmt, "{}.{}.{}", self.class, self.subject, self.detail)?;
64        if let Some(comment) = &self.comment {
65            write!(fmt, " {comment}")?;
66        }
67        Ok(())
68    }
69}
70
71impl From<&rfc5321::Response> for ReportStatus {
72    fn from(response: &rfc5321::Response) -> ReportStatus {
73        let (class, subject, detail) = match &response.enhanced_code {
74            Some(enh) => (enh.class, enh.subject, enh.detail),
75            None => {
76                if response.code >= 500 {
77                    (5, 0, 0)
78                } else if response.code >= 400 {
79                    (4, 0, 0)
80                } else if response.code >= 200 && response.code < 300 {
81                    (2, 0, 0)
82                } else {
83                    (4, 0, 0)
84                }
85            }
86        };
87        ReportStatus {
88            class,
89            subject,
90            detail,
91            comment: Some(response.content.clone()),
92        }
93    }
94}
95
96impl FromStr for ReportStatus {
97    type Err = anyhow::Error;
98    fn from_str(input: &str) -> anyhow::Result<Self> {
99        let mut parts: Vec<_> = input.split(' ').collect();
100
101        let mut status = parts[0].split('.');
102        let class = status
103            .next()
104            .ok_or_else(|| anyhow!("invalid Status: {input}"))?
105            .parse()
106            .context("parsing status.class")?;
107        let subject = status
108            .next()
109            .ok_or_else(|| anyhow!("invalid Status: {input}"))?
110            .parse()
111            .context("parsing status.subject")?;
112        let detail = status
113            .next()
114            .ok_or_else(|| anyhow!("invalid Status: {input}"))?
115            .parse()
116            .context("parsing status.detail")?;
117
118        parts.remove(0);
119        let comment = if parts.is_empty() {
120            None
121        } else {
122            Some(parts.join(" "))
123        };
124
125        Ok(Self {
126            class,
127            subject,
128            detail,
129            comment,
130        })
131    }
132}
133
134#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
135pub struct RemoteMta {
136    pub mta_type: String,
137    pub name: String,
138}
139
140impl std::fmt::Display for RemoteMta {
141    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
142        write!(fmt, "{}; {}", self.mta_type, self.name)
143    }
144}
145
146impl FromStr for RemoteMta {
147    type Err = anyhow::Error;
148
149    fn from_str(input: &str) -> anyhow::Result<Self> {
150        let (mta_type, name) = input
151            .split_once(";")
152            .ok_or_else(|| anyhow!("expected 'name-type; name', got {input}"))?;
153        Ok(Self {
154            mta_type: mta_type.trim().to_string(),
155            name: name.trim().to_string(),
156        })
157    }
158}
159
160#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
161pub struct Recipient {
162    pub recipient_type: String,
163    pub recipient: String,
164}
165
166impl std::fmt::Display for Recipient {
167    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
168        write!(fmt, "{};{}", self.recipient_type, self.recipient)
169    }
170}
171
172impl FromStr for Recipient {
173    type Err = anyhow::Error;
174    fn from_str(input: &str) -> anyhow::Result<Self> {
175        let (recipient_type, recipient) = input
176            .split_once(";")
177            .ok_or_else(|| anyhow!("expected 'recipient-type; recipient', got {input}"))?;
178
179        let recipient = if recipient_type == "rfc822" {
180            recipient
181                .trim()
182                .parse::<EnvelopeAddress>()
183                .map_err(|err| anyhow!("{err}"))?
184                .to_string()
185        } else {
186            recipient.trim().to_string()
187        };
188
189        Ok(Self {
190            recipient_type: recipient_type.trim().to_string(),
191            recipient,
192        })
193    }
194}
195
196#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
197pub struct DiagnosticCode {
198    pub diagnostic_type: String,
199    pub diagnostic: String,
200}
201
202impl std::fmt::Display for DiagnosticCode {
203    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
204        write!(fmt, "{}; {}", self.diagnostic_type, self.diagnostic)
205    }
206}
207
208impl FromStr for DiagnosticCode {
209    type Err = anyhow::Error;
210    fn from_str(input: &str) -> anyhow::Result<Self> {
211        let (diagnostic_type, diagnostic) = input
212            .split_once(";")
213            .ok_or_else(|| anyhow!("expected 'diagnostic-type; diagnostic', got {input}"))?;
214        Ok(Self {
215            diagnostic_type: diagnostic_type.trim().to_string(),
216            diagnostic: diagnostic.trim().to_string(),
217        })
218    }
219}
220
221#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
222pub struct PerRecipientReportEntry {
223    pub final_recipient: Recipient,
224    pub action: ReportAction,
225    pub status: ReportStatus,
226    pub original_recipient: Option<Recipient>,
227    pub remote_mta: Option<RemoteMta>,
228    pub diagnostic_code: Option<DiagnosticCode>,
229    pub last_attempt_date: Option<DateTime<Utc>>,
230    pub final_log_id: Option<String>,
231    pub will_retry_until: Option<DateTime<Utc>>,
232    pub extensions: BTreeMap<String, Vec<BString>>,
233}
234
235impl std::fmt::Display for PerRecipientReportEntry {
236    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
237        if let Some(orig) = &self.original_recipient {
238            write!(fmt, "Original-Recipient: {orig}\r\n")?;
239        }
240        write!(fmt, "Final-Recipient: {}\r\n", self.final_recipient)?;
241        write!(fmt, "Action: {}\r\n", self.action)?;
242        write!(fmt, "Status: {}\r\n", self.status)?;
243        if let Some(mta) = &self.remote_mta {
244            write!(fmt, "Remote-MTA: {mta}\r\n")?;
245        }
246        if let Some(code) = &self.diagnostic_code {
247            write!(fmt, "Diagnostic-Code: {code}\r\n")?;
248        }
249        if let Some(when) = &self.last_attempt_date {
250            write!(fmt, "Last-Attempt-Date: {}\r\n", when.to_rfc2822())?;
251        }
252        if let Some(id) = &self.final_log_id {
253            write!(fmt, "Final-Log-Id: {id}\r\n")?;
254        }
255        if let Some(when) = &self.will_retry_until {
256            write!(fmt, "Will-Retry-Until: {}\r\n", when.to_rfc2822())?;
257        }
258        for (k, vlist) in &self.extensions {
259            for v in vlist {
260                write!(fmt, "{k}: {v}\r\n")?;
261            }
262        }
263        Ok(())
264    }
265}
266
267impl PerRecipientReportEntry {
268    fn parse(part: &str) -> anyhow::Result<Self> {
269        let mut extensions = extract_headers(part.as_bytes())?;
270
271        let original_recipient = extract_single("original-recipient", &mut extensions)?;
272        let final_recipient = extract_single_req("final-recipient", &mut extensions)?;
273        let remote_mta = extract_single("remote-mta", &mut extensions)?;
274
275        let last_attempt_date = extract_single_conv::<DateTimeRfc2822, DateTime<Utc>>(
276            "last-attempt-date",
277            &mut extensions,
278        )?;
279        let will_retry_until = extract_single_conv::<DateTimeRfc2822, DateTime<Utc>>(
280            "will-retry-until",
281            &mut extensions,
282        )?;
283        let final_log_id = extract_single("final-log-id", &mut extensions)?;
284
285        let action = extract_single_req("action", &mut extensions)?;
286        let status = extract_single_req("status", &mut extensions)?;
287        let diagnostic_code = extract_single("diagnostic-code", &mut extensions)?;
288
289        Ok(Self {
290            final_recipient,
291            action,
292            status,
293            diagnostic_code,
294            original_recipient,
295            remote_mta,
296            last_attempt_date,
297            final_log_id,
298            will_retry_until,
299            extensions,
300        })
301    }
302}
303
304#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
305pub struct PerMessageReportEntry {
306    pub original_envelope_id: Option<String>,
307    pub reporting_mta: RemoteMta,
308    pub dsn_gateway: Option<RemoteMta>,
309    pub received_from_mta: Option<RemoteMta>,
310    pub arrival_date: Option<DateTime<Utc>>,
311    pub extensions: BTreeMap<String, Vec<BString>>,
312}
313
314impl std::fmt::Display for PerMessageReportEntry {
315    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
316        write!(fmt, "Reporting-MTA: {}\r\n", self.reporting_mta)?;
317        if let Some(id) = &self.original_envelope_id {
318            write!(fmt, "Original-Envelope-Id: {id}\r\n")?;
319        }
320        if let Some(dsn_gateway) = &self.dsn_gateway {
321            write!(fmt, "DSN-Gateway: {dsn_gateway}\r\n")?;
322        }
323        if let Some(mta) = &self.received_from_mta {
324            write!(fmt, "Received-From-MTA: {mta}\r\n")?;
325        }
326        if let Some(when) = &self.arrival_date {
327            write!(fmt, "Arrival-Date: {}\r\n", when.to_rfc2822())?;
328        }
329        for (k, vlist) in &self.extensions {
330            for v in vlist {
331                write!(fmt, "{k}: {v}\r\n")?;
332            }
333        }
334
335        Ok(())
336    }
337}
338
339impl PerMessageReportEntry {
340    fn parse(part: &str) -> anyhow::Result<Self> {
341        let mut extensions = extract_headers(part.as_bytes())?;
342
343        let reporting_mta = extract_single_req("reporting-mta", &mut extensions)?;
344        let original_envelope_id = extract_single("original-envelope-id", &mut extensions)?;
345        let dsn_gateway = extract_single("dsn-gateway", &mut extensions)?;
346        let received_from_mta = extract_single("received-from-mta", &mut extensions)?;
347
348        let arrival_date =
349            extract_single_conv::<DateTimeRfc2822, DateTime<Utc>>("arrival-date", &mut extensions)?;
350
351        Ok(Self {
352            original_envelope_id,
353            reporting_mta,
354            dsn_gateway,
355            received_from_mta,
356            arrival_date,
357            extensions,
358        })
359    }
360}
361
362#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
363pub struct Report {
364    pub per_message: PerMessageReportEntry,
365    pub per_recipient: Vec<PerRecipientReportEntry>,
366    pub original_message: Option<BString>,
367}
368
369pub(crate) fn content_type(part: &MimePart) -> Option<BString> {
370    let ct = part.headers().content_type().ok()??;
371    Some(ct.value)
372}
373
374impl Report {
375    pub fn parse(input: &[u8]) -> anyhow::Result<Option<Self>> {
376        let mail = MimePart::parse(input).with_context(|| {
377            format!(
378                "Report::parse top; input is {:?}",
379                String::from_utf8_lossy(input)
380            )
381        })?;
382
383        if content_type(&mail).as_ref().map(|b| b.as_bstr()) != Some(BStr::new("multipart/report"))
384        {
385            return Ok(None);
386        }
387
388        let mut original_message = None;
389
390        for part in mail.child_parts() {
391            let ct = content_type(part);
392            let ct = ct.as_ref().map(|b| b.as_bstr());
393            if ct == Some(BStr::new("message/rfc822"))
394                || ct == Some(BStr::new("text/rfc822-headers"))
395            {
396                original_message = Some(BString::new(
397                    part.raw_body().as_bytes().replace(b"\r\n", b"\n"),
398                ));
399            }
400        }
401
402        for part in mail.child_parts() {
403            let ct = content_type(part);
404            let ct = ct.as_ref().map(|b| b.as_bstr());
405            if ct == Some(BStr::new("message/delivery-status"))
406                || ct == Some(BStr::new("message/global-delivery-status"))
407            {
408                return Ok(Some(Self::parse_inner(part, original_message)?));
409            }
410        }
411
412        anyhow::bail!("delivery-status part missing");
413    }
414
415    fn parse_inner(part: &MimePart, original_message: Option<BString>) -> anyhow::Result<Self> {
416        let body = part.body()?.to_string_lossy().replace("\r\n", "\n");
417        let mut parts = body.trim().split("\n\n");
418
419        let per_message = parts
420            .next()
421            .ok_or_else(|| anyhow!("missing per-message section"))?;
422        let per_message = PerMessageReportEntry::parse(per_message)?;
423        let mut per_recipient = vec![];
424        for part in parts {
425            let part = PerRecipientReportEntry::parse(part)?;
426            per_recipient.push(part);
427        }
428
429        Ok(Self {
430            per_message,
431            per_recipient,
432            original_message,
433        })
434    }
435
436    /// msg: the message that experienced an issue
437    /// log: the corresponding log record from the issue
438    pub fn generate(
439        params: &ReportGenerationParams,
440        msg: Option<&MimePart<'_>>,
441        log: &JsonLogRecord,
442    ) -> anyhow::Result<Option<MimePart<'static>>> {
443        if log.sender.is_empty() {
444            // Cannot send a bounce back to the null sender
445            return Ok(None);
446        }
447
448        let action = match &log.kind {
449            RecordType::Bounce
450                if params.enable_bounce && log.delivery_protocol.as_deref() == Some("ESMTP") =>
451            {
452                ReportAction::Failed
453            }
454            RecordType::Expiration if params.enable_expiration => ReportAction::Failed,
455            _ => return Ok(None),
456        };
457
458        let arrival_date = Some(log.created);
459
460        let per_message = PerMessageReportEntry {
461            arrival_date,
462            dsn_gateway: None,
463            extensions: Default::default(),
464            original_envelope_id: None,
465            received_from_mta: None,
466            reporting_mta: params.reporting_mta.clone(),
467        };
468
469        let mut per_recipient = vec![];
470        let recip_list = log.recipient.join(", ");
471
472        for recip in &log.recipient {
473            per_recipient.push(PerRecipientReportEntry {
474                action,
475                extensions: Default::default(),
476                status: (&log.response).into(),
477                diagnostic_code: Some(DiagnosticCode {
478                    diagnostic_type: "smtp".into(),
479                    diagnostic: log.response.to_single_line(),
480                }),
481                final_log_id: None,
482                original_recipient: None,
483                final_recipient: Recipient {
484                    recipient_type: "rfc822".to_string(),
485                    recipient: recip.to_string(),
486                },
487                remote_mta: log.peer_address.as_ref().map(|addr| RemoteMta {
488                    mta_type: "dns".to_string(),
489                    name: addr.name.to_string(),
490                }),
491                last_attempt_date: Some(log.timestamp),
492                will_retry_until: None,
493            });
494        }
495
496        let mut parts = vec![];
497
498        let exposition = match &log.kind {
499            RecordType::Bounce => {
500                let mut data = format!(
501                    "The message was received at {created}\r\n\
502                    from {sender} and addressed to {recip_list}.\r\n\
503                    ",
504                    created = log.created.to_rfc2822(),
505                    sender = log.sender,
506                );
507                if let Some(peer) = &log.peer_address {
508                    data.push_str(&format!(
509                        "While communicating with {host} ({ip}):\r\n\
510                        Response: {resp}\r\n",
511                        host = peer.name,
512                        ip = peer.addr,
513                        resp = log.response.to_single_line(),
514                    ));
515                } else {
516                    data.push_str(&format!("Status: {}\r\n", log.response.to_single_line()));
517                }
518
519                data.push_str(
520                    "\r\nThe message will be deleted from the queue.\r\n\
521                    No further attempts will be made to deliver it.\r\n",
522                );
523
524                data
525            }
526            RecordType::Expiration => {
527                format!(
528                    "The message was received at {created}\r\n\
529                    from {sender} and addressed to {recip_list}.\r\n\
530                    Status: {status}\r\n\
531                    The message will be deleted from the queue.\r\n\
532                    No further attempts will be made to deliver it.\r\n\
533                    ",
534                    created = log.created.to_rfc2822(),
535                    sender = log.sender,
536                    status = log.response.to_single_line()
537                )
538            }
539            _ => unreachable!(),
540        };
541
542        parts.push(MimePart::new_text_plain(&*exposition).context("new_text_plain")?);
543
544        let mut status_text = format!("{per_message}\r\n");
545        for per_recip in per_recipient {
546            status_text.push_str(&format!("{per_recip}\r\n"));
547        }
548        parts.push(
549            MimePart::new_text("message/delivery-status", &*status_text).context("new_text")?,
550        );
551
552        match (params.include_original_message, msg) {
553            (IncludeOriginalMessage::No, _) | (_, None) => {}
554            (IncludeOriginalMessage::HeadersOnly, Some(msg)) => {
555                let mut data = vec![];
556                for hdr in msg.headers().iter() {
557                    hdr.write_header(&mut data).ok();
558                }
559                parts.push(
560                    MimePart::new_no_transfer_encoding("text/rfc822-headers", &data)
561                        .context("new_no_transfer_encoding")?,
562                );
563            }
564            (IncludeOriginalMessage::FullContent, Some(msg)) => {
565                let mut data = vec![];
566                msg.write_message(&mut data).ok();
567                parts.push(
568                    MimePart::new_no_transfer_encoding("message/rfc822", &data)
569                        .context("new_no_transfer_encoding")?,
570                );
571            }
572        };
573
574        let mut report_msg = MimePart::new_multipart(
575            "multipart/report",
576            parts,
577            if params.stable_content {
578                Some(b"report-boundary")
579            } else {
580                None
581            },
582        )?;
583
584        let mut ct = report_msg
585            .headers()
586            .content_type()
587            .context("get content_type")?
588            .expect("assigned during construction");
589        ct.set("report-type", "delivery-status");
590        report_msg
591            .headers_mut()
592            .set_content_type(ct)
593            .context("set_content_type")?;
594        report_msg
595            .headers_mut()
596            .set_subject("Returned mail")
597            .context("set_subject")?;
598        report_msg
599            .headers_mut()
600            .set_mime_version("1.0")
601            .context("set_mime_version")?;
602
603        let message_id = if params.stable_content {
604            format!("<UUID@{}>", params.reporting_mta.name)
605        } else {
606            let id = uuid_helper::now_v1();
607            format!("<{id}@{}>", params.reporting_mta.name)
608        };
609        report_msg
610            .headers_mut()
611            .set_message_id(message_id.as_str())?;
612        report_msg
613            .headers_mut()
614            .set_to(log.sender.as_str())
615            .context("set_to")?;
616
617        let from = format!(
618            "Mail Delivery Subsystem <mailer-daemon@{}>",
619            params.reporting_mta.name
620        );
621        report_msg
622            .headers_mut()
623            .set_from(from.as_str())
624            .context("set_from")?;
625
626        Ok(Some(report_msg))
627    }
628}
629
630#[derive(Default, Debug, PartialEq, Clone, Copy, Deserialize)]
631pub enum IncludeOriginalMessage {
632    #[default]
633    No,
634    HeadersOnly,
635    FullContent,
636}
637
638#[derive(Debug, PartialEq, Clone, Deserialize)]
639#[serde(deny_unknown_fields)]
640pub struct ReportGenerationParams {
641    pub include_original_message: IncludeOriginalMessage,
642    #[serde(default)]
643    pub enable_expiration: bool,
644    #[serde(default)]
645    pub enable_bounce: bool,
646    // If we decide to allow generating for delays in the future,
647    // we'll probably add `enable_delay` here, but we'll also need
648    // to have some kind of discriminating logic to decide when
649    // to emit a DSN; probably should be a list of num_attempts
650    // on which to emit? This is too fiddly to design for right
651    // now, considering that none of our target userbase will
652    // emit DSNs for delayed mail.
653    pub reporting_mta: RemoteMta,
654
655    /// When used for testing, use a stable mime boundary
656    #[serde(default)]
657    pub stable_content: bool,
658}
659
660#[cfg(test)]
661mod test {
662    use super::*;
663    use crate::ResolvedAddress;
664    use rfc5321::{EnhancedStatusCode, Response};
665
666    fn make_message() -> MimePart<'static> {
667        let mut part = MimePart::new_text_plain("hello there").unwrap();
668        part.headers_mut().set_subject("Hello!").unwrap();
669
670        part
671    }
672
673    fn make_bounce() -> JsonLogRecord {
674        let nodeid = uuid_helper::now_v1();
675        let created =
676            chrono::DateTime::parse_from_rfc2822("Tue, 1 Jul 2003 10:52:37 +0200").unwrap();
677        let now = chrono::DateTime::parse_from_rfc2822("Tue, 1 Jul 2003 12:52:37 +0200").unwrap();
678        JsonLogRecord {
679            kind: RecordType::Bounce,
680            id: "ID".to_string(),
681            nodeid,
682            created: created.into(),
683            bounce_classification: Default::default(),
684            delivery_protocol: Some("ESMTP".to_string()),
685            egress_pool: None,
686            egress_source: None,
687            feedback_report: None,
688            headers: Default::default(),
689            meta: Default::default(),
690            num_attempts: 1,
691            peer_address: Some(ResolvedAddress {
692                name: "target.example.com".to_string(),
693                addr: "42.42.42.42".to_string().try_into().unwrap(),
694            }),
695            provider_name: None,
696            queue: "target.example.com".to_string(),
697            reception_protocol: None,
698            recipient: vec!["recip@target.example.com".to_string()],
699            sender: "sender@sender.example.com".to_string(),
700            session_id: None,
701            response: Response {
702                code: 550,
703                command: None,
704                content: "no thanks".to_string(),
705                enhanced_code: Some(EnhancedStatusCode {
706                    class: 5,
707                    subject: 7,
708                    detail: 1,
709                }),
710            },
711            site: "some-site".to_string(),
712            size: 0,
713            source_address: None,
714            timestamp: now.into(),
715            tls_cipher: None,
716            tls_peer_subject_name: None,
717            tls_protocol_version: None,
718        }
719    }
720
721    fn make_expiration() -> JsonLogRecord {
722        let nodeid = uuid_helper::now_v1();
723        let created =
724            chrono::DateTime::parse_from_rfc2822("Tue, 1 Jul 2003 10:52:37 +0200").unwrap();
725        let now = chrono::DateTime::parse_from_rfc2822("Tue, 1 Jul 2003 12:52:37 +0200").unwrap();
726        JsonLogRecord {
727            kind: RecordType::Expiration,
728            id: "ID".to_string(),
729            nodeid,
730            created: created.into(),
731            bounce_classification: Default::default(),
732            delivery_protocol: None,
733            egress_pool: None,
734            egress_source: None,
735            feedback_report: None,
736            headers: Default::default(),
737            meta: Default::default(),
738            num_attempts: 3,
739            peer_address: None,
740            provider_name: None,
741            queue: "target.example.com".to_string(),
742            reception_protocol: None,
743            recipient: vec!["recip@target.example.com".to_string()],
744            sender: "sender@sender.example.com".to_string(),
745            session_id: None,
746            response: Response {
747                code: 551,
748                command: None,
749                content: "Next delivery time would be at SOME TIME which exceeds the expiry time EXPIRES configured via set_scheduling".to_string(),
750                enhanced_code: Some(EnhancedStatusCode {
751                    class: 5,
752                    subject: 4,
753                    detail: 7,
754                }),
755            },
756            site: "".to_string(),
757            size: 0,
758            source_address: None,
759            timestamp: now.into(),
760            tls_cipher: None,
761            tls_peer_subject_name: None,
762            tls_protocol_version: None,
763        }
764    }
765
766    #[test]
767    fn generate_expiration_with_headers() {
768        let params = ReportGenerationParams {
769            reporting_mta: RemoteMta {
770                mta_type: "dns".to_string(),
771                name: "mta1.example.com".to_string(),
772            },
773            enable_bounce: false,
774            enable_expiration: true,
775            include_original_message: IncludeOriginalMessage::HeadersOnly,
776            stable_content: true,
777        };
778
779        let original_msg = make_message();
780
781        let log = make_expiration();
782
783        let report_msg = Report::generate(&params, Some(&original_msg), &log)
784            .unwrap()
785            .unwrap();
786        let report_eml = BString::from(report_msg.to_message_bytes());
787        k9::snapshot!(
788            &report_eml,
789            r#"
790Content-Type: multipart/report;\r
791\tboundary="report-boundary";\r
792\treport-type="delivery-status"\r
793Subject: Returned mail\r
794Mime-Version: 1.0\r
795Message-ID: <UUID@mta1.example.com>\r
796To: sender@sender.example.com\r
797From: Mail Delivery Subsystem <mailer-daemon@mta1.example.com>\r
798\r
799--report-boundary\r
800Content-Type: text/plain;\r
801\tcharset="us-ascii"\r
802Content-Transfer-Encoding: quoted-printable\r
803\r
804The message was received at Tue, 1 Jul 2003 08:52:37 +0000\r
805from sender@sender.example.com and addressed to recip@target.example.com.\r
806Status: 551 5.4.7 Next delivery time would be at SOME TIME which exceeds th=\r
807e expiry time EXPIRES configured via set_scheduling\r
808The message will be deleted from the queue.\r
809No further attempts will be made to deliver it.\r
810--report-boundary\r
811Content-Type: message/delivery-status;\r
812\tcharset="us-ascii"\r
813Content-Transfer-Encoding: quoted-printable\r
814\r
815Reporting-MTA: dns; mta1.example.com\r
816Arrival-Date: Tue, 1 Jul 2003 08:52:37 +0000\r
817\r
818Final-Recipient: rfc822;recip@target.example.com\r
819Action: failed\r
820Status: 5.4.7 Next delivery time would be at SOME TIME which exceeds the ex=\r
821piry time EXPIRES configured via set_scheduling\r
822Diagnostic-Code: smtp; 551 5.4.7 Next delivery time would be at SOME TIME w=\r
823hich exceeds the expiry time EXPIRES configured via set_scheduling\r
824Last-Attempt-Date: Tue, 1 Jul 2003 10:52:37 +0000\r
825\r
826--report-boundary\r
827Content-Type: text/rfc822-headers\r
828\r
829Content-Type: text/plain;\r
830\tcharset="us-ascii"\r
831Subject: Hello!\r
832--report-boundary--\r
833
834"#
835        );
836
837        let round_trip = Report::parse(report_eml.as_bytes()).unwrap().unwrap();
838        k9::snapshot!(
839            &round_trip,
840            r#"
841Report {
842    per_message: PerMessageReportEntry {
843        original_envelope_id: None,
844        reporting_mta: RemoteMta {
845            mta_type: "dns",
846            name: "mta1.example.com",
847        },
848        dsn_gateway: None,
849        received_from_mta: None,
850        arrival_date: Some(
851            2003-07-01T08:52:37Z,
852        ),
853        extensions: {},
854    },
855    per_recipient: [
856        PerRecipientReportEntry {
857            final_recipient: Recipient {
858                recipient_type: "rfc822",
859                recipient: "recip@target.example.com",
860            },
861            action: Failed,
862            status: ReportStatus {
863                class: 5,
864                subject: 4,
865                detail: 7,
866                comment: Some(
867                    "Next delivery time would be at SOME TIME which exceeds the expiry time EXPIRES configured via set_scheduling",
868                ),
869            },
870            original_recipient: None,
871            remote_mta: None,
872            diagnostic_code: Some(
873                DiagnosticCode {
874                    diagnostic_type: "smtp",
875                    diagnostic: "551 5.4.7 Next delivery time would be at SOME TIME which exceeds the expiry time EXPIRES configured via set_scheduling",
876                },
877            ),
878            last_attempt_date: Some(
879                2003-07-01T10:52:37Z,
880            ),
881            final_log_id: None,
882            will_retry_until: None,
883            extensions: {},
884        },
885    ],
886    original_message: Some(
887        "Content-Type: text/plain;
888\tcharset="us-ascii"
889Subject: Hello!
890",
891    ),
892}
893"#
894        );
895    }
896
897    #[test]
898    fn generate_bounce_with_headers() {
899        let params = ReportGenerationParams {
900            reporting_mta: RemoteMta {
901                mta_type: "dns".to_string(),
902                name: "mta1.example.com".to_string(),
903            },
904            enable_bounce: true,
905            enable_expiration: true,
906            include_original_message: IncludeOriginalMessage::HeadersOnly,
907            stable_content: true,
908        };
909
910        let original_msg = make_message();
911
912        let log = make_bounce();
913
914        let report_msg = Report::generate(&params, Some(&original_msg), &log)
915            .unwrap()
916            .unwrap();
917        let report_eml = BString::from(report_msg.to_message_bytes());
918        k9::snapshot!(
919            &report_eml,
920            r#"
921Content-Type: multipart/report;\r
922\tboundary="report-boundary";\r
923\treport-type="delivery-status"\r
924Subject: Returned mail\r
925Mime-Version: 1.0\r
926Message-ID: <UUID@mta1.example.com>\r
927To: sender@sender.example.com\r
928From: Mail Delivery Subsystem <mailer-daemon@mta1.example.com>\r
929\r
930--report-boundary\r
931Content-Type: text/plain;\r
932\tcharset="us-ascii"\r
933\r
934The message was received at Tue, 1 Jul 2003 08:52:37 +0000\r
935from sender@sender.example.com and addressed to recip@target.example.com.\r
936While communicating with target.example.com (42.42.42.42):\r
937Response: 550 5.7.1 no thanks\r
938\r
939The message will be deleted from the queue.\r
940No further attempts will be made to deliver it.\r
941--report-boundary\r
942Content-Type: message/delivery-status;\r
943\tcharset="us-ascii"\r
944\r
945Reporting-MTA: dns; mta1.example.com\r
946Arrival-Date: Tue, 1 Jul 2003 08:52:37 +0000\r
947\r
948Final-Recipient: rfc822;recip@target.example.com\r
949Action: failed\r
950Status: 5.7.1 no thanks\r
951Remote-MTA: dns; target.example.com\r
952Diagnostic-Code: smtp; 550 5.7.1 no thanks\r
953Last-Attempt-Date: Tue, 1 Jul 2003 10:52:37 +0000\r
954\r
955--report-boundary\r
956Content-Type: text/rfc822-headers\r
957\r
958Content-Type: text/plain;\r
959\tcharset="us-ascii"\r
960Subject: Hello!\r
961--report-boundary--\r
962
963"#
964        );
965
966        let round_trip = Report::parse(report_eml.as_bytes()).unwrap().unwrap();
967        k9::snapshot!(
968            &round_trip,
969            r#"
970Report {
971    per_message: PerMessageReportEntry {
972        original_envelope_id: None,
973        reporting_mta: RemoteMta {
974            mta_type: "dns",
975            name: "mta1.example.com",
976        },
977        dsn_gateway: None,
978        received_from_mta: None,
979        arrival_date: Some(
980            2003-07-01T08:52:37Z,
981        ),
982        extensions: {},
983    },
984    per_recipient: [
985        PerRecipientReportEntry {
986            final_recipient: Recipient {
987                recipient_type: "rfc822",
988                recipient: "recip@target.example.com",
989            },
990            action: Failed,
991            status: ReportStatus {
992                class: 5,
993                subject: 7,
994                detail: 1,
995                comment: Some(
996                    "no thanks",
997                ),
998            },
999            original_recipient: None,
1000            remote_mta: Some(
1001                RemoteMta {
1002                    mta_type: "dns",
1003                    name: "target.example.com",
1004                },
1005            ),
1006            diagnostic_code: Some(
1007                DiagnosticCode {
1008                    diagnostic_type: "smtp",
1009                    diagnostic: "550 5.7.1 no thanks",
1010                },
1011            ),
1012            last_attempt_date: Some(
1013                2003-07-01T10:52:37Z,
1014            ),
1015            final_log_id: None,
1016            will_retry_until: None,
1017            extensions: {},
1018        },
1019    ],
1020    original_message: Some(
1021        "Content-Type: text/plain;
1022\tcharset="us-ascii"
1023Subject: Hello!
1024",
1025    ),
1026}
1027"#
1028        );
1029    }
1030    #[test]
1031    fn generate_bounce_with_message() {
1032        let params = ReportGenerationParams {
1033            reporting_mta: RemoteMta {
1034                mta_type: "dns".to_string(),
1035                name: "mta1.example.com".to_string(),
1036            },
1037            enable_bounce: true,
1038            enable_expiration: true,
1039            include_original_message: IncludeOriginalMessage::FullContent,
1040            stable_content: true,
1041        };
1042
1043        let original_msg = make_message();
1044
1045        let log = make_bounce();
1046
1047        let report_msg = Report::generate(&params, Some(&original_msg), &log)
1048            .unwrap()
1049            .unwrap();
1050        let report_eml = BString::from(report_msg.to_message_bytes());
1051        k9::snapshot!(
1052            &report_eml,
1053            r#"
1054Content-Type: multipart/report;\r
1055\tboundary="report-boundary";\r
1056\treport-type="delivery-status"\r
1057Subject: Returned mail\r
1058Mime-Version: 1.0\r
1059Message-ID: <UUID@mta1.example.com>\r
1060To: sender@sender.example.com\r
1061From: Mail Delivery Subsystem <mailer-daemon@mta1.example.com>\r
1062\r
1063--report-boundary\r
1064Content-Type: text/plain;\r
1065\tcharset="us-ascii"\r
1066\r
1067The message was received at Tue, 1 Jul 2003 08:52:37 +0000\r
1068from sender@sender.example.com and addressed to recip@target.example.com.\r
1069While communicating with target.example.com (42.42.42.42):\r
1070Response: 550 5.7.1 no thanks\r
1071\r
1072The message will be deleted from the queue.\r
1073No further attempts will be made to deliver it.\r
1074--report-boundary\r
1075Content-Type: message/delivery-status;\r
1076\tcharset="us-ascii"\r
1077\r
1078Reporting-MTA: dns; mta1.example.com\r
1079Arrival-Date: Tue, 1 Jul 2003 08:52:37 +0000\r
1080\r
1081Final-Recipient: rfc822;recip@target.example.com\r
1082Action: failed\r
1083Status: 5.7.1 no thanks\r
1084Remote-MTA: dns; target.example.com\r
1085Diagnostic-Code: smtp; 550 5.7.1 no thanks\r
1086Last-Attempt-Date: Tue, 1 Jul 2003 10:52:37 +0000\r
1087\r
1088--report-boundary\r
1089Content-Type: message/rfc822\r
1090\r
1091Content-Type: text/plain;\r
1092\tcharset="us-ascii"\r
1093Subject: Hello!\r
1094\r
1095hello there\r
1096--report-boundary--\r
1097
1098"#
1099        );
1100
1101        let round_trip = Report::parse(report_eml.as_bytes()).unwrap().unwrap();
1102        k9::snapshot!(
1103            &round_trip,
1104            r#"
1105Report {
1106    per_message: PerMessageReportEntry {
1107        original_envelope_id: None,
1108        reporting_mta: RemoteMta {
1109            mta_type: "dns",
1110            name: "mta1.example.com",
1111        },
1112        dsn_gateway: None,
1113        received_from_mta: None,
1114        arrival_date: Some(
1115            2003-07-01T08:52:37Z,
1116        ),
1117        extensions: {},
1118    },
1119    per_recipient: [
1120        PerRecipientReportEntry {
1121            final_recipient: Recipient {
1122                recipient_type: "rfc822",
1123                recipient: "recip@target.example.com",
1124            },
1125            action: Failed,
1126            status: ReportStatus {
1127                class: 5,
1128                subject: 7,
1129                detail: 1,
1130                comment: Some(
1131                    "no thanks",
1132                ),
1133            },
1134            original_recipient: None,
1135            remote_mta: Some(
1136                RemoteMta {
1137                    mta_type: "dns",
1138                    name: "target.example.com",
1139                },
1140            ),
1141            diagnostic_code: Some(
1142                DiagnosticCode {
1143                    diagnostic_type: "smtp",
1144                    diagnostic: "550 5.7.1 no thanks",
1145                },
1146            ),
1147            last_attempt_date: Some(
1148                2003-07-01T10:52:37Z,
1149            ),
1150            final_log_id: None,
1151            will_retry_until: None,
1152            extensions: {},
1153        },
1154    ],
1155    original_message: Some(
1156        "Content-Type: text/plain;
1157\tcharset="us-ascii"
1158Subject: Hello!
1159
1160hello there
1161",
1162    ),
1163}
1164"#
1165        );
1166    }
1167
1168    #[test]
1169    fn generate_bounce_no_message() {
1170        let params = ReportGenerationParams {
1171            reporting_mta: RemoteMta {
1172                mta_type: "dns".to_string(),
1173                name: "mta1.example.com".to_string(),
1174            },
1175            enable_bounce: true,
1176            enable_expiration: true,
1177            include_original_message: IncludeOriginalMessage::No,
1178            stable_content: true,
1179        };
1180
1181        let original_msg = make_message();
1182
1183        let log = make_bounce();
1184
1185        let report_msg = Report::generate(&params, Some(&original_msg), &log)
1186            .unwrap()
1187            .unwrap();
1188        let report_eml = BString::from(report_msg.to_message_bytes());
1189        k9::snapshot!(
1190            &report_eml,
1191            r#"
1192Content-Type: multipart/report;\r
1193\tboundary="report-boundary";\r
1194\treport-type="delivery-status"\r
1195Subject: Returned mail\r
1196Mime-Version: 1.0\r
1197Message-ID: <UUID@mta1.example.com>\r
1198To: sender@sender.example.com\r
1199From: Mail Delivery Subsystem <mailer-daemon@mta1.example.com>\r
1200\r
1201--report-boundary\r
1202Content-Type: text/plain;\r
1203\tcharset="us-ascii"\r
1204\r
1205The message was received at Tue, 1 Jul 2003 08:52:37 +0000\r
1206from sender@sender.example.com and addressed to recip@target.example.com.\r
1207While communicating with target.example.com (42.42.42.42):\r
1208Response: 550 5.7.1 no thanks\r
1209\r
1210The message will be deleted from the queue.\r
1211No further attempts will be made to deliver it.\r
1212--report-boundary\r
1213Content-Type: message/delivery-status;\r
1214\tcharset="us-ascii"\r
1215\r
1216Reporting-MTA: dns; mta1.example.com\r
1217Arrival-Date: Tue, 1 Jul 2003 08:52:37 +0000\r
1218\r
1219Final-Recipient: rfc822;recip@target.example.com\r
1220Action: failed\r
1221Status: 5.7.1 no thanks\r
1222Remote-MTA: dns; target.example.com\r
1223Diagnostic-Code: smtp; 550 5.7.1 no thanks\r
1224Last-Attempt-Date: Tue, 1 Jul 2003 10:52:37 +0000\r
1225\r
1226--report-boundary--\r
1227
1228"#
1229        );
1230
1231        let round_trip = Report::parse(report_eml.as_bytes()).unwrap().unwrap();
1232        k9::snapshot!(
1233            &round_trip,
1234            r#"
1235Report {
1236    per_message: PerMessageReportEntry {
1237        original_envelope_id: None,
1238        reporting_mta: RemoteMta {
1239            mta_type: "dns",
1240            name: "mta1.example.com",
1241        },
1242        dsn_gateway: None,
1243        received_from_mta: None,
1244        arrival_date: Some(
1245            2003-07-01T08:52:37Z,
1246        ),
1247        extensions: {},
1248    },
1249    per_recipient: [
1250        PerRecipientReportEntry {
1251            final_recipient: Recipient {
1252                recipient_type: "rfc822",
1253                recipient: "recip@target.example.com",
1254            },
1255            action: Failed,
1256            status: ReportStatus {
1257                class: 5,
1258                subject: 7,
1259                detail: 1,
1260                comment: Some(
1261                    "no thanks",
1262                ),
1263            },
1264            original_recipient: None,
1265            remote_mta: Some(
1266                RemoteMta {
1267                    mta_type: "dns",
1268                    name: "target.example.com",
1269                },
1270            ),
1271            diagnostic_code: Some(
1272                DiagnosticCode {
1273                    diagnostic_type: "smtp",
1274                    diagnostic: "550 5.7.1 no thanks",
1275                },
1276            ),
1277            last_attempt_date: Some(
1278                2003-07-01T10:52:37Z,
1279            ),
1280            final_log_id: None,
1281            will_retry_until: None,
1282            extensions: {},
1283        },
1284    ],
1285    original_message: None,
1286}
1287"#
1288        );
1289    }
1290
1291    #[test]
1292    fn rfc3464_1() {
1293        let result = Report::parse(include_bytes!("../data/rfc3464/1.eml")).unwrap();
1294        k9::snapshot!(
1295            &result,
1296            r#"
1297Some(
1298    Report {
1299        per_message: PerMessageReportEntry {
1300            original_envelope_id: None,
1301            reporting_mta: RemoteMta {
1302                mta_type: "dns",
1303                name: "cs.utk.edu",
1304            },
1305            dsn_gateway: None,
1306            received_from_mta: None,
1307            arrival_date: None,
1308            extensions: {},
1309        },
1310        per_recipient: [
1311            PerRecipientReportEntry {
1312                final_recipient: Recipient {
1313                    recipient_type: "rfc822",
1314                    recipient: "louisl@larry.slip.umd.edu",
1315                },
1316                action: Failed,
1317                status: ReportStatus {
1318                    class: 4,
1319                    subject: 0,
1320                    detail: 0,
1321                    comment: None,
1322                },
1323                original_recipient: Some(
1324                    Recipient {
1325                        recipient_type: "rfc822",
1326                        recipient: "louisl@larry.slip.umd.edu",
1327                    },
1328                ),
1329                remote_mta: None,
1330                diagnostic_code: Some(
1331                    DiagnosticCode {
1332                        diagnostic_type: "smtp",
1333                        diagnostic: "426 connection timed out",
1334                    },
1335                ),
1336                last_attempt_date: Some(
1337                    1994-07-07T21:15:49Z,
1338                ),
1339                final_log_id: None,
1340                will_retry_until: None,
1341                extensions: {},
1342            },
1343        ],
1344        original_message: Some(
1345            "[original message goes here]
1346
1347",
1348        ),
1349    },
1350)
1351"#
1352        );
1353
1354        let report = result.unwrap();
1355
1356        assert_eq!(
1357            report.per_message.to_string(),
1358            "Reporting-MTA: dns; cs.utk.edu\r\n"
1359        );
1360        assert_eq!(
1361            report.per_recipient[0].to_string(),
1362            "Original-Recipient: rfc822;louisl@larry.slip.umd.edu\r\n\
1363            Final-Recipient: rfc822;louisl@larry.slip.umd.edu\r\n\
1364            Action: failed\r\n\
1365            Status: 4.0.0\r\n\
1366            Diagnostic-Code: smtp; 426 connection timed out\r\n\
1367            Last-Attempt-Date: Thu, 7 Jul 1994 21:15:49 +0000\r\n"
1368        );
1369    }
1370
1371    #[test]
1372    fn rfc3464_2() {
1373        let result = Report::parse(include_bytes!("../data/rfc3464/2.eml")).unwrap();
1374        k9::snapshot!(
1375            result,
1376            r#"
1377Some(
1378    Report {
1379        per_message: PerMessageReportEntry {
1380            original_envelope_id: None,
1381            reporting_mta: RemoteMta {
1382                mta_type: "dns",
1383                name: "cs.utk.edu",
1384            },
1385            dsn_gateway: None,
1386            received_from_mta: None,
1387            arrival_date: None,
1388            extensions: {},
1389        },
1390        per_recipient: [
1391            PerRecipientReportEntry {
1392                final_recipient: Recipient {
1393                    recipient_type: "rfc822",
1394                    recipient: "arathib@vnet.ibm.com",
1395                },
1396                action: Failed,
1397                status: ReportStatus {
1398                    class: 5,
1399                    subject: 0,
1400                    detail: 0,
1401                    comment: Some(
1402                        "(permanent failure)",
1403                    ),
1404                },
1405                original_recipient: Some(
1406                    Recipient {
1407                        recipient_type: "rfc822",
1408                        recipient: "arathib@vnet.ibm.com",
1409                    },
1410                ),
1411                remote_mta: Some(
1412                    RemoteMta {
1413                        mta_type: "dns",
1414                        name: "vnet.ibm.com",
1415                    },
1416                ),
1417                diagnostic_code: Some(
1418                    DiagnosticCode {
1419                        diagnostic_type: "smtp",
1420                        diagnostic: "550 'arathib@vnet.IBM.COM' is not a registered gateway user",
1421                    },
1422                ),
1423                last_attempt_date: None,
1424                final_log_id: None,
1425                will_retry_until: None,
1426                extensions: {},
1427            },
1428            PerRecipientReportEntry {
1429                final_recipient: Recipient {
1430                    recipient_type: "rfc822",
1431                    recipient: "johnh@hpnjld.njd.hp.com",
1432                },
1433                action: Delayed,
1434                status: ReportStatus {
1435                    class: 4,
1436                    subject: 0,
1437                    detail: 0,
1438                    comment: Some(
1439                        "(hpnjld.njd.jp.com: host name lookup failure)",
1440                    ),
1441                },
1442                original_recipient: Some(
1443                    Recipient {
1444                        recipient_type: "rfc822",
1445                        recipient: "johnh@hpnjld.njd.hp.com",
1446                    },
1447                ),
1448                remote_mta: None,
1449                diagnostic_code: None,
1450                last_attempt_date: None,
1451                final_log_id: None,
1452                will_retry_until: None,
1453                extensions: {},
1454            },
1455            PerRecipientReportEntry {
1456                final_recipient: Recipient {
1457                    recipient_type: "rfc822",
1458                    recipient: "wsnell@sdcc13.ucsd.edu",
1459                },
1460                action: Failed,
1461                status: ReportStatus {
1462                    class: 5,
1463                    subject: 0,
1464                    detail: 0,
1465                    comment: None,
1466                },
1467                original_recipient: Some(
1468                    Recipient {
1469                        recipient_type: "rfc822",
1470                        recipient: "wsnell@sdcc13.ucsd.edu",
1471                    },
1472                ),
1473                remote_mta: Some(
1474                    RemoteMta {
1475                        mta_type: "dns",
1476                        name: "sdcc13.ucsd.edu",
1477                    },
1478                ),
1479                diagnostic_code: Some(
1480                    DiagnosticCode {
1481                        diagnostic_type: "smtp",
1482                        diagnostic: "550 user unknown",
1483                    },
1484                ),
1485                last_attempt_date: None,
1486                final_log_id: None,
1487                will_retry_until: None,
1488                extensions: {},
1489            },
1490        ],
1491        original_message: Some(
1492            "[original message goes here]
1493
1494",
1495        ),
1496    },
1497)
1498"#
1499        );
1500    }
1501
1502    #[test]
1503    fn rfc3464_3() {
1504        let result = Report::parse(include_bytes!("../data/rfc3464/3.eml")).unwrap();
1505        k9::snapshot!(
1506            result,
1507            r#"
1508Some(
1509    Report {
1510        per_message: PerMessageReportEntry {
1511            original_envelope_id: None,
1512            reporting_mta: RemoteMta {
1513                mta_type: "mailbus",
1514                name: "SYS30",
1515            },
1516            dsn_gateway: None,
1517            received_from_mta: None,
1518            arrival_date: None,
1519            extensions: {},
1520        },
1521        per_recipient: [
1522            PerRecipientReportEntry {
1523                final_recipient: Recipient {
1524                    recipient_type: "unknown",
1525                    recipient: "nair_s",
1526                },
1527                action: Failed,
1528                status: ReportStatus {
1529                    class: 5,
1530                    subject: 0,
1531                    detail: 0,
1532                    comment: Some(
1533                        "(unknown permanent failure)",
1534                    ),
1535                },
1536                original_recipient: None,
1537                remote_mta: None,
1538                diagnostic_code: None,
1539                last_attempt_date: None,
1540                final_log_id: None,
1541                will_retry_until: None,
1542                extensions: {},
1543            },
1544        ],
1545        original_message: None,
1546    },
1547)
1548"#
1549        );
1550    }
1551
1552    #[test]
1553    fn rfc3464_4() {
1554        let result = Report::parse(include_bytes!("../data/rfc3464/4.eml")).unwrap();
1555        k9::snapshot!(
1556            result,
1557            r#"
1558Some(
1559    Report {
1560        per_message: PerMessageReportEntry {
1561            original_envelope_id: None,
1562            reporting_mta: RemoteMta {
1563                mta_type: "dns",
1564                name: "sun2.nsfnet-relay.ac.uk",
1565            },
1566            dsn_gateway: None,
1567            received_from_mta: None,
1568            arrival_date: None,
1569            extensions: {},
1570        },
1571        per_recipient: [
1572            PerRecipientReportEntry {
1573                final_recipient: Recipient {
1574                    recipient_type: "rfc822",
1575                    recipient: "thomas@de-montfort.ac.uk",
1576                },
1577                action: Delayed,
1578                status: ReportStatus {
1579                    class: 4,
1580                    subject: 0,
1581                    detail: 0,
1582                    comment: Some(
1583                        "(unknown temporary failure)",
1584                    ),
1585                },
1586                original_recipient: None,
1587                remote_mta: None,
1588                diagnostic_code: None,
1589                last_attempt_date: None,
1590                final_log_id: None,
1591                will_retry_until: None,
1592                extensions: {},
1593            },
1594        ],
1595        original_message: None,
1596    },
1597)
1598"#
1599        );
1600    }
1601
1602    #[test]
1603    fn rfc3464_5() {
1604        let result = Report::parse(include_bytes!("../data/rfc3464/5.eml")).unwrap();
1605        k9::snapshot!(
1606            result,
1607            r#"
1608Some(
1609    Report {
1610        per_message: PerMessageReportEntry {
1611            original_envelope_id: None,
1612            reporting_mta: RemoteMta {
1613                mta_type: "dns",
1614                name: "mx-by.bbox.fr",
1615            },
1616            dsn_gateway: None,
1617            received_from_mta: None,
1618            arrival_date: Some(
1619                2025-01-29T16:36:51Z,
1620            ),
1621            extensions: {
1622                "x-postfix-queue-id": [
1623                    "897DAC0",
1624                ],
1625                "x-postfix-sender": [
1626                    "rfc822; user@example.com",
1627                ],
1628            },
1629        },
1630        per_recipient: [
1631            PerRecipientReportEntry {
1632                final_recipient: Recipient {
1633                    recipient_type: "rfc822",
1634                    recipient: "recipient@domain.com",
1635                },
1636                action: Failed,
1637                status: ReportStatus {
1638                    class: 5,
1639                    subject: 0,
1640                    detail: 0,
1641                    comment: None,
1642                },
1643                original_recipient: Some(
1644                    Recipient {
1645                        recipient_type: "rfc822",
1646                        recipient: "recipient@domain.com",
1647                    },
1648                ),
1649                remote_mta: Some(
1650                    RemoteMta {
1651                        mta_type: "dns",
1652                        name: "lmtp.cs.dolmen.bouyguestelecom.fr",
1653                    },
1654                ),
1655                diagnostic_code: Some(
1656                    DiagnosticCode {
1657                        diagnostic_type: "smtp",
1658                        diagnostic: "552 <recipient@domain.com> rejected: over quota",
1659                    },
1660                ),
1661                last_attempt_date: None,
1662                final_log_id: None,
1663                will_retry_until: None,
1664                extensions: {},
1665            },
1666        ],
1667        original_message: Some(
1668            "[original message goes here]
1669
1670",
1671        ),
1672    },
1673)
1674"#
1675        );
1676    }
1677
1678    #[test]
1679    fn rfc3464_6() {
1680        let result = Report::parse(include_bytes!("../data/rfc3464/6.eml")).unwrap();
1681        k9::snapshot!(
1682            result,
1683            r#"
1684Some(
1685    Report {
1686        per_message: PerMessageReportEntry {
1687            original_envelope_id: None,
1688            reporting_mta: RemoteMta {
1689                mta_type: "dns",
1690                name: "tls02.example.com",
1691            },
1692            dsn_gateway: None,
1693            received_from_mta: None,
1694            arrival_date: None,
1695            extensions: {},
1696        },
1697        per_recipient: [
1698            PerRecipientReportEntry {
1699                final_recipient: Recipient {
1700                    recipient_type: "rfc822",
1701                    recipient: "redacted@example.com",
1702                },
1703                action: Failed,
1704                status: ReportStatus {
1705                    class: 5,
1706                    subject: 0,
1707                    detail: 0,
1708                    comment: None,
1709                },
1710                original_recipient: Some(
1711                    Recipient {
1712                        recipient_type: "rfc822",
1713                        recipient: "redacted@example.com",
1714                    },
1715                ),
1716                remote_mta: Some(
1717                    RemoteMta {
1718                        mta_type: "dns",
1719                        name: "example-com.mail.eo.outlook.com:25",
1720                    },
1721                ),
1722                diagnostic_code: Some(
1723                    DiagnosticCode {
1724                        diagnostic_type: "smtp",
1725                        diagnostic: "host example-com.mail.eo.outlook.com:25 says: 550 5.4.1 Recipient address rejected: Access denied. For more information see https://aka.ms/EXOSmtpErrors [XXX.namprd05.prod.outlook.com 2026-03-13T18:10:42.797Z XXX]",
1726                    },
1727                ),
1728                last_attempt_date: None,
1729                final_log_id: None,
1730                will_retry_until: None,
1731                extensions: {},
1732            },
1733        ],
1734        original_message: Some(
1735            "Subject: [Bulk Mail] the subject
1736From: INFO <info@email.example.com>
1737To: redacted@example.com
1738
1739",
1740        ),
1741    },
1742)
1743"#
1744        );
1745    }
1746}