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 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}