1#![allow(dead_code)]
2
3use crate::types::record::Record;
4pub use crate::types::results::{Disposition, DispositionWithContext};
5use bstr::BString;
6use dns_resolver::Resolver;
7use std::collections::BTreeMap;
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 dkim: Vec<BTreeMap<String, BString>>,
29}
30
31impl CheckHostParams {
32 pub async fn check(self, resolver: &dyn Resolver) -> DispositionWithContext {
33 let Self {
34 from_domain,
35 mail_from_domain,
36 dkim,
37 } = self;
38
39 match DmarcContext::new(
40 &from_domain,
41 mail_from_domain.as_ref().map(|x| x.as_str()),
42 &dkim[..],
43 ) {
44 Ok(cx) => cx.check(resolver).await,
45 Err(result) => result,
46 }
47 }
48}
49
50pub(crate) enum SenderDomainAlignment {
51 Exact,
53
54 OrganizationalDomain,
57}
58
59pub(crate) enum DmarcRecordResolution {
60 TempError,
62
63 PermError,
65
66 Records(Vec<Record>),
68}
69
70impl From<DmarcRecordResolution> for Disposition {
71 fn from(value: DmarcRecordResolution) -> Self {
72 match value {
73 DmarcRecordResolution::TempError => Disposition::TempError,
74 DmarcRecordResolution::PermError => Disposition::PermError,
75 DmarcRecordResolution::Records(_) => {
76 panic!("records must be parsed before being used in disposition")
77 }
78 }
79 }
80}
81
82struct DmarcContext<'a> {
83 pub(crate) from_domain: &'a str,
84 pub(crate) mail_from_domain: Option<&'a str>,
85 pub(crate) now: SystemTime,
86 pub(crate) dkim: &'a [BTreeMap<String, BString>],
87}
88
89impl<'a> DmarcContext<'a> {
90 fn new(
96 from_domain: &'a str,
97 mail_from_domain: Option<&'a str>,
98 dkim: &'a [BTreeMap<String, BString>],
99 ) -> Result<Self, DispositionWithContext> {
100 Ok(Self {
101 from_domain,
102 mail_from_domain,
103 now: SystemTime::now(),
104 dkim,
105 })
106 }
107
108 pub async fn check(&self, resolver: &dyn Resolver) -> DispositionWithContext {
109 match fetch_dmarc_records(&format!("_dmarc.{}", self.from_domain), resolver).await {
110 DmarcRecordResolution::Records(records) => {
111 for record in records {
112 return record.evaluate(self, SenderDomainAlignment::Exact).await;
113 }
114 }
115 x => {
116 if let Some(organizational_domain) = psl::domain_str(self.from_domain) {
117 if organizational_domain != self.from_domain {
118 let address = format!("_dmarc.{}", organizational_domain);
119 match fetch_dmarc_records(&address, resolver).await {
120 DmarcRecordResolution::TempError => {
121 return DispositionWithContext {
122 result: Disposition::TempError,
123 context: format!(
124 "DNS records could not be resolved for {}",
125 address
126 ),
127 }
128 }
129 DmarcRecordResolution::PermError => {
130 return DispositionWithContext {
131 result: Disposition::PermError,
132 context: format!("no DMARC records found for {}", address),
133 }
134 }
135 DmarcRecordResolution::Records(records) => {
136 for record in records {
137 return record
138 .evaluate(self, SenderDomainAlignment::OrganizationalDomain)
139 .await;
140 }
141 }
142 }
143 } else {
144 return DispositionWithContext {
145 result: x.into(),
146 context: format!("no DMARC records found for {}", &self.from_domain),
147 };
148 }
149 }
150 }
151 }
152
153 DispositionWithContext {
154 result: Disposition::None,
155 context: format!("no DMARC records found for {}", &self.from_domain),
156 }
157 }
158}
159
160pub(crate) async fn fetch_dmarc_records(
161 address: &str,
162 resolver: &dyn Resolver,
163) -> DmarcRecordResolution {
164 let initial_txt = match resolver.resolve_txt(address).await {
165 Ok(answer) => {
166 if answer.records.is_empty() || answer.nxdomain {
167 return DmarcRecordResolution::PermError;
168 } else {
169 eprintln!("answer: {:?}", answer);
170 answer.as_txt()
171 }
172 }
173 Err(_) => {
174 return DmarcRecordResolution::TempError;
175 }
176 };
177
178 let mut records = vec![];
179
180 for txt in initial_txt {
183 if txt.starts_with("v=DMARC1;") {
184 if let Ok(record) = Record::from_str(&txt) {
185 records.push(record);
186 }
187 }
188 }
189
190 if records.is_empty() {
191 return DmarcRecordResolution::PermError;
192 }
193
194 DmarcRecordResolution::Records(records)
195}