Skip to main content

mail_auth/
lib.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7#![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    /// Only query for A (Ipv4) records
102    Ipv4Only,
103    /// Only query for AAAA (Ipv6) records
104    Ipv6Only,
105    /// Query for A and AAAA in parallel
106    //Ipv4AndIpv6,
107    /// Query for Ipv6 if that fails, query for Ipv4
108    Ipv6thenIpv4,
109    /// Query for Ipv4 if that fails, query for Ipv6 (default)
110    #[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)]
182// Authentication-Results header
183pub struct AuthenticationResults<'x> {
184    pub(crate) hostname: &'x str,
185    pub(crate) auth_results: String,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
189// Received-SPF header
190pub 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
452/// Generates a random value between 0 and 100.
453/// Returns true if the generated value is within the requested
454/// sampling percentage specified in a SPF, DKIM or DMARC policy.
455pub(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}