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