1use crate::spec::MacroSpec;
2use crate::{fully_qualify, SpfContext, SpfDisposition, SpfResult};
3use dns_resolver::Resolver;
4use std::fmt;
5use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
6
7#[derive(Debug, Default)]
8pub(crate) struct Record {
9 directives: Vec<Directive>,
10 redirect: Option<MacroSpec>,
11 explanation: Option<MacroSpec>,
12}
13
14impl Record {
15 pub(crate) fn parse(s: &str) -> Result<Self, String> {
16 let mut tokens = s.split_whitespace();
17 let version = tokens
18 .next()
19 .ok_or_else(|| format!("expected version in {s}"))?;
20 if version != "v=spf1" {
21 return Err(format!("expected SPF version 1 in {s}"));
22 }
23
24 let mut new = Self::default();
25 for t in tokens {
26 if t.is_empty() {
27 return Err("invalid empty token".to_string());
28 }
29
30 if let Ok(directive) = Directive::parse(t) {
31 if new.redirect.is_some() || new.explanation.is_some() {
32 return Err("directive after modifier".to_owned());
33 }
34
35 new.directives.push(directive);
36 continue;
37 }
38
39 if let Ok(modifier) = Modifier::parse(t) {
40 match modifier {
41 Modifier::Redirect(domain) => match new.redirect {
42 Some(_) => return Err("duplicate redirect modifier".to_owned()),
43 None => new.redirect = Some(domain),
44 },
45 Modifier::Explanation(domain) => match new.explanation {
46 Some(_) => return Err("duplicate explanation modifier".to_owned()),
47 None => new.explanation = Some(domain),
48 },
49 _ => {} }
51 continue;
52 }
53
54 return Err(format!("invalid token '{t}'"));
55 }
56
57 Ok(new)
58 }
59
60 pub(crate) async fn evaluate(&self, cx: &SpfContext<'_>, resolver: &dyn Resolver) -> SpfResult {
61 let mut failed = None;
62 for directive in &self.directives {
63 match directive.evaluate(cx, resolver).await {
64 Ok(Some(SpfResult {
65 disposition: SpfDisposition::Fail,
66 context,
67 })) => {
68 failed = Some(context);
69 break;
70 }
71 Ok(Some(result)) => return result,
72 Ok(None) => continue,
73 Err(err) => return err,
74 }
75 }
76
77 if let Some(domain) = &self.redirect {
78 let domain = match cx.domain(Some(domain), resolver).await {
79 Ok(domain) => domain,
80 Err(err) => return err,
81 };
82
83 let nested = cx.with_domain(&domain);
84 match Box::pin(nested.check(resolver, false)).await {
85 SpfResult {
86 disposition: SpfDisposition::Fail,
87 context,
88 } => failed = Some(context),
89 result => return result,
90 }
91 }
92
93 let failed = match failed {
94 Some(failed) => failed,
95 None => {
96 return SpfResult {
97 disposition: SpfDisposition::Neutral,
98 context: "default result".to_owned(),
99 }
100 }
101 };
102
103 let domain = match &self.explanation {
104 Some(domain) => match cx.domain(Some(domain), resolver).await {
105 Ok(domain) => domain,
106 Err(err) => return err,
107 },
108 None => return SpfResult::fail(failed),
109 };
110
111 let explanation = match resolver.resolve_txt(&fully_qualify(&domain)).await {
116 Ok(answers) if answers.records.len() == 1 => answers.as_txt().pop().unwrap(),
117 Ok(_) | Err(_) => return SpfResult::fail(failed),
118 };
119
120 let spec = match MacroSpec::parse(&explanation) {
121 Ok(spec) => spec,
122 Err(_) => return SpfResult::fail(failed),
123 };
124
125 match spec.expand(cx, resolver).await {
126 Ok(explanation) => SpfResult::fail(explanation),
127 Err(_) => SpfResult::fail(failed),
128 }
129 }
130}
131
132#[derive(Debug)]
133struct Directive {
134 pub qualifier: Qualifier,
135 pub mechanism: Mechanism,
136}
137
138impl Directive {
139 fn parse(s: &str) -> Result<Self, String> {
140 let mut qualifier = Qualifier::default();
141 let s = match s.get(0..1).and_then(Qualifier::parse) {
142 Some(q) => {
143 qualifier = q;
144 &s[1..]
145 }
146 None => s,
147 };
148
149 Ok(Self {
150 qualifier,
151 mechanism: Mechanism::parse(s)?,
152 })
153 }
154
155 async fn evaluate(
156 &self,
157 cx: &SpfContext<'_>,
158 resolver: &dyn Resolver,
159 ) -> Result<Option<SpfResult>, SpfResult> {
160 let matched = match &self.mechanism {
161 Mechanism::All => true,
162 Mechanism::A { domain, cidr_len } => {
163 cx.check_lookup_limit()?;
164 let domain = cx.domain(domain.as_ref(), resolver).await?;
165 let resolved = match resolver.resolve_ip(&fully_qualify(&domain)).await {
166 Ok(ips) => ips,
167 Err(err) => {
168 return Err(SpfResult {
169 disposition: SpfDisposition::TempError,
170 context: format!("error looking up IP for {domain}: {err}"),
171 })
172 }
173 };
174
175 resolved
176 .iter()
177 .any(|&resolved_ip| cidr_len.matches(cx.client_ip, resolved_ip))
178 }
179 Mechanism::Mx { domain, cidr_len } => {
180 cx.check_lookup_limit()?;
181 let domain = cx.domain(domain.as_ref(), resolver).await?;
182 let exchanges = match resolver.resolve_mx(&fully_qualify(&domain)).await {
183 Ok(exchanges) => exchanges,
184 Err(err) => {
185 return Err(SpfResult {
186 disposition: SpfDisposition::TempError,
187 context: format!("error looking up IP for {domain}: {err}"),
188 })
189 }
190 };
191
192 let mut matched = false;
193 for (idx, exchange) in exchanges.into_iter().enumerate() {
194 if idx >= 10 {
195 return Err(SpfResult {
197 disposition: SpfDisposition::PermError,
198 context: format!("too many MX records for {domain}"),
199 });
200 }
201
202 let resolved = match resolver
203 .resolve_ip(&fully_qualify(&exchange.to_string()))
204 .await
205 {
206 Ok(ips) => ips,
207 Err(err) => {
208 return Err(SpfResult {
209 disposition: SpfDisposition::TempError,
210 context: format!("error looking up IP for {exchange}: {err}"),
211 })
212 }
213 };
214
215 if resolved
216 .iter()
217 .any(|&resolved_ip| cidr_len.matches(cx.client_ip, resolved_ip))
218 {
219 matched = true;
220 break;
221 }
222 }
223
224 matched
225 }
226 Mechanism::Ip4 {
227 ip4_network,
228 cidr_len,
229 } => DualCidrLength {
230 v4: *cidr_len,
231 ..Default::default()
232 }
233 .matches(cx.client_ip, IpAddr::V4(*ip4_network)),
234 Mechanism::Ip6 {
235 ip6_network,
236 cidr_len,
237 } => DualCidrLength {
238 v6: *cidr_len,
239 ..Default::default()
240 }
241 .matches(cx.client_ip, IpAddr::V6(*ip6_network)),
242 Mechanism::Ptr { domain } => {
243 let validated_domain = cx.validated_domain(domain.as_ref(), resolver).await?;
245
246 validated_domain.is_some()
247 }
248 Mechanism::Include { domain } => {
249 let domain = cx.domain(Some(domain), resolver).await?;
250 let nested = cx.with_domain(&domain);
251 use SpfDisposition::*;
252 match Box::pin(nested.check(resolver, false)).await {
253 SpfResult {
254 disposition: Pass, ..
255 } => true,
256 SpfResult {
257 disposition: Fail | SoftFail | Neutral,
258 ..
259 } => false,
260 SpfResult {
261 disposition: TempError,
262 context,
263 } => {
264 return Err(SpfResult {
265 disposition: TempError,
266 context: format!(
267 "temperror while evaluating include:{domain}: {context}"
268 ),
269 })
270 }
271 SpfResult {
272 disposition: disp @ PermError | disp @ None,
273 context,
274 } => {
275 return Err(SpfResult {
276 disposition: PermError,
277 context: format!("{disp} while evaluating include:{domain}: {context}"),
278 })
279 }
280 }
281 }
282 Mechanism::Exists { domain } => {
283 cx.check_lookup_limit()?;
284 let domain = cx.domain(Some(domain), resolver).await?;
285 match resolver.resolve_ip(&fully_qualify(&domain)).await {
286 Ok(ips) => ips.iter().any(|ip| ip.is_ipv4()),
287 Err(err) => {
288 return Err(SpfResult {
289 disposition: SpfDisposition::TempError,
290 context: format!("error looking up IP for {domain}: {err}"),
291 })
292 }
293 }
294 }
295 };
296
297 Ok(if matched {
298 Some(SpfResult {
299 disposition: SpfDisposition::from(self.qualifier),
300 context: format!("matched '{self}' directive"),
301 })
302 } else {
303 None
304 })
305 }
306}
307
308impl fmt::Display for Directive {
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 if self.qualifier != Qualifier::Pass {
311 write!(f, "{}", self.qualifier.as_str())?;
312 }
313 write!(f, "{}", self.mechanism)
314 }
315}
316
317#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
318pub(crate) enum Qualifier {
319 #[default]
321 Pass,
322 Fail,
324 SoftFail,
326 Neutral,
328}
329
330impl Qualifier {
331 fn parse(s: &str) -> Option<Self> {
332 Some(match s {
333 "+" => Self::Pass,
334 "-" => Self::Fail,
335 "~" => Self::SoftFail,
336 "?" => Self::Neutral,
337 _ => return None,
338 })
339 }
340
341 fn as_str(&self) -> &'static str {
342 match self {
343 Self::Pass => "+",
344 Self::Fail => "-",
345 Self::SoftFail => "~",
346 Self::Neutral => "?",
347 }
348 }
349}
350
351#[derive(Debug)]
352struct DualCidrLength {
353 pub v4: u8,
354 pub v6: u8,
355}
356
357impl DualCidrLength {
358 fn matches(&self, observed: IpAddr, specified: IpAddr) -> bool {
361 match (observed, specified, self) {
362 (IpAddr::V4(observed), IpAddr::V4(specified), DualCidrLength { v4, .. }) => {
363 let mask = ipv4_prefix_mask(*v4);
364 let specified_masked = Ipv4Addr::from_bits(specified.to_bits() & mask);
365 let observed_masked = Ipv4Addr::from(observed.to_bits() & mask);
366 specified_masked == observed_masked
367 }
368 (IpAddr::V6(observed), IpAddr::V6(specified), DualCidrLength { v6, .. }) => {
369 let mask = ipv6_prefix_mask(*v6);
370 let specified_masked = Ipv6Addr::from_bits(specified.to_bits() & mask);
371 let observed_masked = Ipv6Addr::from(observed.to_bits() & mask);
372 specified_masked == observed_masked
373 }
374 _ => false,
375 }
376 }
377}
378
379fn ipv4_prefix_mask(prefix: u8) -> u32 {
382 u32::MAX
383 .checked_shl((32 - prefix.min(32)) as u32)
384 .unwrap_or(0)
385}
386
387fn ipv6_prefix_mask(prefix: u8) -> u128 {
389 u128::MAX
390 .checked_shl((128 - prefix.min(128)) as u32)
391 .unwrap_or(0)
392}
393
394impl Default for DualCidrLength {
395 fn default() -> Self {
396 Self { v4: 32, v6: 128 }
397 }
398}
399
400fn parse_cidr_length(s: &str, max: u8) -> Result<u8, String> {
404 let len: u8 = s.parse().map_err(|err| format!("{err}"))?;
405 if len > max {
406 return Err(format!("CIDR length /{len} exceeds maximum of /{max}"));
407 }
408 Ok(len)
409}
410
411impl DualCidrLength {
412 fn parse_from_end(s: &str) -> Result<(&str, Self), String> {
413 match s.rsplit_once('/') {
414 Some((left, right)) => {
415 if left.ends_with('/') {
416 if let Some((prefix, v4cidr)) = left[0..left.len() - 1].rsplit_once('/') {
418 let v4 = parse_cidr_length(v4cidr, 32).map_err(|err| {
419 format!(
420 "invalid dual-cidr-length in {s}: parsing v4 cidr portion: {err}"
421 )
422 })?;
423 let v6 = parse_cidr_length(right, 128)
424 .map_err(|err| format!("invalid dual-cidr-length in {s}: {err}"))?;
425 return Ok((prefix, Self { v4, v6 }));
426 }
427 }
428 let v4 = parse_cidr_length(right, 32)
429 .map_err(|err| format!("invalid dual-cidr-length in {s}: {err}"))?;
430 Ok((
431 left,
432 Self {
433 v4,
434 ..Self::default()
435 },
436 ))
437 }
438 None => Ok((s, Self::default())),
439 }
440 }
441}
442
443impl fmt::Display for DualCidrLength {
444 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445 if self.v4 == 32 && self.v6 == 128 {
446 return Ok(());
447 }
448
449 write!(f, "/{}", self.v4)?;
450 if self.v6 != 128 {
451 write!(f, "/{}", self.v6)?;
452 }
453
454 Ok(())
455 }
456}
457
458#[derive(Debug)]
459enum Mechanism {
460 All,
461 Include {
462 domain: MacroSpec,
463 },
464 A {
465 domain: Option<MacroSpec>,
466 cidr_len: DualCidrLength,
467 },
468 Mx {
469 domain: Option<MacroSpec>,
470 cidr_len: DualCidrLength,
471 },
472 Ptr {
473 domain: Option<MacroSpec>,
474 },
475 Ip4 {
476 ip4_network: Ipv4Addr,
477 cidr_len: u8,
478 },
479 Ip6 {
480 ip6_network: Ipv6Addr,
481 cidr_len: u8,
482 },
483 Exists {
484 domain: MacroSpec,
485 },
486}
487
488impl fmt::Display for Mechanism {
489 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490 match self {
491 Self::All => write!(f, "all"),
492 Self::Include { domain } => write!(f, "include:{}", domain),
493 Self::A { domain, cidr_len } => {
494 write!(f, "a")?;
495 if let Some(domain) = domain {
496 write!(f, ":{}", domain)?;
497 }
498 write!(f, "{}", cidr_len)
499 }
500 Self::Mx { domain, cidr_len } => {
501 write!(f, "mx")?;
502 if let Some(domain) = domain {
503 write!(f, ":{}", domain)?;
504 }
505 write!(f, "{}", cidr_len)
506 }
507 Self::Ptr { domain } => {
508 write!(f, "ptr")?;
509 if let Some(domain) = domain {
510 write!(f, ":{}", domain)?;
511 }
512 Ok(())
513 }
514 Self::Ip4 {
515 ip4_network,
516 cidr_len,
517 } => write!(f, "ip4:{}/{}", ip4_network, cidr_len),
518 Self::Ip6 {
519 ip6_network,
520 cidr_len,
521 } => write!(f, "ip6:{}/{}", ip6_network, cidr_len),
522 Self::Exists { domain } => write!(f, "exists:{}", domain),
523 }
524 }
525}
526
527fn starts_with_ident<'a>(s: &'a str, ident: &str) -> Option<&'a str> {
528 if s.len() < ident.len() {
529 return None;
530 }
531
532 if s.get(0..ident.len())?.eq_ignore_ascii_case(ident) {
533 Some(&s[ident.len()..])
534 } else {
535 None
536 }
537}
538
539impl Mechanism {
540 fn parse(s: &str) -> Result<Self, String> {
541 if s.eq_ignore_ascii_case("all") {
542 return Ok(Self::All);
543 }
544
545 if let Some(spec) = starts_with_ident(s, "include:") {
546 return Ok(Self::Include {
547 domain: MacroSpec::parse(spec)?,
548 });
549 }
550
551 if let Some(remain) = starts_with_ident(s, "a") {
552 let (remain, cidr_len) = DualCidrLength::parse_from_end(remain)?;
553
554 let domain = if let Some(spec) = remain.strip_prefix(":") {
555 Some(MacroSpec::parse(spec)?)
556 } else if remain.is_empty() {
557 None
558 } else {
559 return Err(format!("invalid 'a' mechanism: {s}"));
560 };
561
562 return Ok(Self::A { domain, cidr_len });
563 }
564 if let Some(remain) = starts_with_ident(s, "mx") {
565 let (remain, cidr_len) = DualCidrLength::parse_from_end(remain)?;
566
567 let domain = if let Some(spec) = remain.strip_prefix(":") {
568 Some(MacroSpec::parse(spec)?)
569 } else if remain.is_empty() {
570 None
571 } else {
572 return Err(format!("invalid 'mx' mechanism: {s}"));
573 };
574
575 return Ok(Self::Mx { domain, cidr_len });
576 }
577 if let Some(remain) = starts_with_ident(s, "ptr") {
578 let domain = if let Some(spec) = remain.strip_prefix(":") {
579 Some(MacroSpec::parse(spec)?)
580 } else if remain.is_empty() {
581 None
582 } else {
583 return Err(format!("invalid 'ptr' mechanism: {s}"));
584 };
585
586 return Ok(Self::Ptr { domain });
587 }
588 if let Some(remain) = starts_with_ident(s, "ip4:") {
589 let (addr, len) = if let Some((addr, len)) = remain.split_once('/') {
590 (addr, len)
591 } else {
592 (remain, "32")
594 };
595 let ip4_network = addr
596 .parse()
597 .map_err(|err| format!("invalid 'ip4' mechanism: {s}: {err}"))?;
598 let cidr_len = parse_cidr_length(len, 32)
599 .map_err(|err| format!("invalid 'ip4' mechanism: {s}: {err}"))?;
600
601 return Ok(Self::Ip4 {
602 ip4_network,
603 cidr_len,
604 });
605 }
606 if let Some(remain) = starts_with_ident(s, "ip6:") {
607 let (addr, len) = if let Some((addr, len)) = remain.split_once('/') {
608 (addr, len)
609 } else {
610 (remain, "128")
612 };
613 let ip6_network = addr
614 .parse()
615 .map_err(|err| format!("invalid 'ip6' mechanism: {s}: {err}"))?;
616 let cidr_len = parse_cidr_length(len, 128)
617 .map_err(|err| format!("invalid 'ip6' mechanism: {s}: {err}"))?;
618
619 return Ok(Self::Ip6 {
620 ip6_network,
621 cidr_len,
622 });
623 }
624 if let Some(spec) = starts_with_ident(s, "exists:") {
625 return Ok(Self::Exists {
626 domain: MacroSpec::parse(spec)?,
627 });
628 }
629
630 Err(format!("invalid mechanism {s}"))
631 }
632}
633
634#[derive(Debug)]
635enum Modifier {
636 Redirect(MacroSpec),
637 Explanation(MacroSpec),
638 Unknown,
639}
640
641impl Modifier {
642 fn parse(s: &str) -> Result<Self, String> {
643 if let Some(spec) = starts_with_ident(s, "redirect=") {
644 return Ok(Self::Redirect(MacroSpec::parse(spec)?));
645 }
646 if let Some(spec) = starts_with_ident(s, "exp=") {
647 return Ok(Self::Explanation(MacroSpec::parse(spec)?));
648 }
649
650 let (name, _) = s
651 .split_once('=')
652 .ok_or_else(|| format!("invalid modifier {s}"))?;
653
654 let valid = !name.is_empty()
655 && name
656 .chars()
657 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
658 && name.chars().next().unwrap().is_ascii_alphabetic();
659 if !valid {
660 return Err(format!("modifier name '{name}' is invalid"));
661 }
662
663 Ok(Self::Unknown)
664 }
665}
666
667#[cfg(test)]
668mod test {
669 use super::*;
670
671 fn parse(s: &str) -> Record {
672 eprintln!("**\n{s}");
673 match Record::parse(s) {
674 Ok(r) => r,
675 Err(err) => panic!("{err}: {s}"),
676 }
677 }
678
679 #[test]
680 fn test_cidr_zero_matches_all() {
681 let all_v4 = DualCidrLength { v4: 0, v6: 128 };
684 assert!(all_v4.matches("192.0.2.1".parse().unwrap(), "0.0.0.0".parse().unwrap()));
685 let all_v6 = DualCidrLength { v4: 32, v6: 0 };
686 assert!(all_v6.matches("2001:db8::1".parse().unwrap(), "::".parse().unwrap()));
687 }
688
689 #[test]
690 fn test_cidr_boundary_accepted() {
691 assert!(matches!(
695 Mechanism::parse("ip4:0.0.0.0/0").unwrap(),
696 Mechanism::Ip4 { cidr_len: 0, .. }
697 ));
698 assert!(matches!(
699 Mechanism::parse("ip4:192.0.2.1/32").unwrap(),
700 Mechanism::Ip4 { cidr_len: 32, .. }
701 ));
702 assert!(matches!(
703 Mechanism::parse("ip6:::/0").unwrap(),
704 Mechanism::Ip6 { cidr_len: 0, .. }
705 ));
706 assert!(matches!(
707 Mechanism::parse("ip6:2001:db8::/128").unwrap(),
708 Mechanism::Ip6 { cidr_len: 128, .. }
709 ));
710 assert!(matches!(
711 Mechanism::parse("a/0").unwrap(),
712 Mechanism::A {
713 cidr_len: DualCidrLength { v4: 0, .. },
714 ..
715 }
716 ));
717 assert!(matches!(
718 Mechanism::parse("mx/24//64").unwrap(),
719 Mechanism::Mx {
720 cidr_len: DualCidrLength { v4: 24, v6: 64 },
721 ..
722 }
723 ));
724 }
725
726 #[test]
727 fn test_cidr_out_of_range_rejected() {
728 k9::snapshot!(
729 Record::parse("v=spf1 ip4:0.0.0.0/33 -all").unwrap_err(),
730 "invalid token 'ip4:0.0.0.0/33'"
731 );
732 k9::snapshot!(
733 Record::parse("v=spf1 ip6:::/129 -all").unwrap_err(),
734 "invalid token 'ip6:::/129'"
735 );
736 k9::snapshot!(
737 Record::parse("v=spf1 a/33 -all").unwrap_err(),
738 "invalid token 'a/33'"
739 );
740 k9::snapshot!(
741 Record::parse("v=spf1 mx/0//129 -all").unwrap_err(),
742 "invalid token 'mx/0//129'"
743 );
744 }
745
746 #[test]
747 fn test_zero_width() {
748 k9::snapshot!(
749 Record::parse(
750 "v=spf1 mx a include:spf.host.example.com \u{200b}include:_spf.example.com ?all"
751 )
752 .unwrap_err(),
753 r#"invalid token '\u{200b}include:_spf.example.com'"#
754 );
755 }
756
757 #[test]
758 fn test_parse() {
759 k9::snapshot!(
760 Record::parse("v=spf1 -exists:%(ir).sbl.example.org").unwrap_err(),
761 r#"invalid token '-exists:%(ir).sbl.example.org'"#
762 );
763 k9::snapshot!(
764 Record::parse("v=spf1 -exists:%{ir.sbl.example.org").unwrap_err(),
765 r#"invalid token '-exists:%{ir.sbl.example.org'"#
766 );
767 k9::snapshot!(
768 Record::parse("v=spf1 -exists:%{ir").unwrap_err(),
769 r#"invalid token '-exists:%{ir'"#
770 );
771
772 k9::snapshot!(
775 Record::parse("v=spf1 ").unwrap(),
776 "
777Record {
778 directives: [],
779 redirect: None,
780 explanation: None,
781}
782"
783 );
784
785 k9::snapshot!(
786 parse("v=spf1 mx -all exp=explain._spf.%{d}"),
787 r#"
788Record {
789 directives: [
790 Directive {
791 qualifier: Pass,
792 mechanism: Mx {
793 domain: None,
794 cidr_len: DualCidrLength {
795 v4: 32,
796 v6: 128,
797 },
798 },
799 },
800 Directive {
801 qualifier: Fail,
802 mechanism: All,
803 },
804 ],
805 redirect: None,
806 explanation: Some(
807 MacroSpec {
808 elements: [
809 Literal(
810 "explain._spf.",
811 ),
812 Macro(
813 MacroTerm {
814 name: Domain,
815 transformer_digits: None,
816 url_escape: false,
817 reverse: false,
818 delimiters: "",
819 },
820 ),
821 ],
822 },
823 ),
824}
825"#
826 );
827
828 k9::snapshot!(
829 parse("v=spf1 -exists:%{ir}.sbl.example.org"),
830 r#"
831Record {
832 directives: [
833 Directive {
834 qualifier: Fail,
835 mechanism: Exists {
836 domain: MacroSpec {
837 elements: [
838 Macro(
839 MacroTerm {
840 name: Ip,
841 transformer_digits: None,
842 url_escape: false,
843 reverse: true,
844 delimiters: "",
845 },
846 ),
847 Literal(
848 ".sbl.example.org",
849 ),
850 ],
851 },
852 },
853 },
854 ],
855 redirect: None,
856 explanation: None,
857}
858"#
859 );
860
861 k9::snapshot!(
862 parse("v=spf1 +all"),
863 "
864Record {
865 directives: [
866 Directive {
867 qualifier: Pass,
868 mechanism: All,
869 },
870 ],
871 redirect: None,
872 explanation: None,
873}
874"
875 );
876 k9::snapshot!(
877 parse("v=spf1 a -all"),
878 "
879Record {
880 directives: [
881 Directive {
882 qualifier: Pass,
883 mechanism: A {
884 domain: None,
885 cidr_len: DualCidrLength {
886 v4: 32,
887 v6: 128,
888 },
889 },
890 },
891 Directive {
892 qualifier: Fail,
893 mechanism: All,
894 },
895 ],
896 redirect: None,
897 explanation: None,
898}
899"
900 );
901 k9::snapshot!(
902 parse("v=spf1 a:example.org -all"),
903 r#"
904Record {
905 directives: [
906 Directive {
907 qualifier: Pass,
908 mechanism: A {
909 domain: Some(
910 MacroSpec {
911 elements: [
912 Literal(
913 "example.org",
914 ),
915 ],
916 },
917 ),
918 cidr_len: DualCidrLength {
919 v4: 32,
920 v6: 128,
921 },
922 },
923 },
924 Directive {
925 qualifier: Fail,
926 mechanism: All,
927 },
928 ],
929 redirect: None,
930 explanation: None,
931}
932"#
933 );
934 k9::snapshot!(
935 parse("v=spf1 mx -all"),
936 "
937Record {
938 directives: [
939 Directive {
940 qualifier: Pass,
941 mechanism: Mx {
942 domain: None,
943 cidr_len: DualCidrLength {
944 v4: 32,
945 v6: 128,
946 },
947 },
948 },
949 Directive {
950 qualifier: Fail,
951 mechanism: All,
952 },
953 ],
954 redirect: None,
955 explanation: None,
956}
957"
958 );
959 k9::snapshot!(
960 parse("v=spf1 mx:example.org -all"),
961 r#"
962Record {
963 directives: [
964 Directive {
965 qualifier: Pass,
966 mechanism: Mx {
967 domain: Some(
968 MacroSpec {
969 elements: [
970 Literal(
971 "example.org",
972 ),
973 ],
974 },
975 ),
976 cidr_len: DualCidrLength {
977 v4: 32,
978 v6: 128,
979 },
980 },
981 },
982 Directive {
983 qualifier: Fail,
984 mechanism: All,
985 },
986 ],
987 redirect: None,
988 explanation: None,
989}
990"#
991 );
992 k9::snapshot!(
993 parse("v=spf1 mx mx:example.org -all"),
994 r#"
995Record {
996 directives: [
997 Directive {
998 qualifier: Pass,
999 mechanism: Mx {
1000 domain: None,
1001 cidr_len: DualCidrLength {
1002 v4: 32,
1003 v6: 128,
1004 },
1005 },
1006 },
1007 Directive {
1008 qualifier: Pass,
1009 mechanism: Mx {
1010 domain: Some(
1011 MacroSpec {
1012 elements: [
1013 Literal(
1014 "example.org",
1015 ),
1016 ],
1017 },
1018 ),
1019 cidr_len: DualCidrLength {
1020 v4: 32,
1021 v6: 128,
1022 },
1023 },
1024 },
1025 Directive {
1026 qualifier: Fail,
1027 mechanism: All,
1028 },
1029 ],
1030 redirect: None,
1031 explanation: None,
1032}
1033"#
1034 );
1035 k9::snapshot!(
1036 parse("v=spf1 mx/30 -all"),
1037 "
1038Record {
1039 directives: [
1040 Directive {
1041 qualifier: Pass,
1042 mechanism: Mx {
1043 domain: None,
1044 cidr_len: DualCidrLength {
1045 v4: 30,
1046 v6: 128,
1047 },
1048 },
1049 },
1050 Directive {
1051 qualifier: Fail,
1052 mechanism: All,
1053 },
1054 ],
1055 redirect: None,
1056 explanation: None,
1057}
1058"
1059 );
1060 k9::snapshot!(
1061 parse("v=spf1 mx/30 mx:example.org/30 -all"),
1062 r#"
1063Record {
1064 directives: [
1065 Directive {
1066 qualifier: Pass,
1067 mechanism: Mx {
1068 domain: None,
1069 cidr_len: DualCidrLength {
1070 v4: 30,
1071 v6: 128,
1072 },
1073 },
1074 },
1075 Directive {
1076 qualifier: Pass,
1077 mechanism: Mx {
1078 domain: Some(
1079 MacroSpec {
1080 elements: [
1081 Literal(
1082 "example.org",
1083 ),
1084 ],
1085 },
1086 ),
1087 cidr_len: DualCidrLength {
1088 v4: 30,
1089 v6: 128,
1090 },
1091 },
1092 },
1093 Directive {
1094 qualifier: Fail,
1095 mechanism: All,
1096 },
1097 ],
1098 redirect: None,
1099 explanation: None,
1100}
1101"#
1102 );
1103 k9::snapshot!(
1104 parse("v=spf1 ptr -all"),
1105 "
1106Record {
1107 directives: [
1108 Directive {
1109 qualifier: Pass,
1110 mechanism: Ptr {
1111 domain: None,
1112 },
1113 },
1114 Directive {
1115 qualifier: Fail,
1116 mechanism: All,
1117 },
1118 ],
1119 redirect: None,
1120 explanation: None,
1121}
1122"
1123 );
1124 k9::snapshot!(
1125 parse("v=spf1 ip4:192.0.2.128/28 -all"),
1126 "
1127Record {
1128 directives: [
1129 Directive {
1130 qualifier: Pass,
1131 mechanism: Ip4 {
1132 ip4_network: 192.0.2.128,
1133 cidr_len: 28,
1134 },
1135 },
1136 Directive {
1137 qualifier: Fail,
1138 mechanism: All,
1139 },
1140 ],
1141 redirect: None,
1142 explanation: None,
1143}
1144"
1145 );
1146 k9::snapshot!(
1147 parse("v=spf1 include:example.com include:example.net -all"),
1148 r#"
1149Record {
1150 directives: [
1151 Directive {
1152 qualifier: Pass,
1153 mechanism: Include {
1154 domain: MacroSpec {
1155 elements: [
1156 Literal(
1157 "example.com",
1158 ),
1159 ],
1160 },
1161 },
1162 },
1163 Directive {
1164 qualifier: Pass,
1165 mechanism: Include {
1166 domain: MacroSpec {
1167 elements: [
1168 Literal(
1169 "example.net",
1170 ),
1171 ],
1172 },
1173 },
1174 },
1175 Directive {
1176 qualifier: Fail,
1177 mechanism: All,
1178 },
1179 ],
1180 redirect: None,
1181 explanation: None,
1182}
1183"#
1184 );
1185 k9::snapshot!(
1186 parse("v=spf1 redirect=example.org"),
1187 r#"
1188Record {
1189 directives: [],
1190 redirect: Some(
1191 MacroSpec {
1192 elements: [
1193 Literal(
1194 "example.org",
1195 ),
1196 ],
1197 },
1198 ),
1199 explanation: None,
1200}
1201"#
1202 );
1203 }
1204}