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