mail_auth/spf/
mod.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7pub mod macros;
8pub mod parse;
9pub mod verify;
10
11use std::{
12    borrow::Cow,
13    net::{Ipv4Addr, Ipv6Addr},
14};
15
16use crate::{is_within_pct, SpfOutput, SpfResult, Version};
17
18/*
19      "+" pass
20      "-" fail
21      "~" softfail
22      "?" neutral
23*/
24
25#[derive(Debug, PartialEq, Eq, Clone)]
26pub enum Qualifier {
27    Pass,
28    Fail,
29    SoftFail,
30    Neutral,
31}
32
33/*
34   mechanism        = ( all / include
35                      / a / mx / ptr / ip4 / ip6 / exists )
36*/
37#[derive(Debug, PartialEq, Eq, Clone)]
38pub enum Mechanism {
39    All,
40    Include {
41        macro_string: Macro,
42    },
43    A {
44        macro_string: Macro,
45        ip4_mask: u32,
46        ip6_mask: u128,
47    },
48    Mx {
49        macro_string: Macro,
50        ip4_mask: u32,
51        ip6_mask: u128,
52    },
53    Ptr {
54        macro_string: Macro,
55    },
56    Ip4 {
57        addr: Ipv4Addr,
58        mask: u32,
59    },
60    Ip6 {
61        addr: Ipv6Addr,
62        mask: u128,
63    },
64    Exists {
65        macro_string: Macro,
66    },
67}
68
69/*
70    directive        = [ qualifier ] mechanism
71*/
72#[derive(Debug, PartialEq, Eq, Clone)]
73pub struct Directive {
74    pub qualifier: Qualifier,
75    pub mechanism: Mechanism,
76}
77
78/*
79      s = <sender>
80      l = local-part of <sender>
81      o = domain of <sender>
82      d = <domain>
83      i = <ip>
84      p = the validated domain name of <ip> (do not use)
85      v = the string "in-addr" if <ip> is ipv4, or "ip6" if <ip> is ipv6
86      h = HELO/EHLO domain
87   The following macro letters are allowed only in "exp" text:
88
89      c = SMTP client IP (easily readable format)
90      r = domain name of host performing the check
91      t = current timestamp
92*/
93
94#[derive(Debug, PartialEq, Eq, Clone, Copy)]
95#[repr(u8)]
96pub enum Variable {
97    Sender = 0,
98    SenderLocalPart = 1,
99    SenderDomainPart = 2,
100    Domain = 3,
101    Ip = 4,
102    ValidatedDomain = 5,
103    IpVersion = 6,
104    HeloDomain = 7,
105    SmtpIp = 8,
106    HostDomain = 9,
107    CurrentTime = 10,
108}
109
110#[derive(Debug, PartialEq, Eq, Clone, Default)]
111pub struct Variables<'x> {
112    vars: [Cow<'x, [u8]>; 11],
113}
114
115#[derive(Debug, PartialEq, Eq, Clone)]
116pub enum Macro {
117    Literal(Vec<u8>),
118    Variable {
119        letter: Variable,
120        num_parts: u32,
121        reverse: bool,
122        escape: bool,
123        delimiters: u64,
124    },
125    List(Vec<Macro>),
126    None,
127}
128
129#[derive(Debug, PartialEq, Eq, Clone)]
130pub struct Spf {
131    pub version: Version,
132    pub directives: Vec<Directive>,
133    pub exp: Option<Macro>,
134    pub redirect: Option<Macro>,
135    pub ra: Option<Vec<u8>>,
136    pub rp: u8,
137    pub rr: u8,
138}
139
140pub(crate) const RR_TEMP_PERM_ERROR: u8 = 0x01;
141pub(crate) const RR_FAIL: u8 = 0x02;
142pub(crate) const RR_SOFTFAIL: u8 = 0x04;
143pub(crate) const RR_NEUTRAL_NONE: u8 = 0x08;
144
145impl Directive {
146    pub fn new(qualifier: Qualifier, mechanism: Mechanism) -> Self {
147        Directive {
148            qualifier,
149            mechanism,
150        }
151    }
152}
153
154impl Mechanism {
155    pub fn needs_ptr(&self) -> bool {
156        match self {
157            Mechanism::All
158            | Mechanism::Ip4 { .. }
159            | Mechanism::Ip6 { .. }
160            | Mechanism::Ptr { .. } => false,
161            Mechanism::Include { macro_string } => macro_string.needs_ptr(),
162            Mechanism::A { macro_string, .. } => macro_string.needs_ptr(),
163            Mechanism::Mx { macro_string, .. } => macro_string.needs_ptr(),
164            Mechanism::Exists { macro_string } => macro_string.needs_ptr(),
165        }
166    }
167}
168
169impl TryFrom<&str> for SpfResult {
170    type Error = ();
171
172    fn try_from(value: &str) -> Result<Self, Self::Error> {
173        if value.eq_ignore_ascii_case("pass") {
174            Ok(SpfResult::Pass)
175        } else if value.eq_ignore_ascii_case("fail") {
176            Ok(SpfResult::Fail)
177        } else if value.eq_ignore_ascii_case("softfail") {
178            Ok(SpfResult::SoftFail)
179        } else if value.eq_ignore_ascii_case("neutral") {
180            Ok(SpfResult::Neutral)
181        } else if value.eq_ignore_ascii_case("temperror") {
182            Ok(SpfResult::TempError)
183        } else if value.eq_ignore_ascii_case("permerror") {
184            Ok(SpfResult::PermError)
185        } else if value.eq_ignore_ascii_case("none") {
186            Ok(SpfResult::None)
187        } else {
188            Err(())
189        }
190    }
191}
192
193impl TryFrom<String> for SpfResult {
194    type Error = ();
195
196    fn try_from(value: String) -> Result<Self, Self::Error> {
197        TryFrom::try_from(value.as_str())
198    }
199}
200
201impl SpfOutput {
202    pub fn new(domain: String) -> Self {
203        SpfOutput {
204            result: SpfResult::None,
205            report: None,
206            explanation: None,
207            domain,
208        }
209    }
210
211    pub fn with_result(mut self, result: SpfResult) -> Self {
212        self.result = result;
213        self
214    }
215
216    pub fn with_report(mut self, spf: &Spf) -> Self {
217        match &spf.ra {
218            Some(ra) if is_within_pct(spf.rp) => {
219                if match self.result {
220                    SpfResult::Fail => (spf.rr & RR_FAIL) != 0,
221                    SpfResult::SoftFail => (spf.rr & RR_SOFTFAIL) != 0,
222                    SpfResult::Neutral | SpfResult::None => (spf.rr & RR_NEUTRAL_NONE) != 0,
223                    SpfResult::TempError | SpfResult::PermError => {
224                        (spf.rr & RR_TEMP_PERM_ERROR) != 0
225                    }
226                    SpfResult::Pass => false,
227                } {
228                    self.report = format!("{}@{}", String::from_utf8_lossy(ra), self.domain).into();
229                }
230            }
231            _ => (),
232        }
233        self
234    }
235
236    pub fn with_explanation(mut self, explanation: String) -> Self {
237        self.explanation = explanation.into();
238        self
239    }
240
241    pub fn result(&self) -> SpfResult {
242        self.result
243    }
244
245    pub fn domain(&self) -> &str {
246        &self.domain
247    }
248
249    pub fn explanation(&self) -> Option<&str> {
250        self.explanation.as_deref()
251    }
252
253    pub fn report_address(&self) -> Option<&str> {
254        self.report.as_deref()
255    }
256}