1#![allow(dead_code)]
2
3use crate::types::record::Record;
4use crate::types::results::DmarcResultWithContext;
5use dns_resolver::Resolver;
6use std::collections::BTreeMap;
7use std::net::IpAddr;
8use std::str::FromStr;
9use std::time::SystemTime;
10
11pub use types::results::DmarcResult;
12
13mod types;
14
15#[cfg(test)]
16mod tests;
17
18pub struct CheckHostParams {
19 pub from_domain: String,
21
22 pub mail_from_domain: Option<String>,
26
27 pub client_ip: IpAddr,
29
30 pub dkim: Vec<BTreeMap<String, String>>,
32}
33
34impl CheckHostParams {
35 pub async fn check(self, resolver: &dyn Resolver) -> DmarcResultWithContext {
36 let Self {
37 from_domain,
38 mail_from_domain,
39 client_ip,
40 dkim,
41 } = self;
42
43 match DmarcContext::new(
44 &from_domain,
45 mail_from_domain.as_ref().map(|x| x.as_str()),
46 client_ip,
47 &dkim[..],
48 ) {
49 Ok(cx) => cx.check(resolver).await,
50 Err(result) => result,
51 }
52 }
53}
54
55struct DmarcContext<'a> {
56 pub(crate) from_domain: &'a str,
57 pub(crate) mail_from_domain: Option<&'a str>,
58 pub(crate) client_ip: IpAddr,
59 pub(crate) now: SystemTime,
60 pub(crate) dkim: &'a [BTreeMap<String, String>],
61}
62
63impl<'a> DmarcContext<'a> {
64 fn new(
70 from_domain: &'a str,
71 mail_from_domain: Option<&'a str>,
72 client_ip: IpAddr,
73 dkim: &'a [BTreeMap<String, String>],
74 ) -> Result<Self, DmarcResultWithContext> {
75 Ok(Self {
76 from_domain,
77 mail_from_domain,
78 client_ip,
79 now: SystemTime::now(),
80 dkim,
81 })
82 }
83
84 pub async fn check(&self, resolver: &dyn Resolver) -> DmarcResultWithContext {
85 let initial_txt = match resolver.resolve_txt(self.from_domain).await {
86 Ok(answer) => {
87 if answer.records.is_empty() || answer.nxdomain {
88 return DmarcResultWithContext {
89 result: DmarcResult::Fail,
90 context: format!("no DMARC records found for {}", &self.from_domain),
91 };
92 } else {
93 answer.as_txt()
94 }
95 }
96 Err(err) => {
97 return DmarcResultWithContext {
98 result: DmarcResult::Fail,
99 context: format!("{err}"),
100 };
101 }
102 };
103
104 for txt in initial_txt {
107 if txt.starts_with("v=DMARC1") {
108 match Record::from_str(&txt) {
109 Ok(record) => {
110 return record.evaluate(self, resolver).await;
111 }
112 Err(err) => {
113 return DmarcResultWithContext {
114 result: DmarcResult::Fail,
115 context: format!("failed to parse DMARC record: {err}"),
116 };
117 }
118 }
119 }
120 }
121 DmarcResultWithContext {
122 result: DmarcResult::Fail,
123 context: format!("no DMARC records found for {}", &self.from_domain),
124 }
125 }
126}