1use crate::report::{AuthFailureType, DeliveryResult, Feedback, FeedbackType, IdentityAlignment};
8use mail_builder::{
9 MessageBuilder,
10 headers::{HeaderType, address::Address, content_type::ContentType},
11 mime::{BodyPart, MimePart, make_boundary},
12};
13use mail_parser::DateTime;
14use std::{fmt::Write, io, time::SystemTime};
15
16impl<'x> Feedback<'x> {
17 pub fn write_rfc5322(
18 &self,
19 from: impl Into<Address<'x>>,
20 to: &'x str,
21 subject: &'x str,
22 writer: impl io::Write,
23 ) -> io::Result<()> {
24 let arf = self.to_arf();
26
27 let mut text_body = String::with_capacity(128);
29 if self.feedback_type == FeedbackType::AuthFailure {
30 write!(
31 &mut text_body,
32 "This is an authentication failure report for an email message received\r\n"
33 )
34 } else {
35 write!(
36 &mut text_body,
37 "This is an email abuse report for an email message received\r\n"
38 )
39 }
40 .ok();
41 if let Some(ip) = &self.source_ip {
42 write!(&mut text_body, "from IP address {ip} ").ok();
43 }
44 let dt = DateTime::from_timestamp(if let Some(ad) = &self.arrival_date {
45 *ad
46 } else {
47 SystemTime::now()
48 .duration_since(SystemTime::UNIX_EPOCH)
49 .map(|d| d.as_secs())
50 .unwrap_or(0) as i64
51 });
52 write!(&mut text_body, "on {}.\r\n", dt.to_rfc822()).ok();
53
54 let mut parts = vec![
56 MimePart::new(
57 ContentType::new("text/plain"),
58 BodyPart::Text(text_body.into()),
59 ),
60 MimePart::new(
61 ContentType::new("message/feedback-report"),
62 BodyPart::Text(arf.into()),
63 ),
64 ];
65 if let Some(message) = self.message.as_deref() {
66 parts.push(MimePart::new(
67 ContentType::new("message/rfc822"),
68 BodyPart::Text(message.into()),
69 ));
70 } else if let Some(headers) = self.headers.as_deref() {
71 parts.push(MimePart::new(
72 ContentType::new("text/rfc822-headers"),
73 BodyPart::Text(headers.into()),
74 ));
75 }
76
77 MessageBuilder::new()
78 .from(from)
79 .header("To", HeaderType::Text(to.into()))
80 .header("Auto-Submitted", HeaderType::Text("auto-generated".into()))
81 .message_id(format!(
82 "{}@{}",
83 make_boundary("."),
84 self.reporting_mta().unwrap_or("localhost")
85 ))
86 .subject(subject)
87 .body(MimePart::new(
88 ContentType::new("multipart/report").attribute("report-type", "feedback-report"),
89 BodyPart::Multipart(parts),
90 ))
91 .write_to(writer)
92 }
93
94 pub fn to_rfc5322(
95 &self,
96 from: impl Into<Address<'x>>,
97 to: &'x str,
98 subject: &'x str,
99 ) -> io::Result<String> {
100 let mut buf = Vec::new();
101 self.write_rfc5322(from, to, subject, &mut buf)?;
102 String::from_utf8(buf).map_err(io::Error::other)
103 }
104
105 pub fn to_arf(&self) -> String {
106 let mut arf = String::with_capacity(128);
107
108 write!(&mut arf, "Version: {}\r\n", self.version).ok();
109 write!(
110 &mut arf,
111 "Feedback-Type: {}\r\n",
112 match self.feedback_type {
113 FeedbackType::Abuse => "abuse",
114 FeedbackType::AuthFailure => "auth-failure",
115 FeedbackType::Fraud => "fraud",
116 FeedbackType::NotSpam => "not-spam",
117 FeedbackType::Other => "other",
118 FeedbackType::Virus => "virus",
119 }
120 )
121 .ok();
122 if let Some(ad) = &self.arrival_date {
123 let ad = DateTime::from_timestamp(*ad);
124 write!(&mut arf, "Arrival-Date: {}\r\n", ad.to_rfc822()).ok();
125 }
126
127 if self.feedback_type == FeedbackType::AuthFailure {
128 if self.auth_failure != AuthFailureType::Unspecified {
129 write!(
130 &mut arf,
131 "Auth-Failure: {}\r\n",
132 match self.auth_failure {
133 AuthFailureType::Adsp => "adsp",
134 AuthFailureType::BodyHash => "bodyhash",
135 AuthFailureType::Revoked => "revoked",
136 AuthFailureType::Signature => "signature",
137 AuthFailureType::Spf => "spf",
138 AuthFailureType::Dmarc => "dmarc",
139 AuthFailureType::Unspecified => unreachable!(),
140 }
141 )
142 .ok();
143 }
144
145 if self.delivery_result != DeliveryResult::Unspecified {
146 write!(
147 &mut arf,
148 "Delivery-Result: {}\r\n",
149 match self.delivery_result {
150 DeliveryResult::Delivered => "delivered",
151 DeliveryResult::Spam => "spam",
152 DeliveryResult::Policy => "policy",
153 DeliveryResult::Reject => "reject",
154 DeliveryResult::Other => "other",
155 DeliveryResult::Unspecified => unreachable!(),
156 }
157 )
158 .ok();
159 }
160 if let Some(value) = &self.dkim_adsp_dns {
161 write!(&mut arf, "DKIM-ADSP-DNS: {value}\r\n").ok();
162 }
163 if let Some(value) = &self.dkim_canonicalized_body {
164 write!(&mut arf, "DKIM-Canonicalized-Body: {value}\r\n").ok();
165 }
166 if let Some(value) = &self.dkim_canonicalized_header {
167 write!(&mut arf, "DKIM-Canonicalized-Header: {value}\r\n").ok();
168 }
169 if let Some(value) = &self.dkim_domain {
170 write!(&mut arf, "DKIM-Domain: {value}\r\n").ok();
171 }
172 if let Some(value) = &self.dkim_identity {
173 write!(&mut arf, "DKIM-Identity: {value}\r\n").ok();
174 }
175 if let Some(value) = &self.dkim_selector {
176 write!(&mut arf, "DKIM-Selector: {value}\r\n").ok();
177 }
178 if let Some(value) = &self.dkim_selector_dns {
179 write!(&mut arf, "DKIM-Selector-DNS: {value}\r\n").ok();
180 }
181 if let Some(value) = &self.spf_dns {
182 write!(&mut arf, "SPF-DNS: {value}\r\n").ok();
183 }
184 if self.identity_alignment != IdentityAlignment::Unspecified {
185 write!(
186 &mut arf,
187 "Identity-Alignment: {}\r\n",
188 match self.identity_alignment {
189 IdentityAlignment::None => "none",
190 IdentityAlignment::Spf => "spf",
191 IdentityAlignment::Dkim => "dkim",
192 IdentityAlignment::DkimSpf => "dkim, spf",
193 IdentityAlignment::Unspecified => unreachable!(),
194 }
195 )
196 .ok();
197 }
198 }
199
200 for value in &self.authentication_results {
201 write!(&mut arf, "Authentication-Results: {value}\r\n").ok();
202 }
203 if self.incidents > 1 {
204 write!(&mut arf, "Incidents: {}\r\n", self.incidents).ok();
205 }
206 if let Some(value) = &self.original_envelope_id {
207 write!(&mut arf, "Original-Envelope-Id: {value}\r\n").ok();
208 }
209 if let Some(value) = &self.original_mail_from {
210 write!(&mut arf, "Original-Mail-From: {value}\r\n").ok();
211 }
212 if let Some(value) = &self.original_rcpt_to {
213 write!(&mut arf, "Original-Rcpt-To: {value}\r\n").ok();
214 }
215 for value in &self.reported_domain {
216 write!(&mut arf, "Reported-Domain: {value}\r\n").ok();
217 }
218 for value in &self.reported_uri {
219 write!(&mut arf, "Reported-URI: {value}\r\n").ok();
220 }
221 if let Some(value) = &self.reporting_mta {
222 write!(&mut arf, "Reporting-MTA: dns;{value}\r\n").ok();
223 }
224 if let Some(value) = &self.source_ip {
225 write!(&mut arf, "Source-IP: {value}\r\n").ok();
226 }
227 if self.source_port != 0 {
228 write!(&mut arf, "Source-Port: {}\r\n", self.source_port).ok();
229 }
230 if let Some(value) = &self.user_agent {
231 write!(&mut arf, "User-Agent: {value}\r\n").ok();
232 }
233
234 arf
235 }
236}
237
238#[cfg(test)]
239mod test {
240 use crate::report::{AuthFailureType, Feedback, FeedbackType, IdentityAlignment};
241
242 #[test]
243 fn arf_report_generate() {
244 let feedback = Feedback::new(FeedbackType::AuthFailure)
245 .with_arrival_date(5934759438)
246 .with_authentication_results("dkim=pass")
247 .with_incidents(10)
248 .with_original_envelope_id("821-abc-123")
249 .with_original_mail_from("hello@world.org")
250 .with_original_rcpt_to("ciao@mundo.org")
251 .with_reported_domain("example.org")
252 .with_reported_domain("example2.org")
253 .with_reported_uri("uri:domain.org")
254 .with_reported_uri("uri:domain2.org")
255 .with_reporting_mta("Manchegator 2.0")
256 .with_source_ip("192.168.1.1".parse().unwrap())
257 .with_user_agent("DMARC-Meister")
258 .with_version(2)
259 .with_source_port(1234)
260 .with_auth_failure(AuthFailureType::Dmarc)
261 .with_dkim_adsp_dns("v=dkim1")
262 .with_dkim_canonicalized_body("base64 goes here")
263 .with_dkim_canonicalized_header("more base64")
264 .with_dkim_domain("dkim-domain.org")
265 .with_dkim_identity("my-dkim-identity@domain.org")
266 .with_dkim_selector("the-selector")
267 .with_dkim_selector_dns("v=dkim1;")
268 .with_spf_dns("v=spf1")
269 .with_identity_alignment(IdentityAlignment::DkimSpf)
270 .with_message("From: hello@world.org\r\nTo: ciao@mondo.org\r\n\r\n");
271
272 let message = feedback
273 .to_rfc5322(
274 ("DMARC Reporter", "no-reply@example.org"),
275 "ruf@otherdomain.com",
276 "DMARC Authentication Failure Report",
277 )
278 .unwrap();
279
280 let parsed_feedback = Feedback::parse_rfc5322(message.as_bytes()).unwrap();
281
282 assert_eq!(feedback, parsed_feedback);
283 }
284}