Skip to main content

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    current_time_on_demand: bool,
112}
113
114#[derive(Debug, PartialEq, Eq, Clone)]
115pub enum Macro {
116    Literal(Box<[u8]>),
117    Variable {
118        letter: Variable,
119        num_parts: u32,
120        reverse: bool,
121        escape: bool,
122        delimiters: u64,
123    },
124    List(Box<[Macro]>),
125    None,
126}
127
128#[derive(Debug, PartialEq, Eq, Clone)]
129pub struct Spf {
130    pub version: Version,
131    pub directives: Box<[Directive]>,
132    pub exp: Option<Macro>,
133    pub redirect: Option<Macro>,
134    pub ra: Option<Box<[u8]>>,
135    pub rp: u8,
136    pub rr: u8,
137}
138
139pub(crate) const RR_TEMP_PERM_ERROR: u8 = 0x01;
140pub(crate) const RR_FAIL: u8 = 0x02;
141pub(crate) const RR_SOFTFAIL: u8 = 0x04;
142pub(crate) const RR_NEUTRAL_NONE: u8 = 0x08;
143
144impl Directive {
145    pub fn new(qualifier: Qualifier, mechanism: Mechanism) -> Self {
146        Directive {
147            qualifier,
148            mechanism,
149        }
150    }
151}
152
153impl Mechanism {
154    pub fn needs_ptr(&self) -> bool {
155        match self {
156            Mechanism::All
157            | Mechanism::Ip4 { .. }
158            | Mechanism::Ip6 { .. }
159            | Mechanism::Ptr { .. } => false,
160            Mechanism::Include { macro_string } => macro_string.needs_ptr(),
161            Mechanism::A { macro_string, .. } => macro_string.needs_ptr(),
162            Mechanism::Mx { macro_string, .. } => macro_string.needs_ptr(),
163            Mechanism::Exists { macro_string } => macro_string.needs_ptr(),
164        }
165    }
166}
167
168impl TryFrom<&str> for SpfResult {
169    type Error = ();
170
171    fn try_from(value: &str) -> Result<Self, Self::Error> {
172        if value.eq_ignore_ascii_case("pass") {
173            Ok(SpfResult::Pass)
174        } else if value.eq_ignore_ascii_case("fail") {
175            Ok(SpfResult::Fail)
176        } else if value.eq_ignore_ascii_case("softfail") {
177            Ok(SpfResult::SoftFail)
178        } else if value.eq_ignore_ascii_case("neutral") {
179            Ok(SpfResult::Neutral)
180        } else if value.eq_ignore_ascii_case("temperror") {
181            Ok(SpfResult::TempError)
182        } else if value.eq_ignore_ascii_case("permerror") {
183            Ok(SpfResult::PermError)
184        } else if value.eq_ignore_ascii_case("none") {
185            Ok(SpfResult::None)
186        } else {
187            Err(())
188        }
189    }
190}
191
192impl TryFrom<String> for SpfResult {
193    type Error = ();
194
195    fn try_from(value: String) -> Result<Self, Self::Error> {
196        TryFrom::try_from(value.as_str())
197    }
198}
199
200impl SpfOutput {
201    pub fn new(domain: String) -> Self {
202        SpfOutput {
203            result: SpfResult::None,
204            report: None,
205            explanation: None,
206            domain,
207        }
208    }
209
210    pub fn with_result(mut self, result: SpfResult) -> Self {
211        self.result = result;
212        self
213    }
214
215    pub fn with_report(mut self, spf: &Spf) -> Self {
216        match &spf.ra {
217            Some(ra)
218                if is_within_pct(spf.rp)
219                    && 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            {
229                let ra = String::from_utf8_lossy(ra);
230                let mut report = String::with_capacity(ra.len() + self.domain.len() + 1);
231                report.push_str(ra.as_ref());
232                report.push('@');
233                report.push_str(&self.domain);
234                self.report = report.into();
235            }
236            _ => (),
237        }
238        self
239    }
240
241    pub fn with_explanation(mut self, explanation: String) -> Self {
242        self.explanation = explanation.into();
243        self
244    }
245
246    pub fn result(&self) -> SpfResult {
247        self.result
248    }
249
250    pub fn domain(&self) -> &str {
251        &self.domain
252    }
253
254    pub fn explanation(&self) -> Option<&str> {
255        self.explanation.as_deref()
256    }
257
258    pub fn report_address(&self) -> Option<&str> {
259        self.report.as_deref()
260    }
261}