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