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 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 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 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 pub reporting_mta: RemoteMta,
634
635 #[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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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}