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::{proto::op::ResponseCode, TokioResolver};
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<String, Txt>,
47    MXX: ResolverCache<String, Arc<Vec<MX>>>,
48    IPV4: ResolverCache<String, Arc<Vec<Ipv4Addr>>>,
49    IPV6: ResolverCache<String, Arc<Vec<Ipv6Addr>>>,
50    PTR: ResolverCache<IpAddr, Arc<Vec<String>>>,
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 MX {
102    pub exchanges: Vec<String>,
103    pub preference: u16,
104}
105
106#[derive(Debug, Clone, Default, PartialEq, Eq)]
107pub struct AuthenticatedMessage<'x> {
108    pub headers: Vec<(&'x [u8], &'x [u8])>,
109    pub from: Vec<String>,
110    pub raw_message: &'x [u8],
111    pub body_offset: u32,
112    pub body_hashes: Vec<(Canonicalization, HashAlgorithm, u64, Vec<u8>)>,
113    pub dkim_headers: Vec<Header<'x, crate::Result<dkim::Signature>>>,
114    pub ams_headers: Vec<Header<'x, crate::Result<arc::Signature>>>,
115    pub as_headers: Vec<Header<'x, crate::Result<arc::Seal>>>,
116    pub aar_headers: Vec<Header<'x, crate::Result<arc::Results>>>,
117    pub received_headers_count: usize,
118    pub date_header_present: bool,
119    pub message_id_header_present: bool,
120    pub has_arc_errors: bool,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124// Authentication-Results header
125pub struct AuthenticationResults<'x> {
126    pub(crate) hostname: &'x str,
127    pub(crate) auth_results: String,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
131// Received-SPF header
132pub struct ReceivedSpf {
133    pub(crate) received_spf: String,
134}
135
136#[derive(Debug, PartialEq, Eq, Clone)]
137pub enum DkimResult {
138    Pass,
139    Neutral(crate::Error),
140    Fail(crate::Error),
141    PermError(crate::Error),
142    TempError(crate::Error),
143    None,
144}
145
146#[derive(Debug, PartialEq, Eq, Clone)]
147pub struct DkimOutput<'x> {
148    result: DkimResult,
149    signature: Option<&'x dkim::Signature>,
150    report: Option<String>,
151    is_atps: bool,
152}
153
154#[derive(Debug, PartialEq, Eq, Clone)]
155pub struct ArcOutput<'x> {
156    result: DkimResult,
157    set: Vec<Set<'x>>,
158}
159
160#[derive(Debug, PartialEq, Eq, Clone, Copy)]
161pub enum SpfResult {
162    Pass,
163    Fail,
164    SoftFail,
165    Neutral,
166    TempError,
167    PermError,
168    None,
169}
170
171#[derive(Debug, PartialEq, Eq, Clone)]
172pub struct SpfOutput {
173    result: SpfResult,
174    domain: String,
175    report: Option<String>,
176    explanation: Option<String>,
177}
178
179#[derive(Debug, PartialEq, Eq, Clone)]
180pub struct DmarcOutput {
181    spf_result: DmarcResult,
182    dkim_result: DmarcResult,
183    domain: String,
184    policy: dmarc::Policy,
185    record: Option<Arc<Dmarc>>,
186}
187
188#[derive(Debug, PartialEq, Eq, Clone)]
189pub enum DmarcResult {
190    Pass,
191    Fail(crate::Error),
192    TempError(crate::Error),
193    PermError(crate::Error),
194    None,
195}
196
197#[derive(Debug, PartialEq, Eq, Clone)]
198pub struct IprevOutput {
199    pub result: IprevResult,
200    pub ptr: Option<Arc<Vec<String>>>,
201}
202
203#[derive(Debug, PartialEq, Eq, Clone)]
204pub enum IprevResult {
205    Pass,
206    Fail(crate::Error),
207    TempError(crate::Error),
208    PermError(crate::Error),
209    None,
210}
211
212#[derive(Debug, Hash, PartialEq, Eq, Clone)]
213pub enum Version {
214    V1,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub enum Error {
219    ParseError,
220    MissingParameters,
221    NoHeadersFound,
222    CryptoError(String),
223    Io(String),
224    Base64,
225    UnsupportedVersion,
226    UnsupportedAlgorithm,
227    UnsupportedCanonicalization,
228    UnsupportedKeyType,
229    FailedBodyHashMatch,
230    FailedVerification,
231    FailedAuidMatch,
232    RevokedPublicKey,
233    IncompatibleAlgorithms,
234    SignatureExpired,
235    SignatureLength,
236    DnsError(String),
237    DnsRecordNotFound(ResponseCode),
238    ArcChainTooLong,
239    ArcInvalidInstance(u32),
240    ArcInvalidCV,
241    ArcHasHeaderTag,
242    ArcBrokenChain,
243    NotAligned,
244    InvalidRecordType,
245}
246
247pub type Result<T> = std::result::Result<T, Error>;
248
249impl std::error::Error for Error {}
250
251impl Display for Error {
252    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253        match self {
254            Error::ParseError => write!(f, "Parse error"),
255            Error::MissingParameters => write!(f, "Missing parameters"),
256            Error::NoHeadersFound => write!(f, "No headers found"),
257            Error::CryptoError(err) => write!(f, "Cryptography layer error: {err}"),
258            Error::Io(e) => write!(f, "I/O error: {e}"),
259            Error::Base64 => write!(f, "Base64 encode or decode error."),
260            Error::UnsupportedVersion => write!(f, "Unsupported version in DKIM Signature"),
261            Error::UnsupportedAlgorithm => write!(f, "Unsupported algorithm in DKIM Signature"),
262            Error::UnsupportedCanonicalization => {
263                write!(f, "Unsupported canonicalization method in DKIM Signature")
264            }
265            Error::UnsupportedKeyType => {
266                write!(f, "Unsupported key type in DKIM DNS record")
267            }
268            Error::FailedBodyHashMatch => {
269                write!(f, "Calculated body hash does not match signature hash")
270            }
271            Error::RevokedPublicKey => write!(f, "Public key for this signature has been revoked"),
272            Error::IncompatibleAlgorithms => write!(
273                f,
274                "Incompatible algorithms used in signature and DKIM DNS record"
275            ),
276            Error::FailedVerification => write!(f, "Signature verification failed"),
277            Error::SignatureExpired => write!(f, "Signature expired"),
278            Error::SignatureLength => write!(f, "Insecure 'l=' tag found in Signature"),
279            Error::FailedAuidMatch => write!(f, "AUID does not match domain name"),
280            Error::ArcInvalidInstance(i) => {
281                write!(f, "Invalid 'i={i}' value found in ARC header")
282            }
283            Error::ArcInvalidCV => write!(f, "Invalid 'cv=' value found in ARC header"),
284            Error::ArcHasHeaderTag => write!(f, "Invalid 'h=' tag present in ARC-Seal"),
285            Error::ArcBrokenChain => write!(f, "Broken or missing ARC chain"),
286            Error::ArcChainTooLong => write!(f, "Too many ARC headers"),
287            Error::InvalidRecordType => write!(f, "Invalid record"),
288            Error::DnsError(err) => write!(f, "DNS resolution error: {err}"),
289            Error::DnsRecordNotFound(code) => write!(f, "DNS record not found: {code}"),
290            Error::NotAligned => write!(f, "Policy not aligned"),
291        }
292    }
293}
294
295impl Display for SpfResult {
296    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297        f.write_str(match self {
298            SpfResult::Pass => "Pass",
299            SpfResult::Fail => "Fail",
300            SpfResult::SoftFail => "SoftFail",
301            SpfResult::Neutral => "Neutral",
302            SpfResult::TempError => "TempError",
303            SpfResult::PermError => "PermError",
304            SpfResult::None => "None",
305        })
306    }
307}
308
309impl Display for IprevResult {
310    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311        match self {
312            IprevResult::Pass => f.write_str("pass"),
313            IprevResult::Fail(err) => write!(f, "fail; {err}"),
314            IprevResult::TempError(err) => write!(f, "temp error; {err}"),
315            IprevResult::PermError(err) => write!(f, "perm error; {err}"),
316            IprevResult::None => f.write_str("none"),
317        }
318    }
319}
320
321impl Display for DkimResult {
322    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323        match self {
324            DkimResult::Pass => f.write_str("pass"),
325            DkimResult::Fail(err) => write!(f, "fail; {err}"),
326            DkimResult::Neutral(err) => write!(f, "neutral; {err}"),
327            DkimResult::TempError(err) => write!(f, "temp error; {err}"),
328            DkimResult::PermError(err) => write!(f, "perm error; {err}"),
329            DkimResult::None => f.write_str("none"),
330        }
331    }
332}
333
334impl Display for DmarcResult {
335    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336        match self {
337            DmarcResult::Pass => f.write_str("pass"),
338            DmarcResult::Fail(err) => write!(f, "fail; {err}"),
339            DmarcResult::TempError(err) => write!(f, "temp error; {err}"),
340            DmarcResult::PermError(err) => write!(f, "perm error; {err}"),
341            DmarcResult::None => f.write_str("none"),
342        }
343    }
344}
345
346impl From<io::Error> for Error {
347    fn from(err: io::Error) -> Self {
348        Error::Io(err.to_string())
349    }
350}
351
352#[cfg(feature = "rsa")]
353impl From<rsa::errors::Error> for Error {
354    fn from(err: rsa::errors::Error) -> Self {
355        Error::CryptoError(err.to_string())
356    }
357}
358
359#[cfg(feature = "ed25519-dalek")]
360impl From<ed25519_dalek::ed25519::Error> for Error {
361    fn from(err: ed25519_dalek::ed25519::Error) -> Self {
362        Error::CryptoError(err.to_string())
363    }
364}
365
366impl Default for SpfOutput {
367    fn default() -> Self {
368        Self {
369            result: SpfResult::None,
370            domain: Default::default(),
371            report: Default::default(),
372            explanation: Default::default(),
373        }
374    }
375}
376
377thread_local!(static COUNTER: Cell<u64>  = const { Cell::new(0) });
378
379/// Generates a random value between 0 and 100.
380/// Returns true if the generated value is within the requested
381/// sampling percentage specified in a SPF, DKIM or DMARC policy.
382pub(crate) fn is_within_pct(pct: u8) -> bool {
383    pct == 100
384        || COUNTER.with(|c| {
385            SystemTime::now()
386                .duration_since(SystemTime::UNIX_EPOCH)
387                .map(|d| d.as_secs())
388                .unwrap_or(0)
389                .wrapping_add(c.replace(c.get() + 1))
390                .wrapping_mul(11400714819323198485u64)
391        }) % 100
392            < pct as u64
393}