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