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(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    /// Only query for A (Ipv4) records
103    Ipv4Only,
104    /// Only query for AAAA (Ipv6) records
105    Ipv6Only,
106    /// Query for A and AAAA in parallel
107    //Ipv4AndIpv6,
108    /// Query for Ipv6 if that fails, query for Ipv4
109    Ipv6thenIpv4,
110    /// Query for Ipv4 if that fails, query for Ipv6 (default)
111    #[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)]
183// Authentication-Results header
184pub struct AuthenticationResults<'x> {
185    pub(crate) hostname: &'x str,
186    pub(crate) auth_results: String,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
190// Received-SPF header
191pub 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
453/// Generates a random value between 0 and 100.
454/// Returns true if the generated value is within the requested
455/// sampling percentage specified in a SPF, DKIM or DMARC policy.
456pub(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}