1#![doc = include_str!("../README.md")]
8
9use arc::Set;
10use common::{crypto::HashAlgorithm, headers::Header, verify::DomainKey};
11use dkim::{Atps, Canonicalization, DomainKeyReport};
12use dmarc::Dmarc;
13use hickory_resolver::{proto::op::ResponseCode, TokioResolver};
14use mta_sts::{MtaSts, TlsRpt};
15use spf::{Macro, Spf};
16use std::{
17 borrow::Borrow,
18 cell::Cell,
19 fmt::Display,
20 hash::Hash,
21 io,
22 net::{IpAddr, Ipv4Addr, Ipv6Addr},
23 sync::Arc,
24 time::{Instant, SystemTime},
25};
26
27pub mod arc;
28pub mod common;
29pub mod dkim;
30pub mod dmarc;
31pub mod mta_sts;
32#[cfg(feature = "report")]
33pub mod report;
34pub mod spf;
35
36pub use flate2;
37pub use hickory_resolver;
38#[cfg(feature = "report")]
39pub use zip;
40
41#[derive(Clone)]
42pub struct MessageAuthenticator(pub TokioResolver);
43
44pub struct Parameters<'x, P, TXT, MXX, IPV4, IPV6, PTR>
45where
46 TXT: ResolverCache<String, Txt>,
47 MXX: ResolverCache<String, Arc<Vec<MX>>>,
48 IPV4: ResolverCache<String, Arc<Vec<Ipv4Addr>>>,
49 IPV6: ResolverCache<String, Arc<Vec<Ipv6Addr>>>,
50 PTR: ResolverCache<IpAddr, Arc<Vec<String>>>,
51{
52 pub params: P,
53 pub cache_txt: Option<&'x TXT>,
54 pub cache_mx: Option<&'x MXX>,
55 pub cache_ptr: Option<&'x PTR>,
56 pub cache_ipv4: Option<&'x IPV4>,
57 pub cache_ipv6: Option<&'x IPV6>,
58}
59
60pub trait ResolverCache<K, V>: Sized {
61 fn get<Q>(&self, name: &Q) -> Option<V>
62 where
63 K: Borrow<Q>,
64 Q: Hash + Eq + ?Sized;
65 fn remove<Q>(&self, name: &Q) -> Option<V>
66 where
67 K: Borrow<Q>,
68 Q: Hash + Eq + ?Sized;
69 fn insert(&self, key: K, value: V, valid_until: Instant);
70}
71
72#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq)]
73pub enum IpLookupStrategy {
74 Ipv4Only,
76 Ipv6Only,
78 Ipv6thenIpv4,
82 #[default]
84 Ipv4thenIpv6,
85}
86
87#[derive(Clone)]
88pub enum Txt {
89 Spf(Arc<Spf>),
90 SpfMacro(Arc<Macro>),
91 DomainKey(Arc<DomainKey>),
92 DomainKeyReport(Arc<DomainKeyReport>),
93 Dmarc(Arc<Dmarc>),
94 Atps(Arc<Atps>),
95 MtaSts(Arc<MtaSts>),
96 TlsRpt(Arc<TlsRpt>),
97 Error(Error),
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct MX {
102 pub exchanges: Vec<String>,
103 pub preference: u16,
104}
105
106#[derive(Debug, Clone, Default, PartialEq, Eq)]
107pub struct AuthenticatedMessage<'x> {
108 pub headers: Vec<(&'x [u8], &'x [u8])>,
109 pub from: Vec<String>,
110 pub raw_message: &'x [u8],
111 pub body_offset: u32,
112 pub body_hashes: Vec<(Canonicalization, HashAlgorithm, u64, Vec<u8>)>,
113 pub dkim_headers: Vec<Header<'x, crate::Result<dkim::Signature>>>,
114 pub ams_headers: Vec<Header<'x, crate::Result<arc::Signature>>>,
115 pub as_headers: Vec<Header<'x, crate::Result<arc::Seal>>>,
116 pub aar_headers: Vec<Header<'x, crate::Result<arc::Results>>>,
117 pub received_headers_count: usize,
118 pub date_header_present: bool,
119 pub message_id_header_present: bool,
120 pub has_arc_errors: bool,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct AuthenticationResults<'x> {
126 pub(crate) hostname: &'x str,
127 pub(crate) auth_results: String,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct ReceivedSpf {
133 pub(crate) received_spf: String,
134}
135
136#[derive(Debug, PartialEq, Eq, Clone)]
137pub enum DkimResult {
138 Pass,
139 Neutral(crate::Error),
140 Fail(crate::Error),
141 PermError(crate::Error),
142 TempError(crate::Error),
143 None,
144}
145
146#[derive(Debug, PartialEq, Eq, Clone)]
147pub struct DkimOutput<'x> {
148 result: DkimResult,
149 signature: Option<&'x dkim::Signature>,
150 report: Option<String>,
151 is_atps: bool,
152}
153
154#[derive(Debug, PartialEq, Eq, Clone)]
155pub struct ArcOutput<'x> {
156 result: DkimResult,
157 set: Vec<Set<'x>>,
158}
159
160#[derive(Debug, PartialEq, Eq, Clone, Copy)]
161pub enum SpfResult {
162 Pass,
163 Fail,
164 SoftFail,
165 Neutral,
166 TempError,
167 PermError,
168 None,
169}
170
171#[derive(Debug, PartialEq, Eq, Clone)]
172pub struct SpfOutput {
173 result: SpfResult,
174 domain: String,
175 report: Option<String>,
176 explanation: Option<String>,
177}
178
179#[derive(Debug, PartialEq, Eq, Clone)]
180pub struct DmarcOutput {
181 spf_result: DmarcResult,
182 dkim_result: DmarcResult,
183 domain: String,
184 policy: dmarc::Policy,
185 record: Option<Arc<Dmarc>>,
186}
187
188#[derive(Debug, PartialEq, Eq, Clone)]
189pub enum DmarcResult {
190 Pass,
191 Fail(crate::Error),
192 TempError(crate::Error),
193 PermError(crate::Error),
194 None,
195}
196
197#[derive(Debug, PartialEq, Eq, Clone)]
198pub struct IprevOutput {
199 pub result: IprevResult,
200 pub ptr: Option<Arc<Vec<String>>>,
201}
202
203#[derive(Debug, PartialEq, Eq, Clone)]
204pub enum IprevResult {
205 Pass,
206 Fail(crate::Error),
207 TempError(crate::Error),
208 PermError(crate::Error),
209 None,
210}
211
212#[derive(Debug, Hash, PartialEq, Eq, Clone)]
213pub enum Version {
214 V1,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub enum Error {
219 ParseError,
220 MissingParameters,
221 NoHeadersFound,
222 CryptoError(String),
223 Io(String),
224 Base64,
225 UnsupportedVersion,
226 UnsupportedAlgorithm,
227 UnsupportedCanonicalization,
228 UnsupportedKeyType,
229 FailedBodyHashMatch,
230 FailedVerification,
231 FailedAuidMatch,
232 RevokedPublicKey,
233 IncompatibleAlgorithms,
234 SignatureExpired,
235 SignatureLength,
236 DnsError(String),
237 DnsRecordNotFound(ResponseCode),
238 ArcChainTooLong,
239 ArcInvalidInstance(u32),
240 ArcInvalidCV,
241 ArcHasHeaderTag,
242 ArcBrokenChain,
243 NotAligned,
244 InvalidRecordType,
245}
246
247pub type Result<T> = std::result::Result<T, Error>;
248
249impl std::error::Error for Error {}
250
251impl Display for Error {
252 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253 match self {
254 Error::ParseError => write!(f, "Parse error"),
255 Error::MissingParameters => write!(f, "Missing parameters"),
256 Error::NoHeadersFound => write!(f, "No headers found"),
257 Error::CryptoError(err) => write!(f, "Cryptography layer error: {err}"),
258 Error::Io(e) => write!(f, "I/O error: {e}"),
259 Error::Base64 => write!(f, "Base64 encode or decode error."),
260 Error::UnsupportedVersion => write!(f, "Unsupported version in DKIM Signature"),
261 Error::UnsupportedAlgorithm => write!(f, "Unsupported algorithm in DKIM Signature"),
262 Error::UnsupportedCanonicalization => {
263 write!(f, "Unsupported canonicalization method in DKIM Signature")
264 }
265 Error::UnsupportedKeyType => {
266 write!(f, "Unsupported key type in DKIM DNS record")
267 }
268 Error::FailedBodyHashMatch => {
269 write!(f, "Calculated body hash does not match signature hash")
270 }
271 Error::RevokedPublicKey => write!(f, "Public key for this signature has been revoked"),
272 Error::IncompatibleAlgorithms => write!(
273 f,
274 "Incompatible algorithms used in signature and DKIM DNS record"
275 ),
276 Error::FailedVerification => write!(f, "Signature verification failed"),
277 Error::SignatureExpired => write!(f, "Signature expired"),
278 Error::SignatureLength => write!(f, "Insecure 'l=' tag found in Signature"),
279 Error::FailedAuidMatch => write!(f, "AUID does not match domain name"),
280 Error::ArcInvalidInstance(i) => {
281 write!(f, "Invalid 'i={i}' value found in ARC header")
282 }
283 Error::ArcInvalidCV => write!(f, "Invalid 'cv=' value found in ARC header"),
284 Error::ArcHasHeaderTag => write!(f, "Invalid 'h=' tag present in ARC-Seal"),
285 Error::ArcBrokenChain => write!(f, "Broken or missing ARC chain"),
286 Error::ArcChainTooLong => write!(f, "Too many ARC headers"),
287 Error::InvalidRecordType => write!(f, "Invalid record"),
288 Error::DnsError(err) => write!(f, "DNS resolution error: {err}"),
289 Error::DnsRecordNotFound(code) => write!(f, "DNS record not found: {code}"),
290 Error::NotAligned => write!(f, "Policy not aligned"),
291 }
292 }
293}
294
295impl Display for SpfResult {
296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297 f.write_str(match self {
298 SpfResult::Pass => "Pass",
299 SpfResult::Fail => "Fail",
300 SpfResult::SoftFail => "SoftFail",
301 SpfResult::Neutral => "Neutral",
302 SpfResult::TempError => "TempError",
303 SpfResult::PermError => "PermError",
304 SpfResult::None => "None",
305 })
306 }
307}
308
309impl Display for IprevResult {
310 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311 match self {
312 IprevResult::Pass => f.write_str("pass"),
313 IprevResult::Fail(err) => write!(f, "fail; {err}"),
314 IprevResult::TempError(err) => write!(f, "temp error; {err}"),
315 IprevResult::PermError(err) => write!(f, "perm error; {err}"),
316 IprevResult::None => f.write_str("none"),
317 }
318 }
319}
320
321impl Display for DkimResult {
322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323 match self {
324 DkimResult::Pass => f.write_str("pass"),
325 DkimResult::Fail(err) => write!(f, "fail; {err}"),
326 DkimResult::Neutral(err) => write!(f, "neutral; {err}"),
327 DkimResult::TempError(err) => write!(f, "temp error; {err}"),
328 DkimResult::PermError(err) => write!(f, "perm error; {err}"),
329 DkimResult::None => f.write_str("none"),
330 }
331 }
332}
333
334impl Display for DmarcResult {
335 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336 match self {
337 DmarcResult::Pass => f.write_str("pass"),
338 DmarcResult::Fail(err) => write!(f, "fail; {err}"),
339 DmarcResult::TempError(err) => write!(f, "temp error; {err}"),
340 DmarcResult::PermError(err) => write!(f, "perm error; {err}"),
341 DmarcResult::None => f.write_str("none"),
342 }
343 }
344}
345
346impl From<io::Error> for Error {
347 fn from(err: io::Error) -> Self {
348 Error::Io(err.to_string())
349 }
350}
351
352#[cfg(feature = "rsa")]
353impl From<rsa::errors::Error> for Error {
354 fn from(err: rsa::errors::Error) -> Self {
355 Error::CryptoError(err.to_string())
356 }
357}
358
359#[cfg(feature = "ed25519-dalek")]
360impl From<ed25519_dalek::ed25519::Error> for Error {
361 fn from(err: ed25519_dalek::ed25519::Error) -> Self {
362 Error::CryptoError(err.to_string())
363 }
364}
365
366impl Default for SpfOutput {
367 fn default() -> Self {
368 Self {
369 result: SpfResult::None,
370 domain: Default::default(),
371 report: Default::default(),
372 explanation: Default::default(),
373 }
374 }
375}
376
377thread_local!(static COUNTER: Cell<u64> = const { Cell::new(0) });
378
379pub(crate) fn is_within_pct(pct: u8) -> bool {
383 pct == 100
384 || COUNTER.with(|c| {
385 SystemTime::now()
386 .duration_since(SystemTime::UNIX_EPOCH)
387 .map(|d| d.as_secs())
388 .unwrap_or(0)
389 .wrapping_add(c.replace(c.get() + 1))
390 .wrapping_mul(11400714819323198485u64)
391 }) % 100
392 < pct as u64
393}