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
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    /// Only query for A (Ipv4) records
75    Ipv4Only,
76    /// Only query for AAAA (Ipv6) records
77    Ipv6Only,
78    /// Query for A and AAAA in parallel
79    //Ipv4AndIpv6,
80    /// Query for Ipv6 if that fails, query for Ipv4
81    Ipv6thenIpv4,
82    /// Query for Ipv4 if that fails, query for Ipv6 (default)
83    #[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)]
140// Authentication-Results header
141pub struct AuthenticationResults<'x> {
142    pub(crate) hostname: &'x str,
143    pub(crate) auth_results: String,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
147// Received-SPF header
148pub 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
388/// Generates a random value between 0 and 100.
389/// Returns true if the generated value is within the requested
390/// sampling percentage specified in a SPF, DKIM or DMARC policy.
391pub(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}