mailrs_outbound_queue/
dsn.rs1use mailrs_mail_builder::{Attachment, MessageBuilder};
11
12pub fn format_dsn(
16 reporting_mta: &str,
17 sender: &str,
18 recipient: &str,
19 error: &str,
20 message_id: Option<&str>,
21) -> String {
22 let now = chrono::Utc::now();
23 let dsn_id = format!(
24 "<{}.{}@{}>",
25 now.timestamp(),
26 std::process::id(),
27 reporting_mta,
28 );
29
30 let machine_body = format!(
34 "Reporting-MTA: dns; {reporting_mta}\r\n\
35 \r\n\
36 Final-Recipient: rfc822; {recipient}\r\n\
37 Action: failed\r\n\
38 Status: 5.0.0\r\n\
39 Diagnostic-Code: smtp; {error}\r\n",
40 );
41
42 let human = format!(
46 "Your message to <{recipient}> could not be delivered.\r\n\
47 \r\n\
48 Error: {error}\r\n",
49 );
50
51 let mut b = MessageBuilder::new()
52 .from(format!("Mail Delivery System <mailer-daemon@{reporting_mta}>"))
53 .to(sender)
54 .subject("Delivery Status Notification (Failure)")
55 .header("Auto-Submitted", "auto-replied")
56 .text_body(human)
57 .attachment(Attachment::new(
58 "delivery-status.txt",
59 "message/delivery-status",
60 machine_body.into_bytes(),
61 ))
62 .report_type("delivery-status")
63 .message_id(dsn_id);
64
65 if let Some(mid) = message_id {
66 b = b.header("References", format!("<{mid}>"));
67 }
68
69 let bytes = b.build();
70 String::from_utf8(bytes).expect("mail-builder output is ASCII-safe by construction")
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn format_basic() {
79 let dsn = format_dsn(
80 "mx.example.com",
81 "sender@example.com",
82 "rcpt@remote.com",
83 "550 User not found",
84 None,
85 );
86 assert!(dsn.contains("From: Mail Delivery System <mailer-daemon@mx.example.com>\r\n"));
87 assert!(dsn.contains("To: sender@example.com\r\n"));
91 let unfold = dsn.replace("\r\n ", " ").replace("\r\n\t", " ");
93 assert!(unfold.contains("Final-Recipient: rfc822; rcpt@remote.com"));
94 assert!(unfold.contains("Diagnostic-Code: smtp; 550 User not found"));
95 assert!(!unfold.contains("References:"));
96 }
97
98 #[test]
99 fn format_with_message_id() {
100 let dsn = format_dsn(
101 "mx.example.com",
102 "sender@example.com",
103 "rcpt@remote.com",
104 "421 try again",
105 Some("abc123@example.com"),
106 );
107 assert!(dsn.contains("References: <abc123@example.com>"));
108 }
109
110 #[test]
111 fn dsn_structure() {
112 let dsn = format_dsn(
113 "mx.example.com",
114 "sender@example.com",
115 "rcpt@remote.com",
116 "550 no such user",
117 None,
118 );
119 let unfold = dsn.replace("\r\n ", " ").replace("\r\n\t", " ");
121 assert!(unfold.contains("multipart/report"));
122 assert!(unfold.contains("report-type=delivery-status"));
123 assert!(unfold.contains("message/delivery-status"));
124 let envelope_opens = dsn.matches("\r\n--").count();
128 assert!(envelope_opens >= 2, "got {envelope_opens} envelope markers");
129 assert!(unfold.contains("Reporting-MTA: dns; mx.example.com"));
130 assert!(unfold.contains("Action: failed"));
131 assert!(unfold.contains("Status: 5.0.0"));
132 }
133
134 #[test]
135 fn special_chars_in_error() {
136 let dsn = format_dsn(
137 "mx.example.com",
138 "sender@example.com",
139 "rcpt@remote.com",
140 "550 5.1.1 <rcpt@remote.com>: Recipient address rejected",
141 None,
142 );
143 let unfold = dsn.replace("\r\n ", " ").replace("\r\n\t", " ");
144 assert!(
145 unfold.contains("550 5.1.1 <rcpt@remote.com>: Recipient address rejected"),
146 "diagnostic-code line missing in: {unfold}",
147 );
148 }
149
150 #[test]
151 fn uses_crlf_line_endings() {
152 let dsn = format_dsn(
153 "mx.example.com",
154 "sender@example.com",
155 "rcpt@remote.com",
156 "550 error",
157 None,
158 );
159 for (i, &b) in dsn.as_bytes().iter().enumerate() {
161 if b == b'\n' {
162 assert!(
163 i > 0 && dsn.as_bytes()[i - 1] == b'\r',
164 "bare \\n at byte {i}"
165 );
166 }
167 }
168 }
169
170 #[test]
171 fn dsn_human_readable_part_mentions_recipient() {
172 let dsn = format_dsn(
173 "mx.example.com",
174 "sender@example.com",
175 "rcpt@remote.com",
176 "550 error",
177 None,
178 );
179 let unfold = dsn.replace("\r\n ", " ").replace("\r\n\t", " ");
181 assert!(unfold.contains("<rcpt@remote.com>"));
182 }
183}