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