1pub 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#[derive(Debug, PartialEq, Eq, Clone)]
26pub enum Qualifier {
27 Pass,
28 Fail,
29 SoftFail,
30 Neutral,
31}
32
33#[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#[derive(Debug, PartialEq, Eq, Clone)]
73pub struct Directive {
74 pub qualifier: Qualifier,
75 pub mechanism: Mechanism,
76}
77
78#[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}