Commit
Author: Mauro D [mauro@stalw.art]
Hash: f1584411a5ecf7d5689ff9afb5766e9b6dfcf5df
Timestamp: Wed, 11 Jan 2023 16:34:35 +0000 (1 year ago)

+221 -90 +/-13 browse
Cached body hash lookup for ARC seals + reporting updates.
1diff --git a/examples/arc_seal.rs b/examples/arc_seal.rs
2index 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
15index 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
27index 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
151index 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
265index 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
319index 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
332index 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
349index 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
385index 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
398index 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
456index 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
585index 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
609index 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+ }