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