1use super::{Alignment, Dmarc, Policy, Psd};
8use crate::DnsError;
9use crate::{
10 AuthenticatedMessage, Dkim2Result, DkimOutput, DkimResult, DmarcOutput, DmarcResult, Error, MX,
11 MessageAuthenticator, Parameters, RecordSet, ResolverCache, SpfOutput, SpfResult, Txt,
12 common::cache::NoCache, common::to_a_label, dkim2::Dkim2Output,
13};
14use std::{
15 borrow::Cow,
16 net::{IpAddr, Ipv4Addr, Ipv6Addr},
17 sync::Arc,
18};
19
20const DMARC_PREFIX: &str = "_dmarc.";
21const REPORT_PREFIX: &str = "._report._dmarc.";
22
23pub struct DmarcParameters<'x> {
24 pub message: &'x AuthenticatedMessage<'x>,
25 pub dkim_output: &'x [DkimOutput<'x>],
26 pub dkim2_output: Option<&'x Dkim2Output<'x>>,
27 pub rfc5321_mail_from_domain: &'x str,
28 pub spf_output: &'x SpfOutput,
29}
30
31impl MessageAuthenticator {
32 pub async fn verify_dmarc<'x, TXT, MXX, IPV4, IPV6, PTR>(
34 &self,
35 params: impl Into<Parameters<'x, DmarcParameters<'x>, TXT, MXX, IPV4, IPV6, PTR>>,
36 ) -> DmarcOutput
37 where
38 TXT: ResolverCache<Box<str>, Txt> + 'x,
39 MXX: ResolverCache<Box<str>, RecordSet<MX>> + 'x,
40 IPV4: ResolverCache<Box<str>, RecordSet<Ipv4Addr>> + 'x,
41 IPV6: ResolverCache<Box<str>, RecordSet<Ipv6Addr>> + 'x,
42 PTR: ResolverCache<IpAddr, RecordSet<Box<str>>> + 'x,
43 {
44 let params = params.into();
46 let message = params.params.message;
47 let dkim_output = params.params.dkim_output;
48 let dkim2_output = params.params.dkim2_output;
49 let rfc5321_mail_from_domain = to_a_label(params.params.rfc5321_mail_from_domain);
50 let rfc5321_mail_from_domain = rfc5321_mail_from_domain.as_ref();
51 let spf_output = params.params.spf_output;
52 let cache_txt = params.cache_txt;
53 let cache_ipv4 = params.cache_ipv4;
54 let mut rfc5322_from_domain = Cow::Borrowed("");
55 for from in &message.from {
56 if let Some((_, domain)) = from.rsplit_once('@') {
57 let domain = to_a_label(domain);
58 if rfc5322_from_domain.is_empty() {
59 rfc5322_from_domain = domain;
60 } else if rfc5322_from_domain != domain {
61 return DmarcOutput::default();
64 }
65 }
66 }
67 if rfc5322_from_domain.is_empty() {
68 return DmarcOutput::default();
69 }
70 let rfc5322_from_domain = rfc5322_from_domain.as_ref();
71
72 let walk = match self.dmarc_tree_walk(rfc5322_from_domain, cache_txt).await {
75 Ok(walk) => walk,
76 Err(err) => {
77 let err = DmarcResult::from(err);
78 return DmarcOutput::default()
79 .with_domain(rfc5322_from_domain)
80 .with_dkim_result(err.clone())
81 .with_spf_result(err);
82 }
83 };
84 if walk.is_empty() {
85 return DmarcOutput::default().with_domain(rfc5322_from_domain);
86 }
87
88 let author_org =
90 organizational_domain(&walk, rfc5322_from_domain).unwrap_or(rfc5322_from_domain);
91
92 let (record, is_author_record) =
96 if let Some((_, record)) = walk.iter().find(|(name, _)| *name == rfc5322_from_domain) {
97 (record, true)
98 } else if let Some((_, record)) = walk
99 .iter()
100 .find(|(name, _)| *name == author_org)
101 .or_else(|| walk.last())
102 {
103 (record, false)
104 } else {
105 return DmarcOutput::default().with_domain(rfc5322_from_domain);
106 };
107
108 let mut policy = if is_author_record {
110 record.p
112 } else if record.np != record.sp
113 && self.domain_exists(rfc5322_from_domain, cache_ipv4).await == Some(false)
114 {
115 record.np
118 } else {
119 record.sp
121 };
122
123 if policy == Policy::Unspecified {
126 if record.rua.is_empty() {
127 return DmarcOutput::default().with_domain(rfc5322_from_domain);
128 }
129 policy = Policy::None;
130 }
131
132 if record.t {
135 policy = match policy {
136 Policy::Reject => Policy::Quarantine,
137 Policy::Quarantine => Policy::None,
138 other => other,
139 };
140 }
141 let aspf = record.aspf;
142 let adkim = record.adkim;
143
144 let mut output = DmarcOutput {
145 spf_result: DmarcResult::None,
146 dkim_result: DmarcResult::None,
147 domain: rfc5322_from_domain.to_string(),
148 policy,
149 record: None,
150 };
151
152 let dkim_domains = dkim_output
153 .iter()
154 .filter(|o| o.result == DkimResult::Pass)
155 .filter_map(|o| o.signature.as_ref())
156 .map(|s| s.d.as_str())
157 .chain(
158 dkim2_output
159 .filter(|o| o.result == Dkim2Result::Pass)
160 .and_then(|o| {
161 o.chain
162 .iter()
163 .find(|link| link.signature.i == 1 && link.result == Dkim2Result::Pass)
164 .map(|link| link.signature.d.as_str())
165 }),
166 )
167 .map(to_a_label)
168 .collect::<Vec<_>>();
169
170 let mut org_memo: Vec<(&str, &str)> = vec![(rfc5322_from_domain, author_org)];
172
173 if spf_output.result == SpfResult::Pass {
174 let aligned = rfc5321_mail_from_domain == rfc5322_from_domain
176 || (aspf == Alignment::Relaxed
177 && self
178 .organizational_domain_of(
179 rfc5321_mail_from_domain,
180 cache_txt,
181 &mut org_memo,
182 )
183 .await
184 == author_org);
185 output.spf_result = if aligned {
186 DmarcResult::Pass
187 } else {
188 DmarcResult::Fail(Error::NotAligned)
189 };
190 }
191
192 let has_dkim = !dkim_domains.is_empty();
194 let mut aligned = false;
195 for d in dkim_domains.iter().map(Cow::as_ref) {
196 if d == rfc5322_from_domain
197 || (adkim == Alignment::Relaxed
198 && self
199 .organizational_domain_of(d, cache_txt, &mut org_memo)
200 .await
201 == author_org)
202 {
203 aligned = true;
204 break;
205 }
206 }
207
208 if has_dkim {
209 output.dkim_result = if aligned {
210 DmarcResult::Pass
211 } else {
212 DmarcResult::Fail(Error::NotAligned)
213 };
214 }
215
216 output.with_record(Arc::clone(record))
217 }
218
219 pub async fn verify_dmarc_report_address<'x, T: AsRef<str>>(
221 &self,
222 domain: &str,
223 addresses: &'x [T],
224 txt_cache: Option<&impl ResolverCache<Box<str>, Txt>>,
225 ) -> Option<Vec<&'x T>> {
226 let domain = to_a_label(domain);
227 let domain = domain.as_ref();
228 let mut result = Vec::with_capacity(addresses.len());
229 let mut key = String::new();
230 for address in addresses {
231 let address_ref = address.as_ref();
232 let address_domain = to_a_label(
233 address_ref
234 .rsplit_once('@')
235 .map(|(_, d)| d)
236 .unwrap_or_default(),
237 );
238 let address_domain = address_domain.as_ref();
239 let is_internal = address_domain == domain
242 || address_domain
243 .strip_suffix(domain)
244 .is_some_and(|prefix| prefix.ends_with('.'));
245 let is_authorized = is_internal || {
246 key.clear();
247 key.reserve(domain.len() + REPORT_PREFIX.len() + address_domain.len() + 1);
248 key.push_str(domain);
249 key.push_str(REPORT_PREFIX);
250 key.push_str(address_domain);
251 key.push('.');
252 match self.txt_lookup::<Dmarc>(&key, txt_cache).await {
253 Ok(_) => true,
254 Err(Error::Dns(DnsError::Resolver(_))) => return None,
255 _ => false,
256 }
257 };
258 if is_authorized {
259 result.push(address);
260 }
261 }
262
263 result.into()
264 }
265
266 async fn dmarc_tree_walk<'x>(
270 &self,
271 domain: &'x str,
272 txt_cache: Option<&impl ResolverCache<Box<str>, Txt>>,
273 ) -> crate::Result<Vec<(&'x str, Arc<Dmarc>)>> {
274 let total = domain.split('.').filter(|l| !l.is_empty()).count();
275 let mut found = Vec::new();
276 if total < 2 {
277 return Ok(found);
278 }
279
280 let mut count = total;
284 let mut key = String::with_capacity(domain.len() + DMARC_PREFIX.len() + 1);
285 loop {
286 let name = drop_leftmost_labels(domain, total - count);
287 key.clear();
288 key.push_str(DMARC_PREFIX);
289 key.push_str(name);
290 key.push('.');
291 match self.txt_lookup::<Dmarc>(&key, txt_cache).await {
292 Ok(dmarc) => {
293 let stop = matches!(dmarc.psd, Psd::Yes | Psd::No);
295 found.push((name, dmarc));
296 if stop {
297 break;
298 }
299 }
300 Err(Error::Dns(DnsError::RecordNotFound(_)))
301 | Err(Error::Dns(DnsError::InvalidRecordType)) => (),
302 Err(err) => return Err(err),
303 }
304
305 if count == 1 {
306 break;
307 }
308 count = if count >= 8 { 7 } else { count - 1 };
309 }
310
311 Ok(found)
312 }
313
314 async fn domain_exists(
316 &self,
317 domain: &str,
318 cache_ipv4: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv4Addr>>>,
319 ) -> Option<bool> {
320 match self.ipv4_lookup(domain, cache_ipv4).await {
321 Ok(_) => Some(true),
323 Err(Error::Dns(DnsError::RecordNotFound(code))) => {
326 Some(code != crate::DNS_RCODE_NXDOMAIN)
327 }
328 Err(_) => None,
329 }
330 }
331
332 async fn organizational_domain_of<'x>(
334 &self,
335 domain: &'x str,
336 txt_cache: Option<&impl ResolverCache<Box<str>, Txt>>,
337 memo: &mut Vec<(&'x str, &'x str)>,
338 ) -> &'x str {
339 if let Some(&(_, org)) = memo.iter().find(|(d, _)| *d == domain) {
340 return org;
341 }
342 let org = match self.dmarc_tree_walk(domain, txt_cache).await {
343 Ok(walk) => organizational_domain(&walk, domain).unwrap_or(domain),
344 Err(_) => domain,
345 };
346 memo.push((domain, org));
347 org
348 }
349}
350
351fn organizational_domain<'x>(walk: &[(&'x str, Arc<Dmarc>)], start: &'x str) -> Option<&'x str> {
355 for (name, record) in walk {
356 match record.psd {
357 Psd::No => return Some(name),
358 Psd::Yes if *name != start => return Some(one_label_below(name, start)),
359 _ => {}
360 }
361 }
362 walk.last().map(|(name, _)| *name)
363}
364
365fn one_label_below<'x>(psd_name: &str, start: &'x str) -> &'x str {
367 let depth = psd_name.split('.').filter(|l| !l.is_empty()).count() + 1;
368 let start_labels = start.split('.').filter(|l| !l.is_empty()).count();
369 drop_leftmost_labels(start, start_labels.saturating_sub(depth))
370}
371
372fn drop_leftmost_labels(domain: &str, n: usize) -> &str {
374 let mut suffix = domain;
375 for _ in 0..n {
376 match suffix.split_once('.') {
377 Some((_, rest)) => suffix = rest,
378 None => return "",
379 }
380 }
381 suffix
382}
383
384impl<'x> DmarcParameters<'x> {
385 pub fn new(
386 message: &'x AuthenticatedMessage<'x>,
387 dkim_output: &'x [DkimOutput<'x>],
388 rfc5321_mail_from_domain: &'x str,
389 spf_output: &'x SpfOutput,
390 ) -> Self {
391 Self {
392 message,
393 dkim_output,
394 dkim2_output: None,
395 rfc5321_mail_from_domain,
396 spf_output,
397 }
398 }
399
400 pub fn with_dkim2_output(mut self, dkim2_output: &'x Dkim2Output<'x>) -> Self {
401 self.dkim2_output = Some(dkim2_output);
402 self
403 }
404}
405
406impl<'x> From<DmarcParameters<'x>>
407 for Parameters<
408 'x,
409 DmarcParameters<'x>,
410 NoCache<Box<str>, Txt>,
411 NoCache<Box<str>, RecordSet<MX>>,
412 NoCache<Box<str>, RecordSet<Ipv4Addr>>,
413 NoCache<Box<str>, RecordSet<Ipv6Addr>>,
414 NoCache<IpAddr, RecordSet<Box<str>>>,
415 >
416{
417 fn from(params: DmarcParameters<'x>) -> Self {
418 Parameters::new(params)
419 }
420}
421
422#[cfg(test)]
423#[allow(unused)]
424mod test {
425 use super::DmarcParameters;
426 use crate::{
427 AuthenticatedMessage, DkimOutput, DkimResult, DmarcResult, Error, MessageAuthenticator,
428 SpfOutput, SpfResult,
429 common::{cache::test::DummyCaches, parse::TxtRecordParser},
430 dkim::{DkimError, Signature},
431 dmarc::{Dmarc, Policy, URI},
432 };
433 use mail_parser::MessageParser;
434 use std::time::{Duration, Instant};
435
436 #[tokio::test]
437 async fn dmarc_verify_alignment() {
438 let resolver = MessageAuthenticator::new_system_conf().unwrap();
439 let caches = DummyCaches::new();
440
441 for (
442 dmarc_dns,
443 dmarc,
444 message,
445 rfc5321_mail_from_domain,
446 signature_domain,
447 dkim,
448 spf,
449 expect_dkim,
450 expect_spf,
451 policy,
452 ) in [
453 (
455 "_dmarc.example.org.",
456 "v=DMARC1; p=reject; aspf=s; adkim=s; fo=1; rua=mailto:d@example.org",
457 "From: hello@example.org\r\n\r\n",
458 "example.org",
459 "example.org",
460 DkimResult::Pass,
461 SpfResult::Pass,
462 DmarcResult::Pass,
463 DmarcResult::Pass,
464 Policy::Reject,
465 ),
466 (
468 "_dmarc.example.org.",
469 "v=DMARC1; p=reject; aspf=r; adkim=r; fo=1; rua=mailto:d@example.org",
470 "From: hello@example.org\r\n\r\n",
471 "subdomain.example.org",
472 "subdomain.example.org",
473 DkimResult::Pass,
474 SpfResult::Pass,
475 DmarcResult::Pass,
476 DmarcResult::Pass,
477 Policy::Reject,
478 ),
479 (
481 "_dmarc.example.org.",
482 "v=DMARC1; p=reject; aspf=s; adkim=s; fo=1; rua=mailto:d@example.org",
483 "From: hello@example.org\r\n\r\n",
484 "subdomain.example.org",
485 "subdomain.example.org",
486 DkimResult::Pass,
487 SpfResult::Pass,
488 DmarcResult::Fail(Error::NotAligned),
489 DmarcResult::Fail(Error::NotAligned),
490 Policy::Reject,
491 ),
492 (
494 "_dmarc.xn--eebajf.xn--9dbq2a.",
495 "v=DMARC1; p=reject; aspf=s; adkim=s; fo=1; rua=mailto:d@xn--eebajf.xn--9dbq2a",
496 "From: hello@\u{5de}\u{5d9}\u{5d9}\u{5dc}.\u{5e7}\u{5d5}\u{5dd}\r\n\r\n",
497 "xn--eebajf.xn--9dbq2a",
498 "xn--eebajf.xn--9dbq2a",
499 DkimResult::Pass,
500 SpfResult::Pass,
501 DmarcResult::Pass,
502 DmarcResult::Pass,
503 Policy::Reject,
504 ),
505 (
507 "_dmarc.xn--eebajf.xn--9dbq2a.",
508 "v=DMARC1; p=reject; aspf=s; adkim=s; fo=1; rua=mailto:d@xn--eebajf.xn--9dbq2a",
509 "From: hello@xn--eebajf.xn--9dbq2a\r\n\r\n",
510 "\u{5de}\u{5d9}\u{5d9}\u{5dc}.\u{5e7}\u{5d5}\u{5dd}",
511 "\u{5de}\u{5d9}\u{5d9}\u{5dc}.\u{5e7}\u{5d5}\u{5dd}",
512 DkimResult::Pass,
513 SpfResult::Pass,
514 DmarcResult::Pass,
515 DmarcResult::Pass,
516 Policy::Reject,
517 ),
518 (
520 "_dmarc.example.org.",
521 "v=DMARC1; p=reject; aspf=s; adkim=s; fo=1; rua=mailto:d@example.org",
522 "From: hello@example.org\r\n\r\n",
523 "example.org",
524 "example.org",
525 DkimResult::Fail(Error::Dkim(DkimError::SignatureExpired)),
526 SpfResult::Fail,
527 DmarcResult::None,
528 DmarcResult::None,
529 Policy::Reject,
530 ),
531 ] {
532 caches.txt_add(
533 dmarc_dns,
534 Dmarc::parse(dmarc.as_bytes()).unwrap(),
535 Instant::now() + Duration::new(3200, 0),
536 );
537
538 let auth_message = AuthenticatedMessage::parse(message.as_bytes()).unwrap();
539 let signature = Signature {
540 d: signature_domain.into(),
541 ..Default::default()
542 };
543 let dkim = DkimOutput {
544 result: dkim,
545 signature: (&signature).into(),
546 report: None,
547 is_atps: false,
548 };
549 let spf = SpfOutput {
550 result: spf,
551 domain: rfc5321_mail_from_domain.to_string(),
552 report: None,
553 explanation: None,
554 };
555 let result = resolver
556 .verify_dmarc(caches.parameters(DmarcParameters::new(
557 &auth_message,
558 &[dkim],
559 rfc5321_mail_from_domain,
560 &spf,
561 )))
562 .await;
563 assert_eq!(result.dkim_result, expect_dkim, "dkim {message}");
564 assert_eq!(result.spf_result, expect_spf, "spf {message}");
565 assert_eq!(result.policy, policy, "policy {message}");
566 }
567 }
568
569 #[tokio::test]
570 async fn dmarc_policy_discovery() {
571 let resolver = MessageAuthenticator::new_system_conf().unwrap();
572 let expires = Instant::now() + Duration::new(3200, 0);
573
574 let caches = DummyCaches::new();
576 caches.txt_add(
577 "_dmarc.example.org.",
578 Dmarc::parse(b"v=DMARC1; p=reject; sp=quarantine; np=none").unwrap(),
579 expires,
580 );
581 assert_eq!(
582 policy_of(&resolver, &caches, "hello@example.org").await,
583 Policy::Reject,
584 );
585
586 let caches = DummyCaches::new();
589 caches.txt_add(
590 "_dmarc.example.org.",
591 Dmarc::parse(b"v=DMARC1; p=reject; sp=quarantine; np=none").unwrap(),
592 expires,
593 );
594 caches.ipv4_add("sub.example.org.", vec![[127, 0, 0, 1].into()], expires);
595 assert_eq!(
596 policy_of(&resolver, &caches, "hello@sub.example.org").await,
597 Policy::Quarantine,
598 );
599
600 let caches = DummyCaches::new();
602 caches.txt_add(
603 "_dmarc.example.org.",
604 Dmarc::parse(b"v=DMARC1; p=reject; sp=quarantine; np=none").unwrap(),
605 expires,
606 );
607 assert_eq!(
608 policy_of(&resolver, &caches, "hello@ghost.example.org").await,
609 Policy::None,
610 );
611
612 let caches = DummyCaches::new();
614 caches.txt_add(
615 "_dmarc.example.org.",
616 Dmarc::parse(b"v=DMARC1; rua=mailto:d@example.org").unwrap(),
617 expires,
618 );
619 assert_eq!(
620 policy_of(&resolver, &caches, "hello@example.org").await,
621 Policy::None,
622 );
623
624 let caches = DummyCaches::new();
626 let result = verify(&resolver, &caches, "hello@nothing.example").await;
627 assert_eq!(result.dmarc_record(), None);
628 }
629
630 #[tokio::test]
631 async fn dmarc_tree_walk_psd() {
632 let resolver = MessageAuthenticator::new_system_conf().unwrap();
633 let expires = Instant::now() + Duration::new(3200, 0);
634
635 let caches = DummyCaches::new();
638 caches.txt_add(
639 "_dmarc.mail.example.com.",
640 Dmarc::parse(b"v=DMARC1; p=reject; psd=n; rua=mailto:d@example.com").unwrap(),
641 expires,
642 );
643 caches.ipv4_add("a.mail.example.com.", vec![[127, 0, 0, 1].into()], expires);
644 let result = verify_aligned(
645 &resolver,
646 &caches,
647 "hello@a.mail.example.com",
648 "b.mail.example.com",
649 )
650 .await;
651 assert_eq!(result.spf_result(), &DmarcResult::Pass);
652
653 let caches = DummyCaches::new();
657 caches.txt_add(
658 "_dmarc.bank.example.",
659 Dmarc::parse(b"v=DMARC1; p=reject; psd=y; rua=mailto:d@bank.example").unwrap(),
660 expires,
661 );
662 caches.txt_add(
663 "_dmarc.giant.bank.example.",
664 Dmarc::parse(b"v=DMARC1; p=reject; rua=mailto:d@giant.bank.example").unwrap(),
665 expires,
666 );
667 let result = verify_aligned(
668 &resolver,
669 &caches,
670 "hello@giant.bank.example",
671 "mega.bank.example",
672 )
673 .await;
674 assert_eq!(result.spf_result(), &DmarcResult::Fail(Error::NotAligned));
675 }
676
677 #[tokio::test]
678 async fn dmarc_tree_walk_query_cap() {
679 let resolver = MessageAuthenticator::new_system_conf().unwrap();
680 let expires = Instant::now() + Duration::new(3200, 0);
681
682 let caches = DummyCaches::new();
686 caches.txt_add(
687 "_dmarc.example.com.",
688 Dmarc::parse(b"v=DMARC1; p=reject; np=none; rua=mailto:d@example.com").unwrap(),
689 expires,
690 );
691 assert_eq!(
692 policy_of(
693 &resolver,
694 &caches,
695 "hello@a.b.c.d.e.f.g.h.i.j.mail.example.com",
696 )
697 .await,
698 Policy::None,
699 );
700 }
701
702 async fn verify(
703 resolver: &MessageAuthenticator,
704 caches: &DummyCaches,
705 from: &str,
706 ) -> DmarcOutputHelper {
707 verify_aligned(resolver, caches, from, "").await
708 }
709
710 async fn verify_aligned(
711 resolver: &MessageAuthenticator,
712 caches: &DummyCaches,
713 from: &str,
714 mail_from_domain: &str,
715 ) -> DmarcOutputHelper {
716 let message = format!("From: {from}\r\n\r\n");
717 let auth_message = AuthenticatedMessage::parse(message.as_bytes()).unwrap();
718 let spf = SpfOutput {
719 result: SpfResult::Pass,
720 domain: mail_from_domain.to_string(),
721 report: None,
722 explanation: None,
723 };
724 resolver
725 .verify_dmarc(caches.parameters(DmarcParameters::new(
726 &auth_message,
727 &[],
728 mail_from_domain,
729 &spf,
730 )))
731 .await
732 }
733
734 async fn policy_of(
735 resolver: &MessageAuthenticator,
736 caches: &DummyCaches,
737 from: &str,
738 ) -> Policy {
739 verify(resolver, caches, from).await.policy()
740 }
741
742 type DmarcOutputHelper = crate::DmarcOutput;
743
744 #[tokio::test]
745 async fn dmarc_verify_dkim2() {
746 use crate::Dkim2Result;
747 use crate::dkim2::{ChainLink, Dkim2Output, Signature as Dkim2Signature};
748
749 let resolver = MessageAuthenticator::new_system_conf().unwrap();
750 let caches = DummyCaches::new();
751
752 for (dmarc_dns, dmarc, message, signature_domain, dkim2_result, expect_dkim, policy) in [
753 (
755 "_dmarc.example.org.",
756 "v=DMARC1; p=reject; aspf=s; adkim=s; fo=1; rua=mailto:d@example.org",
757 "From: hello@example.org\r\n\r\n",
758 "example.org",
759 Dkim2Result::Pass,
760 DmarcResult::Pass,
761 Policy::Reject,
762 ),
763 (
765 "_dmarc.example.org.",
766 "v=DMARC1; p=reject; aspf=r; adkim=r; fo=1; rua=mailto:d@example.org",
767 "From: hello@example.org\r\n\r\n",
768 "subdomain.example.org",
769 Dkim2Result::Pass,
770 DmarcResult::Pass,
771 Policy::Reject,
772 ),
773 (
775 "_dmarc.example.org.",
776 "v=DMARC1; p=reject; aspf=s; adkim=s; fo=1; rua=mailto:d@example.org",
777 "From: hello@example.org\r\n\r\n",
778 "subdomain.example.org",
779 Dkim2Result::Pass,
780 DmarcResult::Fail(Error::NotAligned),
781 Policy::Reject,
782 ),
783 (
785 "_dmarc.example.org.",
786 "v=DMARC1; p=reject; aspf=s; adkim=s; fo=1; rua=mailto:d@example.org",
787 "From: hello@example.org\r\n\r\n",
788 "example.org",
789 Dkim2Result::Fail(Error::NotAligned),
790 DmarcResult::None,
791 Policy::Reject,
792 ),
793 ] {
794 caches.txt_add(
795 dmarc_dns,
796 Dmarc::parse(dmarc.as_bytes()).unwrap(),
797 Instant::now() + Duration::new(3200, 0),
798 );
799
800 let auth_message = AuthenticatedMessage::parse(message.as_bytes()).unwrap();
801 let signature = Dkim2Signature {
802 i: 1,
803 d: signature_domain.into(),
804 ..Default::default()
805 };
806 let dkim2 = Dkim2Output {
807 result: dkim2_result.clone(),
808 chain: vec![ChainLink {
809 signature: &signature,
810 instance: None,
811 result: dkim2_result,
812 custody_ok: true,
813 }],
814 };
815 let spf = SpfOutput {
816 result: SpfResult::None,
817 domain: "example.org".to_string(),
818 report: None,
819 explanation: None,
820 };
821 let result = resolver
822 .verify_dmarc(
823 caches.parameters(
824 DmarcParameters::new(&auth_message, &[], "example.org", &spf)
825 .with_dkim2_output(&dkim2),
826 ),
827 )
828 .await;
829 assert_eq!(result.dkim_result, expect_dkim);
830 assert_eq!(result.policy, policy);
831 }
832 }
833
834 #[tokio::test]
835 async fn dmarc_verify_report_address() {
836 let resolver = MessageAuthenticator::new_system_conf().unwrap();
837 let caches = DummyCaches::new().with_txt(
838 "example.org._report._dmarc.external.org.",
839 Dmarc::parse(b"v=DMARC1").unwrap(),
840 Instant::now() + Duration::new(3200, 0),
841 );
842 let uris = vec![
843 URI::new("dmarc@example.org", 0),
844 URI::new("dmarc@external.org", 0),
845 URI::new("domain@other.org", 0),
846 ];
847
848 assert_eq!(
849 resolver
850 .verify_dmarc_report_address("example.org", &uris, Some(&caches.txt))
851 .await
852 .unwrap(),
853 vec![
854 &URI::new("dmarc@example.org", 0),
855 &URI::new("dmarc@external.org", 0),
856 ]
857 );
858 }
859
860 #[tokio::test]
861 async fn dmarc_verify_report_address_idn() {
862 let resolver = MessageAuthenticator::new_system_conf().unwrap();
863 let caches = DummyCaches::new();
864 let uris = vec![
865 URI::new(
866 "dmarc@\u{5de}\u{5d9}\u{5d9}\u{5dc}.\u{5e7}\u{5d5}\u{5dd}",
867 0,
868 ),
869 URI::new("dmarc@sub.xn--eebajf.xn--9dbq2a", 0),
870 ];
871
872 assert_eq!(
874 resolver
875 .verify_dmarc_report_address("xn--eebajf.xn--9dbq2a", &uris, Some(&caches.txt))
876 .await
877 .unwrap(),
878 uris.iter().collect::<Vec<_>>()
879 );
880 }
881
882 #[tokio::test]
883 async fn dmarc_alignment_is_case_insensitive() {
884 let resolver = MessageAuthenticator::new_system_conf().unwrap();
885 let caches = DummyCaches::new();
886 caches.txt_add(
887 "_dmarc.example.org.",
888 Dmarc::parse(b"v=DMARC1; p=reject; aspf=s; rua=mailto:d@example.org").unwrap(),
889 Instant::now() + Duration::new(3200, 0),
890 );
891
892 let result = verify_aligned(&resolver, &caches, "hello@example.org", "EXAMPLE.ORG").await;
893 assert_eq!(result.spf_result(), &DmarcResult::Pass);
894 }
895}