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