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