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