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        if log.sender.is_empty() {
425            // Cannot send a bounce back to the null sender
426            return Ok(None);
427        }
428
429        let action = match &log.kind {
430            RecordType::Bounce
431                if params.enable_bounce && log.delivery_protocol.as_deref() == Some("ESMTP") =>
432            {
433                ReportAction::Failed
434            }
435            RecordType::Expiration if params.enable_expiration => ReportAction::Failed,
436            _ => return Ok(None),
437        };
438
439        let arrival_date = Some(log.created);
440
441        let per_message = PerMessageReportEntry {
442            arrival_date,
443            dsn_gateway: None,
444            extensions: Default::default(),
445            original_envelope_id: None,
446            received_from_mta: None,
447            reporting_mta: params.reporting_mta.clone(),
448        };
449
450        let mut per_recipient = vec![];
451        let recip_list = log.recipient.join(", ");
452
453        for recip in &log.recipient {
454            per_recipient.push(PerRecipientReportEntry {
455                action,
456                extensions: Default::default(),
457                status: (&log.response).into(),
458                diagnostic_code: Some(DiagnosticCode {
459                    diagnostic_type: "smtp".into(),
460                    diagnostic: log.response.to_single_line(),
461                }),
462                final_log_id: None,
463                original_recipient: None,
464                final_recipient: Recipient {
465                    recipient_type: "rfc822".to_string(),
466                    recipient: recip.to_string(),
467                },
468                remote_mta: log.peer_address.as_ref().map(|addr| RemoteMta {
469                    mta_type: "dns".to_string(),
470                    name: addr.name.to_string(),
471                }),
472                last_attempt_date: Some(log.timestamp),
473                will_retry_until: None,
474            });
475        }
476
477        let mut parts = vec![];
478
479        let exposition = match &log.kind {
480            RecordType::Bounce => {
481                let mut data = format!(
482                    "The message was received at {created}\r\n\
483                    from {sender} and addressed to {recip_list}.\r\n\
484                    ",
485                    created = log.created.to_rfc2822(),
486                    sender = log.sender,
487                );
488                if let Some(peer) = &log.peer_address {
489                    data.push_str(&format!(
490                        "While communicating with {host} ({ip}):\r\n\
491                        Response: {resp}\r\n",
492                        host = peer.name,
493                        ip = peer.addr,
494                        resp = log.response.to_single_line(),
495                    ));
496                } else {
497                    data.push_str(&format!("Status: {}\r\n", log.response.to_single_line()));
498                }
499
500                data.push_str(
501                    "\r\nThe message will be deleted from the queue.\r\n\
502                    No further attempts will be made to deliver it.\r\n",
503                );
504
505                data
506            }
507            RecordType::Expiration => {
508                format!(
509                    "The message was received at {created}\r\n\
510                    from {sender} and addressed to {recip_list}.\r\n\
511                    Status: {status}\r\n\
512                    The message will be deleted from the queue.\r\n\
513                    No further attempts will be made to deliver it.\r\n\
514                    ",
515                    created = log.created.to_rfc2822(),
516                    sender = log.sender,
517                    status = log.response.to_single_line()
518                )
519            }
520            _ => unreachable!(),
521        };
522
523        parts.push(MimePart::new_text_plain(&exposition).context("new_text_plain")?);
524
525        let mut status_text = format!("{per_message}\r\n");
526        for per_recip in per_recipient {
527            status_text.push_str(&format!("{per_recip}\r\n"));
528        }
529        parts
530            .push(MimePart::new_text("message/delivery-status", &status_text).context("new_text")?);
531
532        match (params.include_original_message, msg) {
533            (IncludeOriginalMessage::No, _) | (_, None) => {}
534            (IncludeOriginalMessage::HeadersOnly, Some(msg)) => {
535                let mut data = vec![];
536                for hdr in msg.headers().iter() {
537                    hdr.write_header(&mut data).ok();
538                }
539                parts.push(
540                    MimePart::new_no_transfer_encoding("text/rfc822-headers", &data)
541                        .context("new_no_transfer_encoding")?,
542                );
543            }
544            (IncludeOriginalMessage::FullContent, Some(msg)) => {
545                let mut data = vec![];
546                msg.write_message(&mut data).ok();
547                parts.push(
548                    MimePart::new_no_transfer_encoding("message/rfc822", &data)
549                        .context("new_no_transfer_encoding")?,
550                );
551            }
552        };
553
554        let mut report_msg = MimePart::new_multipart(
555            "multipart/report",
556            parts,
557            if params.stable_content {
558                Some("report-boundary")
559            } else {
560                None
561            },
562        )?;
563
564        let mut ct = report_msg
565            .headers()
566            .content_type()
567            .context("get content_type")?
568            .expect("assigned during construction");
569        ct.set("report-type", "delivery-status");
570        report_msg
571            .headers_mut()
572            .set_content_type(ct)
573            .context("set_content_type")?;
574        report_msg
575            .headers_mut()
576            .set_subject("Returned mail")
577            .context("set_subject")?;
578        report_msg
579            .headers_mut()
580            .set_mime_version("1.0")
581            .context("set_mime_version")?;
582
583        let message_id = if params.stable_content {
584            format!("<UUID@{}>", params.reporting_mta.name)
585        } else {
586            let id = uuid_helper::now_v1();
587            format!("<{id}@{}>", params.reporting_mta.name)
588        };
589        report_msg
590            .headers_mut()
591            .set_message_id(message_id.as_str())?;
592        report_msg
593            .headers_mut()
594            .set_to(log.sender.as_str())
595            .context("set_to")?;
596
597        let from = format!(
598            "Mail Delivery Subsystem <mailer-daemon@{}>",
599            params.reporting_mta.name
600        );
601        report_msg
602            .headers_mut()
603            .set_from(from.as_str())
604            .context("set_from")?;
605
606        Ok(Some(report_msg))
607    }
608}
609
610#[derive(Default, Debug, PartialEq, Clone, Copy, Deserialize)]
611pub enum IncludeOriginalMessage {
612    #[default]
613    No,
614    HeadersOnly,
615    FullContent,
616}
617
618#[derive(Debug, PartialEq, Clone, Deserialize)]
619#[serde(deny_unknown_fields)]
620pub struct ReportGenerationParams {
621    pub include_original_message: IncludeOriginalMessage,
622    #[serde(default)]
623    pub enable_expiration: bool,
624    #[serde(default)]
625    pub enable_bounce: bool,
626    // If we decide to allow generating for delays in the future,
627    // we'll probably add `enable_delay` here, but we'll also need
628    // to have some kind of discriminating logic to decide when
629    // to emit a DSN; probably should be a list of num_attempts
630    // on which to emit? This is too fiddly to design for right
631    // now, considering that none of our target userbase will
632    // emit DSNs for delayed mail.
633    pub reporting_mta: RemoteMta,
634
635    /// When used for testing, use a stable mime boundary
636    #[serde(default)]
637    pub stable_content: bool,
638}
639
640#[cfg(test)]
641mod test {
642    use super::*;
643    use crate::ResolvedAddress;
644    use rfc5321::{EnhancedStatusCode, Response};
645
646    fn make_message() -> MimePart<'static> {
647        let mut part = MimePart::new_text_plain("hello there").unwrap();
648        part.headers_mut().set_subject("Hello!").unwrap();
649
650        part
651    }
652
653    fn make_bounce() -> JsonLogRecord {
654        let nodeid = uuid_helper::now_v1();
655        let created =
656            chrono::DateTime::parse_from_rfc2822("Tue, 1 Jul 2003 10:52:37 +0200").unwrap();
657        let now = chrono::DateTime::parse_from_rfc2822("Tue, 1 Jul 2003 12:52:37 +0200").unwrap();
658        JsonLogRecord {
659            kind: RecordType::Bounce,
660            id: "ID".to_string(),
661            nodeid,
662            created: created.into(),
663            bounce_classification: Default::default(),
664            delivery_protocol: Some("ESMTP".to_string()),
665            egress_pool: None,
666            egress_source: None,
667            feedback_report: None,
668            headers: Default::default(),
669            meta: Default::default(),
670            num_attempts: 1,
671            peer_address: Some(ResolvedAddress {
672                name: "target.example.com".to_string(),
673                addr: "42.42.42.42".to_string().try_into().unwrap(),
674            }),
675            provider_name: None,
676            queue: "target.example.com".to_string(),
677            reception_protocol: None,
678            recipient: vec!["recip@target.example.com".to_string()],
679            sender: "sender@sender.example.com".to_string(),
680            session_id: None,
681            response: Response {
682                code: 550,
683                command: None,
684                content: "no thanks".to_string(),
685                enhanced_code: Some(EnhancedStatusCode {
686                    class: 5,
687                    subject: 7,
688                    detail: 1,
689                }),
690            },
691            site: "some-site".to_string(),
692            size: 0,
693            source_address: None,
694            timestamp: now.into(),
695            tls_cipher: None,
696            tls_peer_subject_name: None,
697            tls_protocol_version: None,
698        }
699    }
700
701    fn make_expiration() -> JsonLogRecord {
702        let nodeid = uuid_helper::now_v1();
703        let created =
704            chrono::DateTime::parse_from_rfc2822("Tue, 1 Jul 2003 10:52:37 +0200").unwrap();
705        let now = chrono::DateTime::parse_from_rfc2822("Tue, 1 Jul 2003 12:52:37 +0200").unwrap();
706        JsonLogRecord {
707            kind: RecordType::Expiration,
708            id: "ID".to_string(),
709            nodeid,
710            created: created.into(),
711            bounce_classification: Default::default(),
712            delivery_protocol: None,
713            egress_pool: None,
714            egress_source: None,
715            feedback_report: None,
716            headers: Default::default(),
717            meta: Default::default(),
718            num_attempts: 3,
719            peer_address: None,
720            provider_name: None,
721            queue: "target.example.com".to_string(),
722            reception_protocol: None,
723            recipient: vec!["recip@target.example.com".to_string()],
724            sender: "sender@sender.example.com".to_string(),
725            session_id: None,
726            response: Response {
727                code: 551,
728                command: None,
729                content: "Next delivery time would be at SOME TIME which exceeds the expiry time EXPIRES configured via set_scheduling".to_string(),
730                enhanced_code: Some(EnhancedStatusCode {
731                    class: 5,
732                    subject: 4,
733                    detail: 7,
734                }),
735            },
736            site: "".to_string(),
737            size: 0,
738            source_address: None,
739            timestamp: now.into(),
740            tls_cipher: None,
741            tls_peer_subject_name: None,
742            tls_protocol_version: None,
743        }
744    }
745
746    #[test]
747    fn generate_expiration_with_headers() {
748        let params = ReportGenerationParams {
749            reporting_mta: RemoteMta {
750                mta_type: "dns".to_string(),
751                name: "mta1.example.com".to_string(),
752            },
753            enable_bounce: false,
754            enable_expiration: true,
755            include_original_message: IncludeOriginalMessage::HeadersOnly,
756            stable_content: true,
757        };
758
759        let original_msg = make_message();
760
761        let log = make_expiration();
762
763        let report_msg = Report::generate(&params, Some(&original_msg), &log)
764            .unwrap()
765            .unwrap();
766        let report_eml = report_msg.to_message_string();
767        k9::snapshot!(
768            &report_eml,
769            r#"
770Content-Type: multipart/report;\r
771\tboundary="report-boundary";\r
772\treport-type="delivery-status"\r
773Subject: Returned mail\r
774Mime-Version: 1.0\r
775Message-ID: <UUID@mta1.example.com>\r
776To: sender@sender.example.com\r
777From: Mail Delivery Subsystem <mailer-daemon@mta1.example.com>\r
778\r
779--report-boundary\r
780Content-Type: text/plain;\r
781\tcharset="us-ascii"\r
782Content-Transfer-Encoding: quoted-printable\r
783\r
784The message was received at Tue, 1 Jul 2003 08:52:37 +0000\r
785from sender@sender.example.com and addressed to recip@target.example.com.\r
786Status: 551 5.4.7 Next delivery time would be at SOME TIME which exceeds th=\r
787e expiry time EXPIRES configured via set_scheduling\r
788The message will be deleted from the queue.\r
789No further attempts will be made to deliver it.\r
790--report-boundary\r
791Content-Type: message/delivery-status;\r
792\tcharset="us-ascii"\r
793Content-Transfer-Encoding: quoted-printable\r
794\r
795Reporting-MTA: dns; mta1.example.com\r
796Arrival-Date: Tue, 1 Jul 2003 08:52:37 +0000\r
797\r
798Final-Recipient: rfc822;recip@target.example.com\r
799Action: failed\r
800Status: 5.4.7 Next delivery time would be at SOME TIME which exceeds the ex=\r
801piry time EXPIRES configured via set_scheduling\r
802Diagnostic-Code: smtp; 551 5.4.7 Next delivery time would be at SOME TIME w=\r
803hich exceeds the expiry time EXPIRES configured via set_scheduling\r
804Last-Attempt-Date: Tue, 1 Jul 2003 10:52:37 +0000\r
805\r
806--report-boundary\r
807Content-Type: text/rfc822-headers\r
808\r
809Content-Type: text/plain;\r
810\tcharset="us-ascii"\r
811Subject: Hello!\r
812--report-boundary--\r
813
814"#
815        );
816
817        let round_trip = Report::parse(report_eml.as_bytes()).unwrap().unwrap();
818        k9::snapshot!(
819            &round_trip,
820            r#"
821Report {
822    per_message: PerMessageReportEntry {
823        original_envelope_id: None,
824        reporting_mta: RemoteMta {
825            mta_type: "dns",
826            name: "mta1.example.com",
827        },
828        dsn_gateway: None,
829        received_from_mta: None,
830        arrival_date: Some(
831            2003-07-01T08:52:37Z,
832        ),
833        extensions: {},
834    },
835    per_recipient: [
836        PerRecipientReportEntry {
837            final_recipient: Recipient {
838                recipient_type: "rfc822",
839                recipient: "recip@target.example.com",
840            },
841            action: Failed,
842            status: ReportStatus {
843                class: 5,
844                subject: 4,
845                detail: 7,
846                comment: Some(
847                    "Next delivery time would be at SOME TIME which exceeds the expiry time EXPIRES configured via set_scheduling",
848                ),
849            },
850            original_recipient: None,
851            remote_mta: None,
852            diagnostic_code: Some(
853                DiagnosticCode {
854                    diagnostic_type: "smtp",
855                    diagnostic: "551 5.4.7 Next delivery time would be at SOME TIME which exceeds the expiry time EXPIRES configured via set_scheduling",
856                },
857            ),
858            last_attempt_date: Some(
859                2003-07-01T10:52:37Z,
860            ),
861            final_log_id: None,
862            will_retry_until: None,
863            extensions: {},
864        },
865    ],
866    original_message: Some(
867        "Content-Type: text/plain;
868\tcharset="us-ascii"
869Subject: Hello!
870",
871    ),
872}
873"#
874        );
875    }
876
877    #[test]
878    fn generate_bounce_with_headers() {
879        let params = ReportGenerationParams {
880            reporting_mta: RemoteMta {
881                mta_type: "dns".to_string(),
882                name: "mta1.example.com".to_string(),
883            },
884            enable_bounce: true,
885            enable_expiration: true,
886            include_original_message: IncludeOriginalMessage::HeadersOnly,
887            stable_content: true,
888        };
889
890        let original_msg = make_message();
891
892        let log = make_bounce();
893
894        let report_msg = Report::generate(&params, Some(&original_msg), &log)
895            .unwrap()
896            .unwrap();
897        let report_eml = report_msg.to_message_string();
898        k9::snapshot!(
899            &report_eml,
900            r#"
901Content-Type: multipart/report;\r
902\tboundary="report-boundary";\r
903\treport-type="delivery-status"\r
904Subject: Returned mail\r
905Mime-Version: 1.0\r
906Message-ID: <UUID@mta1.example.com>\r
907To: sender@sender.example.com\r
908From: Mail Delivery Subsystem <mailer-daemon@mta1.example.com>\r
909\r
910--report-boundary\r
911Content-Type: text/plain;\r
912\tcharset="us-ascii"\r
913\r
914The message was received at Tue, 1 Jul 2003 08:52:37 +0000\r
915from sender@sender.example.com and addressed to recip@target.example.com.\r
916While communicating with target.example.com (42.42.42.42):\r
917Response: 550 5.7.1 no thanks\r
918\r
919The message will be deleted from the queue.\r
920No further attempts will be made to deliver it.\r
921--report-boundary\r
922Content-Type: message/delivery-status;\r
923\tcharset="us-ascii"\r
924\r
925Reporting-MTA: dns; mta1.example.com\r
926Arrival-Date: Tue, 1 Jul 2003 08:52:37 +0000\r
927\r
928Final-Recipient: rfc822;recip@target.example.com\r
929Action: failed\r
930Status: 5.7.1 no thanks\r
931Remote-MTA: dns; target.example.com\r
932Diagnostic-Code: smtp; 550 5.7.1 no thanks\r
933Last-Attempt-Date: Tue, 1 Jul 2003 10:52:37 +0000\r
934\r
935--report-boundary\r
936Content-Type: text/rfc822-headers\r
937\r
938Content-Type: text/plain;\r
939\tcharset="us-ascii"\r
940Subject: Hello!\r
941--report-boundary--\r
942
943"#
944        );
945
946        let round_trip = Report::parse(report_eml.as_bytes()).unwrap().unwrap();
947        k9::snapshot!(
948            &round_trip,
949            r#"
950Report {
951    per_message: PerMessageReportEntry {
952        original_envelope_id: None,
953        reporting_mta: RemoteMta {
954            mta_type: "dns",
955            name: "mta1.example.com",
956        },
957        dsn_gateway: None,
958        received_from_mta: None,
959        arrival_date: Some(
960            2003-07-01T08:52:37Z,
961        ),
962        extensions: {},
963    },
964    per_recipient: [
965        PerRecipientReportEntry {
966            final_recipient: Recipient {
967                recipient_type: "rfc822",
968                recipient: "recip@target.example.com",
969            },
970            action: Failed,
971            status: ReportStatus {
972                class: 5,
973                subject: 7,
974                detail: 1,
975                comment: Some(
976                    "no thanks",
977                ),
978            },
979            original_recipient: None,
980            remote_mta: Some(
981                RemoteMta {
982                    mta_type: "dns",
983                    name: "target.example.com",
984                },
985            ),
986            diagnostic_code: Some(
987                DiagnosticCode {
988                    diagnostic_type: "smtp",
989                    diagnostic: "550 5.7.1 no thanks",
990                },
991            ),
992            last_attempt_date: Some(
993                2003-07-01T10:52:37Z,
994            ),
995            final_log_id: None,
996            will_retry_until: None,
997            extensions: {},
998        },
999    ],
1000    original_message: Some(
1001        "Content-Type: text/plain;
1002\tcharset="us-ascii"
1003Subject: Hello!
1004",
1005    ),
1006}
1007"#
1008        );
1009    }
1010    #[test]
1011    fn generate_bounce_with_message() {
1012        let params = ReportGenerationParams {
1013            reporting_mta: RemoteMta {
1014                mta_type: "dns".to_string(),
1015                name: "mta1.example.com".to_string(),
1016            },
1017            enable_bounce: true,
1018            enable_expiration: true,
1019            include_original_message: IncludeOriginalMessage::FullContent,
1020            stable_content: true,
1021        };
1022
1023        let original_msg = make_message();
1024
1025        let log = make_bounce();
1026
1027        let report_msg = Report::generate(&params, Some(&original_msg), &log)
1028            .unwrap()
1029            .unwrap();
1030        let report_eml = report_msg.to_message_string();
1031        k9::snapshot!(
1032            &report_eml,
1033            r#"
1034Content-Type: multipart/report;\r
1035\tboundary="report-boundary";\r
1036\treport-type="delivery-status"\r
1037Subject: Returned mail\r
1038Mime-Version: 1.0\r
1039Message-ID: <UUID@mta1.example.com>\r
1040To: sender@sender.example.com\r
1041From: Mail Delivery Subsystem <mailer-daemon@mta1.example.com>\r
1042\r
1043--report-boundary\r
1044Content-Type: text/plain;\r
1045\tcharset="us-ascii"\r
1046\r
1047The message was received at Tue, 1 Jul 2003 08:52:37 +0000\r
1048from sender@sender.example.com and addressed to recip@target.example.com.\r
1049While communicating with target.example.com (42.42.42.42):\r
1050Response: 550 5.7.1 no thanks\r
1051\r
1052The message will be deleted from the queue.\r
1053No further attempts will be made to deliver it.\r
1054--report-boundary\r
1055Content-Type: message/delivery-status;\r
1056\tcharset="us-ascii"\r
1057\r
1058Reporting-MTA: dns; mta1.example.com\r
1059Arrival-Date: Tue, 1 Jul 2003 08:52:37 +0000\r
1060\r
1061Final-Recipient: rfc822;recip@target.example.com\r
1062Action: failed\r
1063Status: 5.7.1 no thanks\r
1064Remote-MTA: dns; target.example.com\r
1065Diagnostic-Code: smtp; 550 5.7.1 no thanks\r
1066Last-Attempt-Date: Tue, 1 Jul 2003 10:52:37 +0000\r
1067\r
1068--report-boundary\r
1069Content-Type: message/rfc822\r
1070\r
1071Content-Type: text/plain;\r
1072\tcharset="us-ascii"\r
1073Subject: Hello!\r
1074\r
1075hello there\r
1076--report-boundary--\r
1077
1078"#
1079        );
1080
1081        let round_trip = Report::parse(report_eml.as_bytes()).unwrap().unwrap();
1082        k9::snapshot!(
1083            &round_trip,
1084            r#"
1085Report {
1086    per_message: PerMessageReportEntry {
1087        original_envelope_id: None,
1088        reporting_mta: RemoteMta {
1089            mta_type: "dns",
1090            name: "mta1.example.com",
1091        },
1092        dsn_gateway: None,
1093        received_from_mta: None,
1094        arrival_date: Some(
1095            2003-07-01T08:52:37Z,
1096        ),
1097        extensions: {},
1098    },
1099    per_recipient: [
1100        PerRecipientReportEntry {
1101            final_recipient: Recipient {
1102                recipient_type: "rfc822",
1103                recipient: "recip@target.example.com",
1104            },
1105            action: Failed,
1106            status: ReportStatus {
1107                class: 5,
1108                subject: 7,
1109                detail: 1,
1110                comment: Some(
1111                    "no thanks",
1112                ),
1113            },
1114            original_recipient: None,
1115            remote_mta: Some(
1116                RemoteMta {
1117                    mta_type: "dns",
1118                    name: "target.example.com",
1119                },
1120            ),
1121            diagnostic_code: Some(
1122                DiagnosticCode {
1123                    diagnostic_type: "smtp",
1124                    diagnostic: "550 5.7.1 no thanks",
1125                },
1126            ),
1127            last_attempt_date: Some(
1128                2003-07-01T10:52:37Z,
1129            ),
1130            final_log_id: None,
1131            will_retry_until: None,
1132            extensions: {},
1133        },
1134    ],
1135    original_message: Some(
1136        "Content-Type: text/plain;
1137\tcharset="us-ascii"
1138Subject: Hello!
1139
1140hello there
1141",
1142    ),
1143}
1144"#
1145        );
1146    }
1147
1148    #[test]
1149    fn generate_bounce_no_message() {
1150        let params = ReportGenerationParams {
1151            reporting_mta: RemoteMta {
1152                mta_type: "dns".to_string(),
1153                name: "mta1.example.com".to_string(),
1154            },
1155            enable_bounce: true,
1156            enable_expiration: true,
1157            include_original_message: IncludeOriginalMessage::No,
1158            stable_content: true,
1159        };
1160
1161        let original_msg = make_message();
1162
1163        let log = make_bounce();
1164
1165        let report_msg = Report::generate(&params, Some(&original_msg), &log)
1166            .unwrap()
1167            .unwrap();
1168        let report_eml = report_msg.to_message_string();
1169        k9::snapshot!(
1170            &report_eml,
1171            r#"
1172Content-Type: multipart/report;\r
1173\tboundary="report-boundary";\r
1174\treport-type="delivery-status"\r
1175Subject: Returned mail\r
1176Mime-Version: 1.0\r
1177Message-ID: <UUID@mta1.example.com>\r
1178To: sender@sender.example.com\r
1179From: Mail Delivery Subsystem <mailer-daemon@mta1.example.com>\r
1180\r
1181--report-boundary\r
1182Content-Type: text/plain;\r
1183\tcharset="us-ascii"\r
1184\r
1185The message was received at Tue, 1 Jul 2003 08:52:37 +0000\r
1186from sender@sender.example.com and addressed to recip@target.example.com.\r
1187While communicating with target.example.com (42.42.42.42):\r
1188Response: 550 5.7.1 no thanks\r
1189\r
1190The message will be deleted from the queue.\r
1191No further attempts will be made to deliver it.\r
1192--report-boundary\r
1193Content-Type: message/delivery-status;\r
1194\tcharset="us-ascii"\r
1195\r
1196Reporting-MTA: dns; mta1.example.com\r
1197Arrival-Date: Tue, 1 Jul 2003 08:52:37 +0000\r
1198\r
1199Final-Recipient: rfc822;recip@target.example.com\r
1200Action: failed\r
1201Status: 5.7.1 no thanks\r
1202Remote-MTA: dns; target.example.com\r
1203Diagnostic-Code: smtp; 550 5.7.1 no thanks\r
1204Last-Attempt-Date: Tue, 1 Jul 2003 10:52:37 +0000\r
1205\r
1206--report-boundary--\r
1207
1208"#
1209        );
1210
1211        let round_trip = Report::parse(report_eml.as_bytes()).unwrap().unwrap();
1212        k9::snapshot!(
1213            &round_trip,
1214            r#"
1215Report {
1216    per_message: PerMessageReportEntry {
1217        original_envelope_id: None,
1218        reporting_mta: RemoteMta {
1219            mta_type: "dns",
1220            name: "mta1.example.com",
1221        },
1222        dsn_gateway: None,
1223        received_from_mta: None,
1224        arrival_date: Some(
1225            2003-07-01T08:52:37Z,
1226        ),
1227        extensions: {},
1228    },
1229    per_recipient: [
1230        PerRecipientReportEntry {
1231            final_recipient: Recipient {
1232                recipient_type: "rfc822",
1233                recipient: "recip@target.example.com",
1234            },
1235            action: Failed,
1236            status: ReportStatus {
1237                class: 5,
1238                subject: 7,
1239                detail: 1,
1240                comment: Some(
1241                    "no thanks",
1242                ),
1243            },
1244            original_recipient: None,
1245            remote_mta: Some(
1246                RemoteMta {
1247                    mta_type: "dns",
1248                    name: "target.example.com",
1249                },
1250            ),
1251            diagnostic_code: Some(
1252                DiagnosticCode {
1253                    diagnostic_type: "smtp",
1254                    diagnostic: "550 5.7.1 no thanks",
1255                },
1256            ),
1257            last_attempt_date: Some(
1258                2003-07-01T10:52:37Z,
1259            ),
1260            final_log_id: None,
1261            will_retry_until: None,
1262            extensions: {},
1263        },
1264    ],
1265    original_message: None,
1266}
1267"#
1268        );
1269    }
1270
1271    #[test]
1272    fn rfc3464_1() {
1273        let result = Report::parse(include_bytes!("../data/rfc3464/1.eml")).unwrap();
1274        k9::snapshot!(
1275            &result,
1276            r#"
1277Some(
1278    Report {
1279        per_message: PerMessageReportEntry {
1280            original_envelope_id: None,
1281            reporting_mta: RemoteMta {
1282                mta_type: "dns",
1283                name: "cs.utk.edu",
1284            },
1285            dsn_gateway: None,
1286            received_from_mta: None,
1287            arrival_date: None,
1288            extensions: {},
1289        },
1290        per_recipient: [
1291            PerRecipientReportEntry {
1292                final_recipient: Recipient {
1293                    recipient_type: "rfc822",
1294                    recipient: "louisl@larry.slip.umd.edu",
1295                },
1296                action: Failed,
1297                status: ReportStatus {
1298                    class: 4,
1299                    subject: 0,
1300                    detail: 0,
1301                    comment: None,
1302                },
1303                original_recipient: Some(
1304                    Recipient {
1305                        recipient_type: "rfc822",
1306                        recipient: "louisl@larry.slip.umd.edu",
1307                    },
1308                ),
1309                remote_mta: None,
1310                diagnostic_code: Some(
1311                    DiagnosticCode {
1312                        diagnostic_type: "smtp",
1313                        diagnostic: "426 connection timed out",
1314                    },
1315                ),
1316                last_attempt_date: Some(
1317                    1994-07-07T21:15:49Z,
1318                ),
1319                final_log_id: None,
1320                will_retry_until: None,
1321                extensions: {},
1322            },
1323        ],
1324        original_message: Some(
1325            "[original message goes here]
1326
1327",
1328        ),
1329    },
1330)
1331"#
1332        );
1333
1334        let report = result.unwrap();
1335
1336        assert_eq!(
1337            report.per_message.to_string(),
1338            "Reporting-MTA: dns; cs.utk.edu\r\n"
1339        );
1340        assert_eq!(
1341            report.per_recipient[0].to_string(),
1342            "Original-Recipient: rfc822;louisl@larry.slip.umd.edu\r\n\
1343            Final-Recipient: rfc822;louisl@larry.slip.umd.edu\r\n\
1344            Action: failed\r\n\
1345            Status: 4.0.0\r\n\
1346            Diagnostic-Code: smtp; 426 connection timed out\r\n\
1347            Last-Attempt-Date: Thu, 7 Jul 1994 21:15:49 +0000\r\n"
1348        );
1349    }
1350
1351    #[test]
1352    fn rfc3464_2() {
1353        let result = Report::parse(include_bytes!("../data/rfc3464/2.eml")).unwrap();
1354        k9::snapshot!(
1355            result,
1356            r#"
1357Some(
1358    Report {
1359        per_message: PerMessageReportEntry {
1360            original_envelope_id: None,
1361            reporting_mta: RemoteMta {
1362                mta_type: "dns",
1363                name: "cs.utk.edu",
1364            },
1365            dsn_gateway: None,
1366            received_from_mta: None,
1367            arrival_date: None,
1368            extensions: {},
1369        },
1370        per_recipient: [
1371            PerRecipientReportEntry {
1372                final_recipient: Recipient {
1373                    recipient_type: "rfc822",
1374                    recipient: "arathib@vnet.ibm.com",
1375                },
1376                action: Failed,
1377                status: ReportStatus {
1378                    class: 5,
1379                    subject: 0,
1380                    detail: 0,
1381                    comment: Some(
1382                        "(permanent failure)",
1383                    ),
1384                },
1385                original_recipient: Some(
1386                    Recipient {
1387                        recipient_type: "rfc822",
1388                        recipient: "arathib@vnet.ibm.com",
1389                    },
1390                ),
1391                remote_mta: Some(
1392                    RemoteMta {
1393                        mta_type: "dns",
1394                        name: "vnet.ibm.com",
1395                    },
1396                ),
1397                diagnostic_code: Some(
1398                    DiagnosticCode {
1399                        diagnostic_type: "smtp",
1400                        diagnostic: "550 'arathib@vnet.IBM.COM' is not a registered gateway user",
1401                    },
1402                ),
1403                last_attempt_date: None,
1404                final_log_id: None,
1405                will_retry_until: None,
1406                extensions: {},
1407            },
1408            PerRecipientReportEntry {
1409                final_recipient: Recipient {
1410                    recipient_type: "rfc822",
1411                    recipient: "johnh@hpnjld.njd.hp.com",
1412                },
1413                action: Delayed,
1414                status: ReportStatus {
1415                    class: 4,
1416                    subject: 0,
1417                    detail: 0,
1418                    comment: Some(
1419                        "(hpnjld.njd.jp.com: host name lookup failure)",
1420                    ),
1421                },
1422                original_recipient: Some(
1423                    Recipient {
1424                        recipient_type: "rfc822",
1425                        recipient: "johnh@hpnjld.njd.hp.com",
1426                    },
1427                ),
1428                remote_mta: None,
1429                diagnostic_code: None,
1430                last_attempt_date: None,
1431                final_log_id: None,
1432                will_retry_until: None,
1433                extensions: {},
1434            },
1435            PerRecipientReportEntry {
1436                final_recipient: Recipient {
1437                    recipient_type: "rfc822",
1438                    recipient: "wsnell@sdcc13.ucsd.edu",
1439                },
1440                action: Failed,
1441                status: ReportStatus {
1442                    class: 5,
1443                    subject: 0,
1444                    detail: 0,
1445                    comment: None,
1446                },
1447                original_recipient: Some(
1448                    Recipient {
1449                        recipient_type: "rfc822",
1450                        recipient: "wsnell@sdcc13.ucsd.edu",
1451                    },
1452                ),
1453                remote_mta: Some(
1454                    RemoteMta {
1455                        mta_type: "dns",
1456                        name: "sdcc13.ucsd.edu",
1457                    },
1458                ),
1459                diagnostic_code: Some(
1460                    DiagnosticCode {
1461                        diagnostic_type: "smtp",
1462                        diagnostic: "550 user unknown",
1463                    },
1464                ),
1465                last_attempt_date: None,
1466                final_log_id: None,
1467                will_retry_until: None,
1468                extensions: {},
1469            },
1470        ],
1471        original_message: Some(
1472            "[original message goes here]
1473
1474",
1475        ),
1476    },
1477)
1478"#
1479        );
1480    }
1481
1482    #[test]
1483    fn rfc3464_3() {
1484        let result = Report::parse(include_bytes!("../data/rfc3464/3.eml")).unwrap();
1485        k9::snapshot!(
1486            result,
1487            r#"
1488Some(
1489    Report {
1490        per_message: PerMessageReportEntry {
1491            original_envelope_id: None,
1492            reporting_mta: RemoteMta {
1493                mta_type: "mailbus",
1494                name: "SYS30",
1495            },
1496            dsn_gateway: None,
1497            received_from_mta: None,
1498            arrival_date: None,
1499            extensions: {},
1500        },
1501        per_recipient: [
1502            PerRecipientReportEntry {
1503                final_recipient: Recipient {
1504                    recipient_type: "unknown",
1505                    recipient: "nair_s",
1506                },
1507                action: Failed,
1508                status: ReportStatus {
1509                    class: 5,
1510                    subject: 0,
1511                    detail: 0,
1512                    comment: Some(
1513                        "(unknown permanent failure)",
1514                    ),
1515                },
1516                original_recipient: None,
1517                remote_mta: None,
1518                diagnostic_code: None,
1519                last_attempt_date: None,
1520                final_log_id: None,
1521                will_retry_until: None,
1522                extensions: {},
1523            },
1524        ],
1525        original_message: None,
1526    },
1527)
1528"#
1529        );
1530    }
1531
1532    #[test]
1533    fn rfc3464_4() {
1534        let result = Report::parse(include_bytes!("../data/rfc3464/4.eml")).unwrap();
1535        k9::snapshot!(
1536            result,
1537            r#"
1538Some(
1539    Report {
1540        per_message: PerMessageReportEntry {
1541            original_envelope_id: None,
1542            reporting_mta: RemoteMta {
1543                mta_type: "dns",
1544                name: "sun2.nsfnet-relay.ac.uk",
1545            },
1546            dsn_gateway: None,
1547            received_from_mta: None,
1548            arrival_date: None,
1549            extensions: {},
1550        },
1551        per_recipient: [
1552            PerRecipientReportEntry {
1553                final_recipient: Recipient {
1554                    recipient_type: "rfc822",
1555                    recipient: "thomas@de-montfort.ac.uk",
1556                },
1557                action: Delayed,
1558                status: ReportStatus {
1559                    class: 4,
1560                    subject: 0,
1561                    detail: 0,
1562                    comment: Some(
1563                        "(unknown temporary failure)",
1564                    ),
1565                },
1566                original_recipient: None,
1567                remote_mta: None,
1568                diagnostic_code: None,
1569                last_attempt_date: None,
1570                final_log_id: None,
1571                will_retry_until: None,
1572                extensions: {},
1573            },
1574        ],
1575        original_message: None,
1576    },
1577)
1578"#
1579        );
1580    }
1581
1582    #[test]
1583    fn rfc3464_5() {
1584        let result = Report::parse(include_bytes!("../data/rfc3464/5.eml")).unwrap();
1585        k9::snapshot!(
1586            result,
1587            r#"
1588Some(
1589    Report {
1590        per_message: PerMessageReportEntry {
1591            original_envelope_id: None,
1592            reporting_mta: RemoteMta {
1593                mta_type: "dns",
1594                name: "mx-by.bbox.fr",
1595            },
1596            dsn_gateway: None,
1597            received_from_mta: None,
1598            arrival_date: Some(
1599                2025-01-29T16:36:51Z,
1600            ),
1601            extensions: {
1602                "x-postfix-queue-id": [
1603                    "897DAC0",
1604                ],
1605                "x-postfix-sender": [
1606                    "rfc822; user@example.com",
1607                ],
1608            },
1609        },
1610        per_recipient: [
1611            PerRecipientReportEntry {
1612                final_recipient: Recipient {
1613                    recipient_type: "rfc822",
1614                    recipient: "recipient@domain.com",
1615                },
1616                action: Failed,
1617                status: ReportStatus {
1618                    class: 5,
1619                    subject: 0,
1620                    detail: 0,
1621                    comment: None,
1622                },
1623                original_recipient: Some(
1624                    Recipient {
1625                        recipient_type: "rfc822",
1626                        recipient: "recipient@domain.com",
1627                    },
1628                ),
1629                remote_mta: Some(
1630                    RemoteMta {
1631                        mta_type: "dns",
1632                        name: "lmtp.cs.dolmen.bouyguestelecom.fr",
1633                    },
1634                ),
1635                diagnostic_code: Some(
1636                    DiagnosticCode {
1637                        diagnostic_type: "smtp",
1638                        diagnostic: "552 <recipient@domain.com> rejected: over quota",
1639                    },
1640                ),
1641                last_attempt_date: None,
1642                final_log_id: None,
1643                will_retry_until: None,
1644                extensions: {},
1645            },
1646        ],
1647        original_message: Some(
1648            "[original message goes here]
1649
1650",
1651        ),
1652    },
1653)
1654"#
1655        );
1656    }
1657}