1use crate::{
8 common::headers::HeaderIterator,
9 report::{AuthFailureType, DeliveryResult, Error, Feedback, FeedbackType, IdentityAlignment},
10};
11use mail_parser::{HeaderValue, MessageParser, MimeHeaders, PartType, parsers::MessageStream};
12use std::borrow::Cow;
13
14impl<'x> Feedback<'x> {
15 pub fn parse_rfc5322(message: &'x [u8]) -> Result<Self, Error> {
16 let message = MessageParser::new()
17 .parse(message)
18 .ok_or(Error::MailParseError)?;
19 let mut feedback = None;
20 let mut included_message = None;
21 let mut included_headers = None;
22
23 for part in message.parts {
24 let arf = match part.body {
25 PartType::Text(arf) | PartType::Html(arf)
26 if part.is_content_type("message", "feedback-report") =>
27 {
28 match arf {
29 Cow::Borrowed(arf) => Cow::Borrowed(arf.as_bytes()),
30 Cow::Owned(arf) => Cow::Owned(arf.into_bytes()),
31 }
32 }
33 PartType::Binary(arf) | PartType::InlineBinary(arf)
34 if part.is_content_type("message", "feedback-report") =>
35 {
36 arf
37 }
38 PartType::Text(headers) if part.is_content_type("text", "rfc822-headers") => {
39 included_headers = match headers {
40 Cow::Borrowed(arf) => Cow::Borrowed(arf.as_bytes()),
41 Cow::Owned(arf) => Cow::Owned(arf.into_bytes()),
42 }
43 .into();
44 continue;
45 }
46 PartType::Message(message) => {
47 included_message = match message.raw_message {
48 Cow::Borrowed(message) => Cow::Borrowed(
49 message
50 .get(part.offset_body as usize..part.offset_end as usize)
51 .unwrap_or_default(),
52 ),
53 message => message,
54 }
55 .into();
56 continue;
57 }
58 _ => continue,
59 };
60
61 feedback = match arf {
62 Cow::Borrowed(arf) => Feedback::parse_arf(arf),
63 Cow::Owned(arf) => Feedback::parse_arf(&arf).map(|f| f.into_owned()),
64 };
65 }
66
67 if let Some(mut feedback) = feedback {
68 for (feedback, included) in [
69 (&mut feedback.message, included_message),
70 (&mut feedback.headers, included_headers),
71 ] {
72 if let Some(included) = included {
73 *feedback = match included {
74 Cow::Borrowed(bytes) => Some(String::from_utf8_lossy(bytes)),
75 Cow::Owned(bytes) => Some(
76 String::from_utf8(bytes)
77 .unwrap_or_else(|err| {
78 String::from_utf8_lossy(err.as_bytes()).into_owned()
79 })
80 .into(),
81 ),
82 };
83 }
84 }
85
86 Ok(feedback)
87 } else {
88 Err(Error::NoReportsFound)
89 }
90 }
91
92 pub fn parse_arf(arf: &'x [u8]) -> Option<Self> {
93 let mut f = Feedback {
94 incidents: 1,
95 ..Default::default()
96 };
97 let mut has_ft = false;
98
99 let mut fields = HeaderIterator::new(arf);
100 fields.seek_start();
101
102 for (key, value) in fields {
103 let txt_value = std::str::from_utf8(value).unwrap_or_default().trim();
104
105 hashify::fnc_map_ignore_case!(key,
106 b"Arrival-Date" => {
107 if let HeaderValue::DateTime(dt) = MessageStream::new(value).parse_date() {
108 f.arrival_date = dt.to_timestamp().into();
109 }
110 },
111 b"Auth-Failure" => {
112 f.auth_failure = match hashify::tiny_map_ignore_case!(txt_value.as_bytes(),
113 b"adsp" => AuthFailureType::Adsp,
114 b"bodyhash" => AuthFailureType::BodyHash,
115 b"revoked" => AuthFailureType::Revoked,
116 b"signature" => AuthFailureType::Signature,
117 b"spf" => AuthFailureType::Spf,
118 b"dmarc" => AuthFailureType::Dmarc,
119 ) {
120 Some(auth_failure) => auth_failure,
121 None => continue,
122 };
123 },
124 b"Authentication-Results" => {
125 f.authentication_results.push(txt_value.into());
126 },
127 b"DKIM-ADSP-DNS" => {
128 f.dkim_adsp_dns = Some(txt_value.into());
129 },
130 b"DKIM-Canonicalized-Body" => {
131 f.dkim_canonicalized_body = Some(txt_value.into());
132 },
133 b"DKIM-Canonicalized-Header" => {
134 f.dkim_canonicalized_header = Some(txt_value.into());
135 },
136 b"DKIM-Domain" => {
137 f.dkim_domain = Some(txt_value.into());
138 },
139 b"DKIM-Identity" => {
140 f.dkim_identity = Some(txt_value.into());
141 },
142 b"DKIM-Selector" => {
143 f.dkim_selector = Some(txt_value.into());
144 },
145 b"DKIM-Selector-DNS" => {
146 f.dkim_selector_dns = Some(txt_value.into());
147 },
148 b"Delivery-Result" => {
149 f.delivery_result = match hashify::tiny_map_ignore_case!(txt_value.as_bytes(),
150 b"delivered" => DeliveryResult::Delivered,
151 b"spam" => DeliveryResult::Spam,
152 b"policy" => DeliveryResult::Policy,
153 b"reject" => DeliveryResult::Reject,
154 b"other" => DeliveryResult::Other,
155 ) {
156 Some(delivery_result) => delivery_result,
157 None => continue,
158 };
159 },
160 b"Feedback-Type" => {
161 f.feedback_type = match hashify::tiny_map_ignore_case!(txt_value.as_bytes(),
162 b"abuse" => FeedbackType::Abuse,
163 b"auth-failure" => FeedbackType::AuthFailure,
164 b"fraud" => FeedbackType::Fraud,
165 b"not-spam" => FeedbackType::NotSpam,
166 b"other" => FeedbackType::Other,
167 b"virus" => FeedbackType::Virus,
168 ) {
169 Some(feedback_type) => feedback_type,
170 None => continue,
171 };
172 has_ft = true;
173 },
174 b"Identity-Alignment" => {
175 for id in txt_value.split(',') {
176 let id = id.trim();
177 hashify::fnc_map_ignore_case!(id.as_bytes(),
178 b"dkim" => {
179 f.identity_alignment =
180 if f.identity_alignment == IdentityAlignment::Spf {
181 IdentityAlignment::DkimSpf
182 } else {
183 IdentityAlignment::Dkim
184 };
185 },
186 b"spf" => {
187 f.identity_alignment =
188 if f.identity_alignment == IdentityAlignment::Dkim {
189 IdentityAlignment::DkimSpf
190 } else {
191 IdentityAlignment::Spf
192 };
193 },
194 b"none" => {
195 f.identity_alignment = IdentityAlignment::None;
196 break;
197 },
198 _ => ()
199 );
200 }
201 },
202 b"Incidents" => {
203 f.incidents = txt_value.parse().unwrap_or(1);
204 },
205 b"Original-Envelope-Id" => {
206 f.original_envelope_id = Some(txt_value.into());
207 },
208 b"Original-Mail-From" => {
209 f.original_mail_from = Some(txt_value.into());
210 },
211 b"Original-Rcpt-To" => {
212 f.original_rcpt_to = Some(txt_value.into());
213 },
214 b"Reported-Domain" => {
215 f.reported_domain.push(txt_value.into());
216 },
217 b"Reported-URI" => {
218 f.reported_uri.push(txt_value.into());
219 },
220 b"Reporting-MTA" => {
221 f.reporting_mta = Some(if let Some(mta) = txt_value.strip_prefix("dns;") {
222 mta.trim().into()
223 } else {
224 txt_value.into()
225 });
226 },
227 b"Received-Date" => {
228 if let HeaderValue::DateTime(dt) = MessageStream::new(value).parse_date() {
229 f.arrival_date = dt.to_timestamp().into();
230 }
231 },
232 b"SPF-DNS" => {
233 f.spf_dns = Some(txt_value.into());
234 },
235 b"Source-IP" => {
236 f.source_ip = if let Some((ip, _)) = txt_value.split_once(' ') {
237 ip.parse().ok()
238 } else {
239 txt_value.parse().ok()
240 };
241 },
242 b"Source-Port" => {
243 f.source_port = txt_value.parse().unwrap_or(0);
244 },
245 b"User-Agent" => {
246 f.user_agent = Some(txt_value.into());
247 },
248 b"Version" => {
249 f.version = txt_value.parse().unwrap_or(0);
250 },
251 _ => ()
252 );
253 }
254
255 if has_ft { Some(f) } else { None }
256 }
257}
258
259#[cfg(test)]
260mod test {
261 use std::{fs, path::PathBuf};
262
263 use crate::report::Feedback;
264
265 #[test]
266 fn arf_report_parse() {
267 let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
268 test_dir.push("resources");
269 test_dir.push("arf");
270
271 for file_name in fs::read_dir(&test_dir).unwrap() {
272 let mut file_name = file_name.unwrap().path();
273 if !file_name.extension().unwrap().to_str().unwrap().eq("eml") {
274 continue;
275 }
276 println!("Parsing ARF feedback {}", file_name.to_str().unwrap());
277
278 let arf = fs::read(&file_name).unwrap();
279 let mut feedback = Feedback::parse_rfc5322(&arf).unwrap();
280 feedback.message = None;
281
282 file_name.set_extension("json");
283
284 let expected_feedback =
285 serde_json::from_slice::<Feedback>(&fs::read(&file_name).unwrap()).unwrap();
286
287 assert_eq!(expected_feedback, feedback);
288
289 }
295 }
296}