Commit
+221 -90 +/-13 browse
1 | diff --git a/examples/arc_seal.rs b/examples/arc_seal.rs |
2 | index f6f84c0..beda3a6 100644 |
3 | --- a/examples/arc_seal.rs |
4 | +++ b/examples/arc_seal.rs |
5 | @@ -49,7 +49,7 @@ async fn main() { |
6 | |
7 | // Build Authenticated-Results header |
8 | let auth_results = AuthenticationResults::new("mx.mydomain.org") |
9 | - .with_dkim_result(&dkim_result, "sender@example.org") |
10 | + .with_dkim_results(&dkim_result, "sender@example.org") |
11 | .with_arc_result(&arc_result, "127.0.0.1".parse().unwrap()); |
12 | |
13 | // Seal message |
14 | diff --git a/examples/report_arf_generate.rs b/examples/report_arf_generate.rs |
15 | index 4792527..d2802a7 100644 |
16 | --- a/examples/report_arf_generate.rs |
17 | +++ b/examples/report_arf_generate.rs |
18 | @@ -40,6 +40,7 @@ fn main() { |
19 | .with_identity_alignment(IdentityAlignment::DkimSpf) |
20 | .with_message(&b"From: hello@world.org\r\nTo: ciao@mundo.org\r\n\r\n"[..]) |
21 | .to_rfc5322( |
22 | + "DMARC Reports", |
23 | "no-reply@example.org", |
24 | "ruf@otherdomain.com", |
25 | "DMARC Authentication Failure Report", |
26 | diff --git a/src/arc/seal.rs b/src/arc/seal.rs |
27 | index 038b147..381063c 100644 |
28 | --- a/src/arc/seal.rs |
29 | +++ b/src/arc/seal.rs |
30 | @@ -14,7 +14,7 @@ use mail_builder::encoders::base64::base64_encode; |
31 | |
32 | use crate::{ |
33 | common::{ |
34 | - crypto::{HashContext, Sha256, SigningKey}, |
35 | + crypto::{HashAlgorithm, HashContext, Sha256, SigningKey}, |
36 | headers::Writer, |
37 | }, |
38 | dkim::{Canonicalization, Done}, |
39 | @@ -55,26 +55,46 @@ impl<T: SigningKey<Hasher = Sha256>> ArcSealer<T, Done> { |
40 | _ => ChainValidation::Fail, |
41 | }; |
42 | } |
43 | - |
44 | - // Create hashes |
45 | - let mut body_hasher = self.key.hasher(); |
46 | + // Canonicalize headers |
47 | let mut header_hasher = self.key.hasher(); |
48 | - |
49 | - // Canonicalize headers and body |
50 | - let (body_len, signed_headers) = |
51 | - set.signature |
52 | - .canonicalize(message, &mut header_hasher, &mut body_hasher)?; |
53 | + let signed_headers = set |
54 | + .signature |
55 | + .canonicalize_headers(message, &mut header_hasher)?; |
56 | |
57 | if signed_headers.is_empty() { |
58 | return Err(Error::NoHeadersFound); |
59 | } |
60 | |
61 | + // Canonicalize body |
62 | + if set.signature.l > 0 { |
63 | + set.signature.l = (message.raw_message.len() - message.body_offset) as u64; |
64 | + } |
65 | + let ha = HashAlgorithm::from(set.signature.a); |
66 | + if let Some((_, _, _, bh)) = message |
67 | + .body_hashes |
68 | + .iter() |
69 | + .find(|(c, h, l, _)| c == &set.signature.cb && h == &ha && l == &set.signature.l) |
70 | + { |
71 | + // Use cached hash |
72 | + set.signature.bh = base64_encode(bh)?; |
73 | + } else { |
74 | + let mut body_hasher = self.key.hasher(); |
75 | + set.signature.cb.canonicalize_body( |
76 | + message |
77 | + .raw_message |
78 | + .get(message.body_offset..) |
79 | + .unwrap_or_default(), |
80 | + &mut body_hasher, |
81 | + ); |
82 | + set.signature.bh = base64_encode(body_hasher.complete().as_ref())?; |
83 | + } |
84 | + |
85 | // Create Signature |
86 | let now = SystemTime::now() |
87 | .duration_since(SystemTime::UNIX_EPOCH) |
88 | .map(|d| d.as_secs()) |
89 | .unwrap_or(0); |
90 | - set.signature.bh = base64_encode(body_hasher.complete().as_ref())?; |
91 | + |
92 | set.signature.t = now; |
93 | set.signature.x = if set.signature.x > 0 { |
94 | now + set.signature.x |
95 | @@ -82,9 +102,6 @@ impl<T: SigningKey<Hasher = Sha256>> ArcSealer<T, Done> { |
96 | 0 |
97 | }; |
98 | set.signature.h = signed_headers; |
99 | - if set.signature.l > 0 { |
100 | - set.signature.l = body_len as u64; |
101 | - } |
102 | |
103 | // Add signature to hash |
104 | set.signature.write(&mut header_hasher, false); |
105 | @@ -123,13 +140,11 @@ impl<T: SigningKey<Hasher = Sha256>> ArcSealer<T, Done> { |
106 | } |
107 | |
108 | impl Signature { |
109 | - #[allow(clippy::while_let_on_iterator)] |
110 | - pub(crate) fn canonicalize<'x>( |
111 | + pub(crate) fn canonicalize_headers<'x>( |
112 | &self, |
113 | message: &'x AuthenticatedMessage<'x>, |
114 | header_hasher: &mut impl Writer, |
115 | - body_hasher: &mut impl Writer, |
116 | - ) -> crate::Result<(usize, Vec<String>)> { |
117 | + ) -> crate::Result<Vec<String>> { |
118 | let mut headers = Vec::with_capacity(self.h.len()); |
119 | let mut found_headers = vec![false; self.h.len()]; |
120 | let mut signed_headers = Vec::with_capacity(self.h.len()); |
121 | @@ -146,10 +161,8 @@ impl Signature { |
122 | } |
123 | } |
124 | |
125 | - let body_len = message.body.len(); |
126 | self.ch |
127 | .canonicalize_headers(&mut headers.into_iter().rev(), header_hasher); |
128 | - self.cb.canonicalize_body(message.body, body_hasher); |
129 | |
130 | // Add any missing headers |
131 | signed_headers.reverse(); |
132 | @@ -159,7 +172,7 @@ impl Signature { |
133 | } |
134 | } |
135 | |
136 | - Ok((body_len, signed_headers)) |
137 | + Ok(signed_headers) |
138 | } |
139 | } |
140 | |
141 | @@ -287,7 +300,7 @@ GMot/L2x0IYyMLAz6oLWh2hm7zwtb0CgOrPo1ke44hFYnfc= |
142 | "ARC validation failed: {:?}", |
143 | arc_result.result() |
144 | ); |
145 | - let auth_results = AuthenticationResults::new(d).with_dkim_result(&dkim_result, d); |
146 | + let auth_results = AuthenticationResults::new(d).with_dkim_results(&dkim_result, d); |
147 | let arc = ArcSealer::from_key(pk) |
148 | .domain(d) |
149 | .selector(s) |
150 | diff --git a/src/common/auth_results.rs b/src/common/auth_results.rs |
151 | index 6939b28..7f740c4 100644 |
152 | --- a/src/common/auth_results.rs |
153 | +++ b/src/common/auth_results.rs |
154 | @@ -8,7 +8,11 @@ |
155 | * except according to those terms. |
156 | */ |
157 | |
158 | - use std::{borrow::Cow, fmt::Write, net::IpAddr}; |
159 | + use std::{ |
160 | + borrow::Cow, |
161 | + fmt::{Display, Write}, |
162 | + net::IpAddr, |
163 | + }; |
164 | |
165 | use mail_builder::encoders::base64::base64_encode; |
166 | |
167 | @@ -27,38 +31,47 @@ impl<'x> AuthenticationResults<'x> { |
168 | } |
169 | } |
170 | |
171 | - pub fn with_dkim_result(mut self, dkim: &[DkimOutput], header_from: &str) -> Self { |
172 | + pub fn with_dkim_results(mut self, dkim: &[DkimOutput], header_from: &str) -> Self { |
173 | for dkim in dkim { |
174 | - if !dkim.is_atps { |
175 | - self.auth_results.push_str(";\r\n\tdkim="); |
176 | + self.set_dkim_result(dkim, header_from); |
177 | + } |
178 | + self |
179 | + } |
180 | + |
181 | + pub fn with_dkim_result(mut self, dkim: &DkimOutput, header_from: &str) -> Self { |
182 | + self.set_dkim_result(dkim, header_from); |
183 | + self |
184 | + } |
185 | + |
186 | + pub fn set_dkim_result(&mut self, dkim: &DkimOutput, header_from: &str) { |
187 | + if !dkim.is_atps { |
188 | + self.auth_results.push_str(";\r\n\tdkim="); |
189 | + } else { |
190 | + self.auth_results.push_str(";\r\n\tdkim-atps="); |
191 | + } |
192 | + dkim.result.as_auth_result(&mut self.auth_results); |
193 | + if let Some(signature) = &dkim.signature { |
194 | + if !signature.i.is_empty() { |
195 | + self.auth_results.push_str(" header.i="); |
196 | + self.auth_results.push_str(&signature.i); |
197 | } else { |
198 | - self.auth_results.push_str(";\r\n\tdkim-atps="); |
199 | + self.auth_results.push_str(" header.d="); |
200 | + self.auth_results.push_str(&signature.d); |
201 | } |
202 | - dkim.result.as_auth_result(&mut self.auth_results); |
203 | - if let Some(signature) = &dkim.signature { |
204 | - if !signature.i.is_empty() { |
205 | - self.auth_results.push_str(" header.i="); |
206 | - self.auth_results.push_str(&signature.i); |
207 | - } else { |
208 | - self.auth_results.push_str(" header.d="); |
209 | - self.auth_results.push_str(&signature.d); |
210 | - } |
211 | - self.auth_results.push_str(" header.s="); |
212 | - self.auth_results.push_str(&signature.s); |
213 | - if signature.b.len() >= 6 { |
214 | - self.auth_results.push_str(" header.b="); |
215 | - self.auth_results.push_str( |
216 | - &String::from_utf8(base64_encode(&signature.b[..6]).unwrap_or_default()) |
217 | - .unwrap_or_default(), |
218 | - ); |
219 | - } |
220 | + self.auth_results.push_str(" header.s="); |
221 | + self.auth_results.push_str(&signature.s); |
222 | + if signature.b.len() >= 6 { |
223 | + self.auth_results.push_str(" header.b="); |
224 | + self.auth_results.push_str( |
225 | + &String::from_utf8(base64_encode(&signature.b[..6]).unwrap_or_default()) |
226 | + .unwrap_or_default(), |
227 | + ); |
228 | } |
229 | + } |
230 | |
231 | - if dkim.is_atps { |
232 | - write!(self.auth_results, " header.from={}", header_from).ok(); |
233 | - } |
234 | + if dkim.is_atps { |
235 | + write!(self.auth_results, " header.from={}", header_from).ok(); |
236 | } |
237 | - self |
238 | } |
239 | |
240 | pub fn with_spf_ehlo_result( |
241 | @@ -136,6 +149,13 @@ impl<'x> AuthenticationResults<'x> { |
242 | } |
243 | } |
244 | |
245 | + impl<'x> Display for AuthenticationResults<'x> { |
246 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
247 | + f.write_str(self.hostname)?; |
248 | + f.write_str(&self.auth_results) |
249 | + } |
250 | + } |
251 | + |
252 | impl<'x> HeaderWriter for AuthenticationResults<'x> { |
253 | fn write_header(&self, writer: &mut impl Writer) { |
254 | writer.write(b"Authentication-Results: "); |
255 | @@ -401,7 +421,7 @@ mod test { |
256 | }, |
257 | ), |
258 | ] { |
259 | - auth_results = auth_results.with_dkim_result(&[dkim], "jdoe@example.org"); |
260 | + auth_results = auth_results.with_dkim_results(&[dkim], "jdoe@example.org"); |
261 | assert_eq!( |
262 | auth_results.auth_results.rsplit_once(';').unwrap().1.trim(), |
263 | expected_auth_results |
264 | diff --git a/src/common/message.rs b/src/common/message.rs |
265 | index 2f36c7a..c6ed497 100644 |
266 | --- a/src/common/message.rs |
267 | +++ b/src/common/message.rs |
268 | @@ -23,7 +23,7 @@ impl<'x> AuthenticatedMessage<'x> { |
269 | let mut message = AuthenticatedMessage { |
270 | headers: Vec::new(), |
271 | from: Vec::new(), |
272 | - body: b"", |
273 | + raw_message, |
274 | body_offset: 0, |
275 | body_hashes: Vec::new(), |
276 | dkim_headers: Vec::new(), |
277 | @@ -152,16 +152,16 @@ impl<'x> AuthenticatedMessage<'x> { |
278 | // Obtain message body |
279 | if let Some(offset) = headers.body_offset() { |
280 | message.body_offset = offset; |
281 | - message.body = raw_message.get(offset..).unwrap_or_default(); |
282 | } else { |
283 | message.body_offset = raw_message.len(); |
284 | } |
285 | + let body = raw_message.get(message.body_offset..).unwrap_or_default(); |
286 | |
287 | // Calculate body hashes |
288 | for (cb, ha, l, bh) in &mut message.body_hashes { |
289 | *bh = match ha { |
290 | - HashAlgorithm::Sha256 => cb.hash_body::<Sha256>(message.body, *l).as_ref().to_vec(), |
291 | - HashAlgorithm::Sha1 => cb.hash_body::<Sha1>(message.body, *l).as_ref().to_vec(), |
292 | + HashAlgorithm::Sha256 => cb.hash_body::<Sha256>(body, *l).as_ref().to_vec(), |
293 | + HashAlgorithm::Sha1 => cb.hash_body::<Sha1>(body, *l).as_ref().to_vec(), |
294 | }; |
295 | } |
296 | |
297 | @@ -205,11 +205,19 @@ impl<'x> AuthenticatedMessage<'x> { |
298 | self.date_header_present |
299 | } |
300 | |
301 | + pub fn raw_headers(&self) -> &[u8] { |
302 | + self.raw_message.get(..self.body_offset).unwrap_or_default() |
303 | + } |
304 | + |
305 | pub fn body_offset(&self) -> usize { |
306 | self.body_offset |
307 | } |
308 | |
309 | - pub fn from(&self) -> &[String] { |
310 | + pub fn froms(&self) -> &[String] { |
311 | &self.from |
312 | } |
313 | + |
314 | + pub fn from(&self) -> &str { |
315 | + self.from.first().map_or("", |f| f.as_str()) |
316 | + } |
317 | } |
318 | diff --git a/src/common/verify.rs b/src/common/verify.rs |
319 | index 1a3f1d6..c1b6857 100644 |
320 | --- a/src/common/verify.rs |
321 | +++ b/src/common/verify.rs |
322 | @@ -88,7 +88,7 @@ impl DomainKey { |
323 | } |
324 | } |
325 | |
326 | - pub(crate) trait VerifySignature { |
327 | + pub trait VerifySignature { |
328 | fn selector(&self) -> &str; |
329 | |
330 | fn domain(&self) -> &str; |
331 | diff --git a/src/dkim/mod.rs b/src/dkim/mod.rs |
332 | index e4bc283..6c5be8a 100644 |
333 | --- a/src/dkim/mod.rs |
334 | +++ b/src/dkim/mod.rs |
335 | @@ -161,6 +161,12 @@ impl VerifySignature for Signature { |
336 | } |
337 | } |
338 | |
339 | + impl Signature { |
340 | + pub fn identity(&self) -> &str { |
341 | + &self.i |
342 | + } |
343 | + } |
344 | + |
345 | impl<'x> DkimOutput<'x> { |
346 | pub(crate) fn pass() -> Self { |
347 | DkimOutput { |
348 | diff --git a/src/dmarc/mod.rs b/src/dmarc/mod.rs |
349 | index 93f16b7..b462429 100644 |
350 | --- a/src/dmarc/mod.rs |
351 | +++ b/src/dmarc/mod.rs |
352 | @@ -90,6 +90,14 @@ impl URI { |
353 | max_size, |
354 | } |
355 | } |
356 | + |
357 | + pub fn uri(&self) -> &str { |
358 | + &self.uri |
359 | + } |
360 | + |
361 | + pub fn max_size(&self) -> usize { |
362 | + self.max_size |
363 | + } |
364 | } |
365 | |
366 | impl From<Error> for DmarcResult { |
367 | @@ -155,6 +163,16 @@ impl DmarcOutput { |
368 | self.record.as_deref() |
369 | } |
370 | |
371 | + pub fn dmarc_record_cloned(&self) -> Option<Arc<Dmarc>> { |
372 | + self.record.clone() |
373 | + } |
374 | + |
375 | + pub fn requested_reports(&self) -> bool { |
376 | + self.record |
377 | + .as_ref() |
378 | + .map_or(false, |r| !r.rua.is_empty() || !r.ruf.is_empty()) |
379 | + } |
380 | + |
381 | /// Returns the failure reporting options |
382 | pub fn failure_report(&self) -> Option<Report> { |
383 | // Send failure reports |
384 | diff --git a/src/lib.rs b/src/lib.rs |
385 | index 8e9d18b..f2714e7 100644 |
386 | --- a/src/lib.rs |
387 | +++ b/src/lib.rs |
388 | @@ -318,7 +318,7 @@ pub struct MX { |
389 | pub struct AuthenticatedMessage<'x> { |
390 | pub(crate) headers: Vec<(&'x [u8], &'x [u8])>, |
391 | pub(crate) from: Vec<String>, |
392 | - pub(crate) body: &'x [u8], |
393 | + pub(crate) raw_message: &'x [u8], |
394 | pub(crate) body_offset: usize, |
395 | pub(crate) body_hashes: Vec<(Canonicalization, HashAlgorithm, u64, Vec<u8>)>, |
396 | pub(crate) dkim_headers: Vec<Header<'x, crate::Result<dkim::Signature>>>, |
397 | diff --git a/src/report/arf/generate.rs b/src/report/arf/generate.rs |
398 | index aff004c..972bb4a 100644 |
399 | --- a/src/report/arf/generate.rs |
400 | +++ b/src/report/arf/generate.rs |
401 | @@ -22,7 +22,8 @@ use crate::report::{AuthFailureType, DeliveryResult, Feedback, FeedbackType, Ide |
402 | impl<'x> Feedback<'x> { |
403 | pub fn write_rfc5322( |
404 | &self, |
405 | - from: &'x str, |
406 | + from_name: &'x str, |
407 | + from_addr: &'x str, |
408 | to: &'x str, |
409 | subject: &'x str, |
410 | writer: impl io::Write, |
411 | @@ -89,7 +90,7 @@ impl<'x> Feedback<'x> { |
412 | } |
413 | |
414 | MessageBuilder::new() |
415 | - .header("From", HeaderType::Text(from.into())) |
416 | + .from((from_name, from_addr)) |
417 | .header("To", HeaderType::Text(to.into())) |
418 | .header("Auto-Submitted", HeaderType::Text("auto-generated".into())) |
419 | .subject(subject) |
420 | @@ -100,9 +101,15 @@ impl<'x> Feedback<'x> { |
421 | .write_to(writer) |
422 | } |
423 | |
424 | - pub fn to_rfc5322(&self, from: &str, to: &str, subject: &str) -> io::Result<String> { |
425 | + pub fn to_rfc5322( |
426 | + &self, |
427 | + from_name: &str, |
428 | + from_addr: &str, |
429 | + to: &str, |
430 | + subject: &str, |
431 | + ) -> io::Result<String> { |
432 | let mut buf = Vec::new(); |
433 | - self.write_rfc5322(from, to, subject, &mut buf)?; |
434 | + self.write_rfc5322(from_name, from_addr, to, subject, &mut buf)?; |
435 | String::from_utf8(buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err)) |
436 | } |
437 | |
438 | @@ -223,7 +230,7 @@ impl<'x> Feedback<'x> { |
439 | write!(&mut arf, "Reported-URI: {}\r\n", value).ok(); |
440 | } |
441 | if let Some(value) = &self.reporting_mta { |
442 | - write!(&mut arf, "Reporting-MTA: {}\r\n", value).ok(); |
443 | + write!(&mut arf, "Reporting-MTA: dns;{}\r\n", value).ok(); |
444 | } |
445 | if let Some(value) = &self.source_ip { |
446 | write!(&mut arf, "Source-IP: {}\r\n", value).ok(); |
447 | @@ -275,6 +282,7 @@ mod test { |
448 | |
449 | let message = feedback |
450 | .to_rfc5322( |
451 | + "DMARC Reporter", |
452 | "no-reply@example.org", |
453 | "ruf@otherdomain.com", |
454 | "DMARC Authentication Failure Report", |
455 | diff --git a/src/report/dmarc/mod.rs b/src/report/dmarc/mod.rs |
456 | index bc16567..7049982 100644 |
457 | --- a/src/report/dmarc/mod.rs |
458 | +++ b/src/report/dmarc/mod.rs |
459 | @@ -15,7 +15,6 @@ use std::fmt::Write; |
460 | use std::net::IpAddr; |
461 | |
462 | use crate::{ |
463 | - dmarc::Dmarc, |
464 | report::{ |
465 | ActionDisposition, Alignment, DKIMAuthResult, Disposition, DkimResult, DmarcResult, |
466 | PolicyOverride, PolicyOverrideReason, Record, Report, SPFAuthResult, SPFDomainScope, |
467 | @@ -24,6 +23,8 @@ use crate::{ |
468 | ArcOutput, DkimOutput, DmarcOutput, SpfOutput, |
469 | }; |
470 | |
471 | + use super::PolicyPublished; |
472 | + |
473 | impl Report { |
474 | pub fn new() -> Self { |
475 | Self::default() |
476 | @@ -182,21 +183,8 @@ impl Report { |
477 | self |
478 | } |
479 | |
480 | - pub fn with_policy_published(mut self, dmarc: &Dmarc) -> Self { |
481 | - self.policy_published.adkim = (&dmarc.adkim).into(); |
482 | - self.policy_published.aspf = (&dmarc.aspf).into(); |
483 | - self.policy_published.p = (&dmarc.p).into(); |
484 | - self.policy_published.sp = (&dmarc.sp).into(); |
485 | - self.policy_published.testing = dmarc.t; |
486 | - self.policy_published.fo = match &dmarc.fo { |
487 | - crate::dmarc::Report::All => "0", |
488 | - crate::dmarc::Report::Any => "1", |
489 | - crate::dmarc::Report::Dkim => "d", |
490 | - crate::dmarc::Report::Spf => "s", |
491 | - crate::dmarc::Report::DkimSpf => "d:s", |
492 | - } |
493 | - .to_string() |
494 | - .into(); |
495 | + pub fn with_policy_published(mut self, policy_published: PolicyPublished) -> Self { |
496 | + self.policy_published = policy_published; |
497 | self |
498 | } |
499 | } |
500 | @@ -206,7 +194,7 @@ impl Record { |
501 | Record::default() |
502 | } |
503 | |
504 | - pub fn with_dkim_output(mut self, dkim_output: &[DkimOutput]) { |
505 | + pub fn with_dkim_output(mut self, dkim_output: &[DkimOutput]) -> Self { |
506 | for dkim in dkim_output { |
507 | if let Some(signature) = &dkim.signature { |
508 | let (result, human_result) = match &dkim.result { |
509 | @@ -232,9 +220,10 @@ impl Record { |
510 | }); |
511 | } |
512 | } |
513 | + self |
514 | } |
515 | |
516 | - pub fn with_spf_output(mut self, spf_output: &SpfOutput, scope: SPFDomainScope) { |
517 | + pub fn with_spf_output(mut self, spf_output: &SpfOutput, scope: SPFDomainScope) -> Self { |
518 | self.auth_results.spf.push(SPFAuthResult { |
519 | domain: spf_output.domain.to_string(), |
520 | scope, |
521 | @@ -249,9 +238,10 @@ impl Record { |
522 | }, |
523 | human_result: None, |
524 | }); |
525 | + self |
526 | } |
527 | |
528 | - pub fn with_dmarc_output(mut self, dmarc_output: &DmarcOutput) { |
529 | + pub fn with_dmarc_output(mut self, dmarc_output: &DmarcOutput) -> Self { |
530 | self.row.policy_evaluated.disposition = match dmarc_output.policy { |
531 | crate::dmarc::Policy::None => ActionDisposition::None, |
532 | crate::dmarc::Policy::Quarantine => ActionDisposition::Quarantine, |
533 | @@ -260,9 +250,10 @@ impl Record { |
534 | }; |
535 | self.row.policy_evaluated.dkim = (&dmarc_output.dkim_result).into(); |
536 | self.row.policy_evaluated.spf = (&dmarc_output.spf_result).into(); |
537 | + self |
538 | } |
539 | |
540 | - pub fn with_arc_output(mut self, arc_output: &ArcOutput) { |
541 | + pub fn with_arc_output(mut self, arc_output: &ArcOutput) -> Self { |
542 | if arc_output.result == crate::DkimResult::Pass { |
543 | let mut comment = "arc=pass".to_string(); |
544 | for set in arc_output.set.iter().rev() { |
545 | @@ -279,6 +270,7 @@ impl Record { |
546 | .reason |
547 | .push(PolicyOverrideReason::new(PolicyOverride::LocalPolicy).with_comment(comment)); |
548 | } |
549 | + self |
550 | } |
551 | |
552 | pub fn source_ip(&self) -> IpAddr { |
553 | @@ -381,6 +373,30 @@ impl Record { |
554 | } |
555 | } |
556 | |
557 | + impl From<DmarcOutput> for PolicyPublished { |
558 | + fn from(value: DmarcOutput) -> Self { |
559 | + let dmarc = value.record.unwrap(); |
560 | + PolicyPublished { |
561 | + domain: value.domain, |
562 | + adkim: (&dmarc.adkim).into(), |
563 | + aspf: (&dmarc.aspf).into(), |
564 | + p: (&dmarc.p).into(), |
565 | + sp: (&dmarc.sp).into(), |
566 | + testing: dmarc.t, |
567 | + fo: match &dmarc.fo { |
568 | + crate::dmarc::Report::All => "0", |
569 | + crate::dmarc::Report::Any => "1", |
570 | + crate::dmarc::Report::Dkim => "d", |
571 | + crate::dmarc::Report::Spf => "s", |
572 | + crate::dmarc::Report::DkimSpf => "d:s", |
573 | + } |
574 | + .to_string() |
575 | + .into(), |
576 | + version_published: None, |
577 | + } |
578 | + } |
579 | + } |
580 | + |
581 | impl DKIMAuthResult { |
582 | pub fn new() -> Self { |
583 | DKIMAuthResult::default() |
584 | diff --git a/src/report/mod.rs b/src/report/mod.rs |
585 | index 7d0ab31..6a94f4a 100644 |
586 | --- a/src/report/mod.rs |
587 | +++ b/src/report/mod.rs |
588 | @@ -363,3 +363,19 @@ impl Default for FeedbackType { |
589 | FeedbackType::Other |
590 | } |
591 | } |
592 | + |
593 | + impl From<&crate::DkimResult> for AuthFailureType { |
594 | + fn from(value: &crate::DkimResult) -> Self { |
595 | + match value { |
596 | + crate::DkimResult::Neutral(err) |
597 | + | crate::DkimResult::Fail(err) |
598 | + | crate::DkimResult::PermError(err) |
599 | + | crate::DkimResult::TempError(err) => match err { |
600 | + crate::Error::FailedBodyHashMatch => AuthFailureType::BodyHash, |
601 | + crate::Error::RevokedPublicKey => AuthFailureType::Revoked, |
602 | + _ => AuthFailureType::Signature, |
603 | + }, |
604 | + crate::DkimResult::Pass | crate::DkimResult::None => AuthFailureType::Signature, |
605 | + } |
606 | + } |
607 | + } |
608 | diff --git a/src/report/tlsrpt/mod.rs b/src/report/tlsrpt/mod.rs |
609 | index 8a07c16..c95fa54 100644 |
610 | --- a/src/report/tlsrpt/mod.rs |
611 | +++ b/src/report/tlsrpt/mod.rs |
612 | @@ -51,7 +51,7 @@ pub struct Policy { |
613 | pub failure_details: Vec<FailureDetails>, |
614 | } |
615 | |
616 | - #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] |
617 | + #[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)] |
618 | pub struct PolicyDetails { |
619 | #[serde(rename = "policy-type")] |
620 | pub policy_type: PolicyType, |
621 | @@ -80,7 +80,7 @@ pub struct Summary { |
622 | pub total_failure: u32, |
623 | } |
624 | |
625 | - #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] |
626 | + #[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)] |
627 | pub struct FailureDetails { |
628 | #[serde(rename = "result-type")] |
629 | pub result_type: ResultType, |
630 | @@ -89,8 +89,7 @@ pub struct FailureDetails { |
631 | pub sending_mta_ip: Option<IpAddr>, |
632 | |
633 | #[serde(rename = "receiving-mx-hostname")] |
634 | - #[serde(default)] |
635 | - pub receiving_mx_hostname: String, |
636 | + pub receiving_mx_hostname: Option<String>, |
637 | |
638 | #[serde(rename = "receiving-mx-helo")] |
639 | pub receiving_mx_helo: Option<String>, |
640 | @@ -106,8 +105,7 @@ pub struct FailureDetails { |
641 | pub additional_information: Option<String>, |
642 | |
643 | #[serde(rename = "failure-reason-code")] |
644 | - #[serde(default)] |
645 | - pub failure_reason_code: String, |
646 | + pub failure_reason_code: Option<String>, |
647 | } |
648 | |
649 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] |
650 | @@ -122,7 +120,7 @@ pub struct DateRange { |
651 | pub end_datetime: DateTime, |
652 | } |
653 | |
654 | - #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] |
655 | + #[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)] |
656 | pub enum PolicyType { |
657 | #[serde(rename = "tlsa")] |
658 | Tlsa, |
659 | @@ -131,10 +129,11 @@ pub enum PolicyType { |
660 | #[serde(rename = "no-policy-found")] |
661 | NoPolicyFound, |
662 | #[serde(other)] |
663 | + #[default] |
664 | Other, |
665 | } |
666 | |
667 | - #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] |
668 | + #[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)] |
669 | pub enum ResultType { |
670 | #[serde(rename = "starttls-not-supported")] |
671 | StartTlsNotSupported, |
672 | @@ -159,6 +158,7 @@ pub enum ResultType { |
673 | #[serde(rename = "sts-webpki-invalid")] |
674 | StsWebpkiInvalid, |
675 | #[serde(other)] |
676 | + #[default] |
677 | Other, |
678 | } |
679 | |
680 | @@ -178,3 +178,28 @@ where |
681 | { |
682 | serializer.serialize_str(&datetime.to_rfc3339()) |
683 | } |
684 | + |
685 | + impl PolicyDetails { |
686 | + pub fn new(policy_type: PolicyType, policy_domain: impl Into<String>) -> Self { |
687 | + Self { |
688 | + policy_type, |
689 | + policy_string: vec![], |
690 | + policy_domain: policy_domain.into(), |
691 | + mx_host: vec![], |
692 | + } |
693 | + } |
694 | + } |
695 | + |
696 | + impl FailureDetails { |
697 | + pub fn new(result_type: impl Into<ResultType>) -> Self { |
698 | + FailureDetails { |
699 | + result_type: result_type.into(), |
700 | + ..Default::default() |
701 | + } |
702 | + } |
703 | + |
704 | + pub fn with_failure_reason_code(mut self, code: impl Into<String>) -> Self { |
705 | + self.failure_reason_code = Some(code.into()); |
706 | + self |
707 | + } |
708 | + } |