1#![doc = include_str!("../README.md")]
8
9#[cfg(feature = "arc")]
10use arc::Set;
11#[cfg(feature = "dns-doh")]
12use common::doh::DohResolver;
13use common::{
14 crypto::{CryptoError, HashAlgorithm},
15 headers::Header,
16 verify::DomainKey,
17};
18use dkim::{Atps, Canonicalization, DomainKeyReport};
19use dmarc::Dmarc;
20#[cfg(not(feature = "dns-doh"))]
21use hickory_resolver::{TokioResolver, proto::op::ResponseCode};
22use mta_sts::{MtaSts, TlsRpt};
23use spf::{Macro, Spf};
24use std::{
25 borrow::Borrow,
26 cell::Cell,
27 fmt::Display,
28 hash::Hash,
29 io,
30 net::{IpAddr, Ipv4Addr, Ipv6Addr},
31 sync::Arc,
32};
33
34#[cfg(not(feature = "dns-doh"))]
35pub(crate) use std::time::{Instant, SystemTime};
36#[cfg(feature = "dns-doh")]
37pub(crate) use web_time::{Instant, SystemTime};
38
39#[cfg(feature = "arc")]
40pub mod arc;
41pub mod common;
42pub mod dkim;
43pub mod dkim2;
44pub mod dmarc;
45pub mod mta_sts;
46#[cfg(feature = "report")]
47pub mod report;
48pub mod spf;
49
50#[cfg(all(feature = "dns-hickory", feature = "dns-doh"))]
51compile_error!(
52 "features `dns-hickory` and `dns-doh` are mutually exclusive; enable only one DNS backend"
53);
54#[cfg(not(any(feature = "dns-hickory", feature = "dns-doh")))]
55compile_error!("a DNS backend is required; enable feature `dns-hickory` or `dns-doh`");
56
57pub use flate2;
58#[cfg(not(feature = "dns-doh"))]
59pub use hickory_resolver;
60#[cfg(feature = "report")]
61pub use zip;
62
63#[derive(Clone)]
64#[cfg(not(feature = "dns-doh"))]
65pub struct MessageAuthenticator(pub TokioResolver);
66
67#[derive(Clone)]
68#[cfg(feature = "dns-doh")]
69pub struct MessageAuthenticator(pub DohResolver);
70
71pub struct Parameters<'x, P, TXT, MXX, IPV4, IPV6, PTR>
72where
73 TXT: ResolverCache<Box<str>, Txt>,
74 MXX: ResolverCache<Box<str>, RecordSet<MX>>,
75 IPV4: ResolverCache<Box<str>, RecordSet<Ipv4Addr>>,
76 IPV6: ResolverCache<Box<str>, RecordSet<Ipv6Addr>>,
77 PTR: ResolverCache<IpAddr, RecordSet<Box<str>>>,
78{
79 pub params: P,
80 pub cache_txt: Option<&'x TXT>,
81 pub cache_mx: Option<&'x MXX>,
82 pub cache_ptr: Option<&'x PTR>,
83 pub cache_ipv4: Option<&'x IPV4>,
84 pub cache_ipv6: Option<&'x IPV6>,
85}
86
87pub trait ResolverCache<K, V>: Sized {
88 fn get<Q>(&self, name: &Q) -> Option<V>
89 where
90 K: Borrow<Q>,
91 Q: Hash + Eq + ?Sized;
92 fn remove<Q>(&self, name: &Q) -> Option<V>
93 where
94 K: Borrow<Q>,
95 Q: Hash + Eq + ?Sized;
96 fn insert(&self, key: K, value: V, valid_until: Instant);
97}
98
99#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq)]
100pub enum IpLookupStrategy {
101 Ipv4Only,
103 Ipv6Only,
105 Ipv6thenIpv4,
109 #[default]
111 Ipv4thenIpv6,
112}
113
114#[derive(Clone)]
115pub enum Txt {
116 Spf(Arc<Spf>),
117 SpfMacro(Arc<Macro>),
118 DomainKey(Arc<DomainKey>),
119 DomainKeyReport(Arc<DomainKeyReport>),
120 Dmarc(Arc<Dmarc>),
121 Atps(Arc<Atps>),
122 MtaSts(Arc<MtaSts>),
123 TlsRpt(Arc<TlsRpt>),
124 Error(Error),
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct RecordSet<T> {
129 pub rrset: Arc<[T]>,
130 pub dnssec_status: DnssecStatus,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct MX {
135 pub exchanges: Box<[Box<str>]>,
136 pub preference: u16,
137}
138
139#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq)]
140#[repr(u16)]
141pub enum DnssecStatus {
142 Secure,
143 Insecure,
144 Bogus,
145 #[default]
146 Indeterminate,
147}
148
149#[derive(Debug, Clone, Default, PartialEq, Eq)]
150pub struct AuthenticatedMessage<'x> {
151 pub headers: Vec<(&'x [u8], &'x [u8])>,
152 pub from: Vec<String>,
153 pub raw_message: &'x [u8],
154 pub body_offset: u32,
155 pub body_hashes: Vec<(Canonicalization, HashAlgorithm, u64, Vec<u8>)>,
156 pub dkim_headers: Vec<Header<'x, dkim::Signature>>,
157 pub dkim2_signatures: Vec<Header<'x, dkim2::Signature>>,
158 pub dkim2_instances: Vec<Header<'x, dkim2::MessageInstance>>,
159 #[cfg(feature = "arc")]
160 pub ams_headers: Vec<Header<'x, arc::Signature>>,
161 #[cfg(feature = "arc")]
162 pub as_headers: Vec<Header<'x, arc::Seal>>,
163 #[cfg(feature = "arc")]
164 pub aar_headers: Vec<Header<'x, arc::Results>>,
165 pub received_headers_count: usize,
166 pub date_header_present: bool,
167 pub message_id_header_present: bool,
168 pub errors: Vec<Header<'x, Error>>,
169 pub has_dkim_errors: bool,
170 #[cfg(feature = "arc")]
171 pub has_arc_errors: bool,
172 pub has_dkim2_errors: bool,
173}
174
175impl<'x> AsRef<AuthenticatedMessage<'x>> for AuthenticatedMessage<'x> {
176 fn as_ref(&self) -> &AuthenticatedMessage<'x> {
177 self
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct AuthenticationResults<'x> {
184 pub(crate) hostname: &'x str,
185 pub(crate) auth_results: String,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct ReceivedSpf {
191 pub(crate) received_spf: String,
192}
193
194#[derive(Debug, PartialEq, Eq, Clone)]
195pub enum DkimResult {
196 Pass,
197 Neutral(crate::Error),
198 Fail(crate::Error),
199 PermError(crate::Error),
200 TempError(crate::Error),
201 None,
202}
203
204#[derive(Debug, PartialEq, Eq, Clone)]
205pub enum Dkim2Result {
206 Pass,
207 Fail(crate::Error),
208 PermError(crate::Error),
209 TempError(crate::Error),
210 None,
211}
212
213impl From<Error> for Dkim2Result {
214 fn from(err: Error) -> Self {
215 if matches!(&err, Error::Dns(DnsError::Resolver(_))) {
216 Dkim2Result::TempError(err)
217 } else {
218 Dkim2Result::PermError(err)
219 }
220 }
221}
222
223#[derive(Debug, PartialEq, Eq, Clone)]
224pub struct DkimOutput<'x> {
225 result: DkimResult,
226 signature: Option<&'x dkim::Signature>,
227 report: Option<String>,
228 is_atps: bool,
229}
230
231#[cfg(feature = "arc")]
232#[derive(Debug, PartialEq, Eq, Clone)]
233pub struct ArcOutput<'x> {
234 result: DkimResult,
235 set: Vec<Set<'x>>,
236}
237
238#[derive(Debug, PartialEq, Eq, Clone, Copy)]
239pub enum SpfResult {
240 Pass,
241 Fail,
242 SoftFail,
243 Neutral,
244 TempError,
245 PermError,
246 None,
247}
248
249#[derive(Debug, PartialEq, Eq, Clone)]
250pub struct SpfOutput {
251 result: SpfResult,
252 domain: String,
253 report: Option<String>,
254 explanation: Option<String>,
255}
256
257#[derive(Debug, PartialEq, Eq, Clone)]
258pub struct DmarcOutput {
259 spf_result: DmarcResult,
260 dkim_result: DmarcResult,
261 domain: String,
262 policy: dmarc::Policy,
263 record: Option<Arc<Dmarc>>,
264}
265
266#[derive(Debug, PartialEq, Eq, Clone)]
267pub enum DmarcResult {
268 Pass,
269 Fail(crate::Error),
270 TempError(crate::Error),
271 PermError(crate::Error),
272 None,
273}
274
275#[derive(Debug, PartialEq, Eq, Clone)]
276pub struct IprevOutput {
277 pub result: IprevResult,
278 pub ptr: Option<Arc<[Box<str>]>>,
279}
280
281#[derive(Debug, PartialEq, Eq, Clone)]
282pub enum IprevResult {
283 Pass,
284 Fail(crate::Error),
285 TempError(crate::Error),
286 PermError(crate::Error),
287 None,
288}
289
290#[derive(Debug, Hash, PartialEq, Eq, Clone)]
291pub enum Version {
292 V1,
293}
294
295#[derive(Debug, Clone, PartialEq, Eq)]
296pub enum DnsError {
297 Resolver(String),
298 #[cfg(not(feature = "dns-doh"))]
299 RecordNotFound(ResponseCode),
300 #[cfg(feature = "dns-doh")]
301 RecordNotFound(u16),
302 InvalidRecordType,
303}
304
305#[cfg(not(feature = "dns-doh"))]
306pub(crate) const DNS_RCODE_NXDOMAIN: ResponseCode = ResponseCode::NXDomain;
307#[cfg(feature = "dns-doh")]
308pub(crate) const DNS_RCODE_NXDOMAIN: u16 = 3;
309
310impl Display for DnsError {
311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312 match self {
313 DnsError::Resolver(err) => write!(f, "DNS resolution error: {err}"),
314 DnsError::RecordNotFound(code) => write!(f, "DNS record not found: {code}"),
315 DnsError::InvalidRecordType => write!(f, "Invalid record"),
316 }
317 }
318}
319
320impl<'x> TryFrom<&'x [u8]> for AuthenticatedMessage<'x> {
321 type Error = Error;
322
323 fn try_from(value: &'x [u8]) -> std::prelude::v1::Result<Self, Self::Error> {
324 AuthenticatedMessage::parse(value).ok_or(Error::ParseError)
325 }
326}
327
328impl<'x> TryFrom<&'x Vec<u8>> for AuthenticatedMessage<'x> {
329 type Error = Error;
330
331 fn try_from(value: &'x Vec<u8>) -> std::prelude::v1::Result<Self, Self::Error> {
332 AuthenticatedMessage::parse(value).ok_or(Error::ParseError)
333 }
334}
335
336#[derive(Debug, Clone, PartialEq, Eq)]
337pub enum Error {
338 ParseError,
339 MissingParameters,
340 NoHeadersFound,
341 Base64,
342 NotAligned,
343 Io(String),
344 Crypto(CryptoError),
345 Dns(DnsError),
346 Dkim(crate::dkim::DkimError),
347 #[cfg(feature = "arc")]
348 Arc(crate::arc::ArcError),
349 Dkim2(crate::dkim2::Dkim2Error),
350}
351
352pub type Result<T> = std::result::Result<T, Error>;
353
354impl std::error::Error for Error {}
355
356impl Display for Error {
357 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358 match self {
359 Error::ParseError => write!(f, "Parse error"),
360 Error::MissingParameters => write!(f, "Missing parameters"),
361 Error::NoHeadersFound => write!(f, "No headers found"),
362 Error::Io(e) => write!(f, "I/O error: {e}"),
363 Error::Base64 => write!(f, "Base64 encode or decode error."),
364 Error::NotAligned => write!(f, "Policy not aligned"),
365 Error::Crypto(e) => e.fmt(f),
366 Error::Dns(e) => e.fmt(f),
367 Error::Dkim(e) => e.fmt(f),
368 #[cfg(feature = "arc")]
369 Error::Arc(e) => e.fmt(f),
370 Error::Dkim2(e) => e.fmt(f),
371 }
372 }
373}
374
375impl Display for SpfResult {
376 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377 f.write_str(match self {
378 SpfResult::Pass => "Pass",
379 SpfResult::Fail => "Fail",
380 SpfResult::SoftFail => "SoftFail",
381 SpfResult::Neutral => "Neutral",
382 SpfResult::TempError => "TempError",
383 SpfResult::PermError => "PermError",
384 SpfResult::None => "None",
385 })
386 }
387}
388
389impl Display for IprevResult {
390 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391 match self {
392 IprevResult::Pass => f.write_str("pass"),
393 IprevResult::Fail(err) => write!(f, "fail; {err}"),
394 IprevResult::TempError(err) => write!(f, "temp error; {err}"),
395 IprevResult::PermError(err) => write!(f, "perm error; {err}"),
396 IprevResult::None => f.write_str("none"),
397 }
398 }
399}
400
401impl Display for DkimResult {
402 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
403 match self {
404 DkimResult::Pass => f.write_str("pass"),
405 DkimResult::Fail(err) => write!(f, "fail; {err}"),
406 DkimResult::Neutral(err) => write!(f, "neutral; {err}"),
407 DkimResult::TempError(err) => write!(f, "temp error; {err}"),
408 DkimResult::PermError(err) => write!(f, "perm error; {err}"),
409 DkimResult::None => f.write_str("none"),
410 }
411 }
412}
413
414impl Display for DmarcResult {
415 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416 match self {
417 DmarcResult::Pass => f.write_str("pass"),
418 DmarcResult::Fail(err) => write!(f, "fail; {err}"),
419 DmarcResult::TempError(err) => write!(f, "temp error; {err}"),
420 DmarcResult::PermError(err) => write!(f, "perm error; {err}"),
421 DmarcResult::None => f.write_str("none"),
422 }
423 }
424}
425
426impl From<io::Error> for Error {
427 fn from(err: io::Error) -> Self {
428 Error::Io(err.to_string())
429 }
430}
431
432#[cfg(feature = "rsa")]
433impl From<rsa::errors::Error> for Error {
434 fn from(err: rsa::errors::Error) -> Self {
435 Error::Crypto(CryptoError::Library(err.to_string()))
436 }
437}
438
439impl Default for SpfOutput {
440 fn default() -> Self {
441 Self {
442 result: SpfResult::None,
443 domain: Default::default(),
444 report: Default::default(),
445 explanation: Default::default(),
446 }
447 }
448}
449
450thread_local!(static COUNTER: Cell<u64> = const { Cell::new(0) });
451
452pub(crate) fn is_within_pct(pct: u8) -> bool {
456 pct == 100
457 || COUNTER.with(|c| {
458 SystemTime::now()
459 .duration_since(SystemTime::UNIX_EPOCH)
460 .map(|d| d.as_secs())
461 .unwrap_or(0)
462 .wrapping_add(c.replace(c.get() + 1))
463 .wrapping_mul(11400714819323198485u64)
464 }) % 100
465 < pct as u64
466}