1use mail_auth::common::headers::HeaderWriter;
16use mail_auth::spf::verify::SpfParameters;
17use mail_auth::dmarc::verify::DmarcParameters;
18use mail_auth::{AuthenticatedMessage, AuthenticationResults, DmarcResult, MessageAuthenticator};
19use sioul_core::config::sioul_authserv_id;
20use sioul_core::headers::RawHeaders;
21use sioul_core::trust;
22use std::path::Path;
23use std::time::Duration;
24use tokio::time::timeout;
25
26const CHECKS: Duration = Duration::from_secs(20);
28
29pub struct Verifier {
30 authenticator: MessageAuthenticator,
31 id: String,
32}
33
34impl Verifier {
35 pub fn new() -> Option<Verifier> {
36 let authenticator = MessageAuthenticator::new_system_conf().ok()?;
37 Some(Verifier { authenticator, id: sioul_authserv_id() })
38 }
39
40 pub fn quick(timeout: Duration) -> Option<Verifier> {
45 let (config, mut options) = mail_auth::hickory_resolver::system_conf::read_system_conf().ok()?;
46 options.timeout = timeout;
47 options.attempts = 1;
48 options.cache_size = options.cache_size.max(16_384);
49 let authenticator = MessageAuthenticator::new(config, options).ok()?;
50 Some(Verifier { authenticator, id: sioul_authserv_id() })
51 }
52
53 pub async fn results_of(&self, raw: &[u8], whole: bool) -> Option<String> {
61 self.results(raw, whole).await
62 }
63
64 pub async fn answers(&self, domain: &str) -> bool {
68 let made_up = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_nanos());
69 let name = format!("sioul-{made_up:x}.{}.", domain.trim_end_matches('.'));
70 match timeout(CHECKS, self.authenticator.resolver().lookup_ip(name.as_str())).await {
71 Ok(Ok(_)) => true,
72 Ok(Err(e)) => e.is_no_records_found(),
73 Err(_) => false,
74 }
75 }
76
77 pub async fn stamp(&self, raw: &[u8]) -> Vec<u8> {
80 if self.stamped(raw) {
81 return raw.to_vec();
82 }
83 match timeout(CHECKS, self.results(raw, true)).await {
84 Ok(Some(header)) => [header.as_bytes(), raw].concat(),
85 _ => raw.to_vec(),
86 }
87 }
88
89 pub fn stamped(&self, raw: &[u8]) -> bool {
91 let head = &raw[..raw.len().min(16 * 1024)];
92 let needle = format!("Authentication-Results: {}", self.id);
93 String::from_utf8_lossy(head).contains(&needle)
94 }
95
96 async fn results(&self, raw: &[u8], whole: bool) -> Option<String> {
97 let message = AuthenticatedMessage::parse(raw)?;
98 let headers = RawHeaders::parse(raw);
99 let dkim = if whole { self.authenticator.verify_dkim(&message).await } else { Vec::new() };
101 let arc = if whole { Some(self.authenticator.verify_arc(&message).await) } else { None };
102 let entry = trust::boundary(&headers);
103 let mail_from = trust::mail_from(&headers).unwrap_or_default();
104 let mail_from_domain = mail_from.rsplit_once('@').map_or(String::new(), |(_, d)| d.to_ascii_lowercase());
105 let results = AuthenticationResults::new(&self.id).with_dkim_results(&dkim, message.from());
106 let Some(e) = entry else { return Some(results.to_header()) };
110 let spf = self.authenticator.verify_spf(SpfParameters::verify_mail_from(e.ip, &e.helo, &e.by, &mail_from)).await;
111 let spf_domain = if mail_from_domain.is_empty() { e.helo.clone() } else { mail_from_domain };
112 let dmarc = self.authenticator.verify_dmarc(DmarcParameters::new(&message, &dkim, &spf_domain, &spf)).await;
113 let iprev = self.authenticator.verify_iprev(e.ip).await;
114 let mut results = results.with_spf_mailfrom_result(&spf, e.ip, &mail_from, &e.helo);
115 if let Some(arc) = &arc {
116 results = results.with_arc_result(arc, e.ip);
117 }
118 results = results.with_iprev_result(&iprev, e.ip);
119 if whole || dmarc.result() == DmarcResult::Pass {
120 results = results.with_dmarc_result(&dmarc);
121 }
122 Some(results.to_header())
123 }
124
125 pub fn unstamp(&self, raw: &[u8]) -> Vec<u8> {
127 if !raw.starts_with(format!("Authentication-Results: {}", self.id).as_bytes()) {
128 return raw.to_vec();
129 }
130 let mut end = 0;
131 for line in raw.split_inclusive(|b| *b == b'\n') {
132 if end > 0 && !line.starts_with(b" ") && !line.starts_with(b"\t") {
133 break;
134 }
135 end += line.len();
136 }
137 raw[end..].to_vec()
138 }
139}
140
141pub fn verify_folder(root: &Path, again: bool) -> usize {
145 let Some(verifier) = Verifier::new() else { return 0 };
146 let Ok(runtime) = tokio::runtime::Builder::new_current_thread().enable_all().build() else { return 0 };
147 runtime.block_on(async {
148 let mut checked = 0;
149 for sub in ["new", "cur"] {
150 let Ok(entries) = std::fs::read_dir(root.join(sub)) else { continue };
151 for path in entries.filter_map(Result::ok).map(|e| e.path()).filter(|p| p.is_file()) {
152 let Ok(raw) = std::fs::read(&path) else { continue };
153 let raw = if again { verifier.unstamp(&raw) } else { raw };
154 if verifier.stamped(&raw) {
155 continue;
156 }
157 let stamped = verifier.stamp(&raw).await;
158 if stamped.len() == raw.len() {
159 continue;
160 }
161 let temporary = root.join("tmp").join(path.file_name().unwrap_or_default());
163 let written = std::fs::create_dir_all(root.join("tmp")).and_then(|()| write_private(&temporary, &stamped));
164 let modified = std::fs::metadata(&path).and_then(|m| m.modified()).ok();
165 if written.is_ok() && std::fs::rename(&temporary, &path).is_ok() {
166 if let (Some(time), Ok(file)) = (modified, std::fs::File::options().write(true).open(&path)) {
167 let _ = file.set_modified(time);
168 }
169 checked += 1;
170 }
171 }
172 }
173 checked
174 })
175}
176
177fn write_private(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> {
180 use std::io::Write;
181 let mut options = std::fs::OpenOptions::new();
182 options.write(true).create(true).truncate(true);
183 #[cfg(unix)]
184 {
185 use std::os::unix::fs::OpenOptionsExt;
186 options.mode(0o600);
187 }
188 let mut file = options.open(path)?;
189 file.write_all(bytes)?;
190 file.sync_all()
191}