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