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::{TokioResolver, proto::op::ResponseCode};
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<Box<str>, Txt>,
47 MXX: ResolverCache<Box<str>, RecordSet<MX>>,
48 IPV4: ResolverCache<Box<str>, RecordSet<Ipv4Addr>>,
49 IPV6: ResolverCache<Box<str>, RecordSet<Ipv6Addr>>,
50 PTR: ResolverCache<IpAddr, RecordSet<Box<str>>>,
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 RecordSet<T> {
102 pub rrset: Arc<[T]>,
103 pub dnssec_status: DnssecStatus,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct MX {
108 pub exchanges: Box<[Box<str>]>,
109 pub preference: u16,
110}
111
112#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq)]
113#[repr(u16)]
114pub enum DnssecStatus {
115 Secure,
116 Insecure,
117 Bogus,
118 #[default]
119 Indeterminate,
120}
121
122#[derive(Debug, Clone, Default, PartialEq, Eq)]
123pub struct AuthenticatedMessage<'x> {
124 pub headers: Vec<(&'x [u8], &'x [u8])>,
125 pub from: Vec<String>,
126 pub raw_message: &'x [u8],
127 pub body_offset: u32,
128 pub body_hashes: Vec<(Canonicalization, HashAlgorithm, u64, Vec<u8>)>,
129 pub dkim_headers: Vec<Header<'x, crate::Result<dkim::Signature>>>,
130 pub ams_headers: Vec<Header<'x, crate::Result<arc::Signature>>>,
131 pub as_headers: Vec<Header<'x, crate::Result<arc::Seal>>>,
132 pub aar_headers: Vec<Header<'x, crate::Result<arc::Results>>>,
133 pub received_headers_count: usize,
134 pub date_header_present: bool,
135 pub message_id_header_present: bool,
136 pub has_arc_errors: bool,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct AuthenticationResults<'x> {
142 pub(crate) hostname: &'x str,
143 pub(crate) auth_results: String,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct ReceivedSpf {
149 pub(crate) received_spf: String,
150}
151
152#[derive(Debug, PartialEq, Eq, Clone)]
153pub enum DkimResult {
154 Pass,
155 Neutral(crate::Error),
156 Fail(crate::Error),
157 PermError(crate::Error),
158 TempError(crate::Error),
159 None,
160}
161
162#[derive(Debug, PartialEq, Eq, Clone)]
163pub struct DkimOutput<'x> {
164 result: DkimResult,
165 signature: Option<&'x dkim::Signature>,
166 report: Option<String>,
167 is_atps: bool,
168}
169
170#[derive(Debug, PartialEq, Eq, Clone)]
171pub struct ArcOutput<'x> {
172 result: DkimResult,
173 set: Vec<Set<'x>>,
174}
175
176#[derive(Debug, PartialEq, Eq, Clone, Copy)]
177pub enum SpfResult {
178 Pass,
179 Fail,
180 SoftFail,
181 Neutral,
182 TempError,
183 PermError,
184 None,
185}
186
187#[derive(Debug, PartialEq, Eq, Clone)]
188pub struct SpfOutput {
189 result: SpfResult,
190 domain: String,
191 report: Option<String>,
192 explanation: Option<String>,
193}
194
195#[derive(Debug, PartialEq, Eq, Clone)]
196pub struct DmarcOutput {
197 spf_result: DmarcResult,
198 dkim_result: DmarcResult,
199 domain: String,
200 policy: dmarc::Policy,
201 record: Option<Arc<Dmarc>>,
202}
203
204#[derive(Debug, PartialEq, Eq, Clone)]
205pub enum DmarcResult {
206 Pass,
207 Fail(crate::Error),
208 TempError(crate::Error),
209 PermError(crate::Error),
210 None,
211}
212
213#[derive(Debug, PartialEq, Eq, Clone)]
214pub struct IprevOutput {
215 pub result: IprevResult,
216 pub ptr: Option<Arc<[Box<str>]>>,
217}
218
219#[derive(Debug, PartialEq, Eq, Clone)]
220pub enum IprevResult {
221 Pass,
222 Fail(crate::Error),
223 TempError(crate::Error),
224 PermError(crate::Error),
225 None,
226}
227
228#[derive(Debug, Hash, PartialEq, Eq, Clone)]
229pub enum Version {
230 V1,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
234pub enum Error {
235 ParseError,
236 MissingParameters,
237 NoHeadersFound,
238 CryptoError(String),
239 Io(String),
240 Base64,
241 UnsupportedVersion,
242 UnsupportedAlgorithm,
243 UnsupportedCanonicalization,
244 UnsupportedKeyType,
245 FailedBodyHashMatch,
246 FailedVerification,
247 FailedAuidMatch,
248 RevokedPublicKey,
249 IncompatibleAlgorithms,
250 SignatureExpired,
251 SignatureLength,
252 DnsError(String),
253 DnsRecordNotFound(ResponseCode),
254 ArcChainTooLong,
255 ArcInvalidInstance(u32),
256 ArcInvalidCV,
257 ArcHasHeaderTag,
258 ArcBrokenChain,
259 NotAligned,
260 InvalidRecordType,
261}
262
263pub type Result<T> = std::result::Result<T, Error>;
264
265impl std::error::Error for Error {}
266
267impl Display for Error {
268 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 match self {
270 Error::ParseError => write!(f, "Parse error"),
271 Error::MissingParameters => write!(f, "Missing parameters"),
272 Error::NoHeadersFound => write!(f, "No headers found"),
273 Error::CryptoError(err) => write!(f, "Cryptography layer error: {err}"),
274 Error::Io(e) => write!(f, "I/O error: {e}"),
275 Error::Base64 => write!(f, "Base64 encode or decode error."),
276 Error::UnsupportedVersion => write!(f, "Unsupported version in DKIM Signature"),
277 Error::UnsupportedAlgorithm => write!(f, "Unsupported algorithm in DKIM Signature"),
278 Error::UnsupportedCanonicalization => {
279 write!(f, "Unsupported canonicalization method in DKIM Signature")
280 }
281 Error::UnsupportedKeyType => {
282 write!(f, "Unsupported key type in DKIM DNS record")
283 }
284 Error::FailedBodyHashMatch => {
285 write!(f, "Calculated body hash does not match signature hash")
286 }
287 Error::RevokedPublicKey => write!(f, "Public key for this signature has been revoked"),
288 Error::IncompatibleAlgorithms => write!(
289 f,
290 "Incompatible algorithms used in signature and DKIM DNS record"
291 ),
292 Error::FailedVerification => write!(f, "Signature verification failed"),
293 Error::SignatureExpired => write!(f, "Signature expired"),
294 Error::SignatureLength => write!(f, "Insecure 'l=' tag found in Signature"),
295 Error::FailedAuidMatch => write!(f, "AUID does not match domain name"),
296 Error::ArcInvalidInstance(i) => {
297 write!(f, "Invalid 'i={i}' value found in ARC header")
298 }
299 Error::ArcInvalidCV => write!(f, "Invalid 'cv=' value found in ARC header"),
300 Error::ArcHasHeaderTag => write!(f, "Invalid 'h=' tag present in ARC-Seal"),
301 Error::ArcBrokenChain => write!(f, "Broken or missing ARC chain"),
302 Error::ArcChainTooLong => write!(f, "Too many ARC headers"),
303 Error::InvalidRecordType => write!(f, "Invalid record"),
304 Error::DnsError(err) => write!(f, "DNS resolution error: {err}"),
305 Error::DnsRecordNotFound(code) => write!(f, "DNS record not found: {code}"),
306 Error::NotAligned => write!(f, "Policy not aligned"),
307 }
308 }
309}
310
311impl Display for SpfResult {
312 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313 f.write_str(match self {
314 SpfResult::Pass => "Pass",
315 SpfResult::Fail => "Fail",
316 SpfResult::SoftFail => "SoftFail",
317 SpfResult::Neutral => "Neutral",
318 SpfResult::TempError => "TempError",
319 SpfResult::PermError => "PermError",
320 SpfResult::None => "None",
321 })
322 }
323}
324
325impl Display for IprevResult {
326 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327 match self {
328 IprevResult::Pass => f.write_str("pass"),
329 IprevResult::Fail(err) => write!(f, "fail; {err}"),
330 IprevResult::TempError(err) => write!(f, "temp error; {err}"),
331 IprevResult::PermError(err) => write!(f, "perm error; {err}"),
332 IprevResult::None => f.write_str("none"),
333 }
334 }
335}
336
337impl Display for DkimResult {
338 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339 match self {
340 DkimResult::Pass => f.write_str("pass"),
341 DkimResult::Fail(err) => write!(f, "fail; {err}"),
342 DkimResult::Neutral(err) => write!(f, "neutral; {err}"),
343 DkimResult::TempError(err) => write!(f, "temp error; {err}"),
344 DkimResult::PermError(err) => write!(f, "perm error; {err}"),
345 DkimResult::None => f.write_str("none"),
346 }
347 }
348}
349
350impl Display for DmarcResult {
351 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352 match self {
353 DmarcResult::Pass => f.write_str("pass"),
354 DmarcResult::Fail(err) => write!(f, "fail; {err}"),
355 DmarcResult::TempError(err) => write!(f, "temp error; {err}"),
356 DmarcResult::PermError(err) => write!(f, "perm error; {err}"),
357 DmarcResult::None => f.write_str("none"),
358 }
359 }
360}
361
362impl From<io::Error> for Error {
363 fn from(err: io::Error) -> Self {
364 Error::Io(err.to_string())
365 }
366}
367
368#[cfg(feature = "rsa")]
369impl From<rsa::errors::Error> for Error {
370 fn from(err: rsa::errors::Error) -> Self {
371 Error::CryptoError(err.to_string())
372 }
373}
374
375impl Default for SpfOutput {
376 fn default() -> Self {
377 Self {
378 result: SpfResult::None,
379 domain: Default::default(),
380 report: Default::default(),
381 explanation: Default::default(),
382 }
383 }
384}
385
386thread_local!(static COUNTER: Cell<u64> = const { Cell::new(0) });
387
388pub(crate) fn is_within_pct(pct: u8) -> bool {
392 pct == 100
393 || COUNTER.with(|c| {
394 SystemTime::now()
395 .duration_since(SystemTime::UNIX_EPOCH)
396 .map(|d| d.as_secs())
397 .unwrap_or(0)
398 .wrapping_add(c.replace(c.get() + 1))
399 .wrapping_mul(11400714819323198485u64)
400 }) % 100
401 < pct as u64
402}