1use crate::spec::MacroSpec;
2use crate::{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(&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 Qualifier::parse(&s[0..1]) {
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(&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(&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.resolve_ip(&exchange.to_string()).await {
203 Ok(ips) => ips,
204 Err(err) => {
205 return Err(SpfResult {
206 disposition: SpfDisposition::TempError,
207 context: format!("error looking up IP for {exchange}: {err}"),
208 })
209 }
210 };
211
212 if resolved
213 .iter()
214 .any(|&resolved_ip| cidr_len.matches(cx.client_ip, resolved_ip))
215 {
216 matched = true;
217 break;
218 }
219 }
220
221 matched
222 }
223 Mechanism::Ip4 {
224 ip4_network,
225 cidr_len,
226 } => DualCidrLength {
227 v4: *cidr_len,
228 ..Default::default()
229 }
230 .matches(cx.client_ip, IpAddr::V4(*ip4_network)),
231 Mechanism::Ip6 {
232 ip6_network,
233 cidr_len,
234 } => DualCidrLength {
235 v6: *cidr_len,
236 ..Default::default()
237 }
238 .matches(cx.client_ip, IpAddr::V6(*ip6_network)),
239 Mechanism::Ptr { domain } => {
240 let validated_domain = cx.validated_domain(domain.as_ref(), resolver).await?;
242
243 validated_domain.is_some()
244 }
245 Mechanism::Include { domain } => {
246 let domain = cx.domain(Some(domain), resolver).await?;
247 let nested = cx.with_domain(&domain);
248 use SpfDisposition::*;
249 match Box::pin(nested.check(resolver, false)).await {
250 SpfResult {
251 disposition: Pass, ..
252 } => true,
253 SpfResult {
254 disposition: Fail | SoftFail | Neutral,
255 ..
256 } => false,
257 SpfResult {
258 disposition: TempError,
259 context,
260 } => {
261 return Err(SpfResult {
262 disposition: TempError,
263 context: format!(
264 "temperror while evaluating include:{domain}: {context}"
265 ),
266 })
267 }
268 SpfResult {
269 disposition: disp @ PermError | disp @ None,
270 context,
271 } => {
272 return Err(SpfResult {
273 disposition: PermError,
274 context: format!("{disp} while evaluating include:{domain}: {context}"),
275 })
276 }
277 }
278 }
279 Mechanism::Exists { domain } => {
280 cx.check_lookup_limit()?;
281 let domain = cx.domain(Some(domain), resolver).await?;
282 match resolver.resolve_ip(&domain).await {
283 Ok(ips) => ips.iter().any(|ip| ip.is_ipv4()),
284 Err(err) => {
285 return Err(SpfResult {
286 disposition: SpfDisposition::TempError,
287 context: format!("error looking up IP for {domain}: {err}"),
288 })
289 }
290 }
291 }
292 };
293
294 Ok(if matched {
295 Some(SpfResult {
296 disposition: SpfDisposition::from(self.qualifier),
297 context: format!("matched '{self}' directive"),
298 })
299 } else {
300 None
301 })
302 }
303}
304
305impl fmt::Display for Directive {
306 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307 if self.qualifier != Qualifier::Pass {
308 write!(f, "{}", self.qualifier.as_str())?;
309 }
310 write!(f, "{}", self.mechanism)
311 }
312}
313
314#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
315pub(crate) enum Qualifier {
316 #[default]
318 Pass,
319 Fail,
321 SoftFail,
323 Neutral,
325}
326
327impl Qualifier {
328 fn parse(s: &str) -> Option<Self> {
329 Some(match s {
330 "+" => Self::Pass,
331 "-" => Self::Fail,
332 "~" => Self::SoftFail,
333 "?" => Self::Neutral,
334 _ => return None,
335 })
336 }
337
338 fn as_str(&self) -> &'static str {
339 match self {
340 Self::Pass => "+",
341 Self::Fail => "-",
342 Self::SoftFail => "~",
343 Self::Neutral => "?",
344 }
345 }
346}
347
348#[derive(Debug)]
349struct DualCidrLength {
350 pub v4: u8,
351 pub v6: u8,
352}
353
354impl DualCidrLength {
355 fn matches(&self, observed: IpAddr, specified: IpAddr) -> bool {
358 match (observed, specified, self) {
359 (IpAddr::V4(observed), IpAddr::V4(specified), DualCidrLength { v4, .. }) => {
360 let mask = u32::MAX << (32 - v4);
361 let specified_masked = Ipv4Addr::from_bits(specified.to_bits() & mask);
362 let observed_masked = Ipv4Addr::from(observed.to_bits() & mask);
363 specified_masked == observed_masked
364 }
365 (IpAddr::V6(observed), IpAddr::V6(specified), DualCidrLength { v6, .. }) => {
366 let mask = u128::MAX << (128 - v6);
367 let specified_masked = Ipv6Addr::from_bits(specified.to_bits() & mask);
368 let observed_masked = Ipv6Addr::from(observed.to_bits() & mask);
369 specified_masked == observed_masked
370 }
371 _ => false,
372 }
373 }
374}
375
376impl Default for DualCidrLength {
377 fn default() -> Self {
378 Self { v4: 32, v6: 128 }
379 }
380}
381
382impl DualCidrLength {
383 fn parse_from_end(s: &str) -> Result<(&str, Self), String> {
384 match s.rsplit_once('/') {
385 Some((left, right)) => {
386 let right_cidr: u8 = right
387 .parse()
388 .map_err(|err| format!("invalid dual-cidr-length in {s}: {err}"))?;
389
390 if left.ends_with('/') {
391 if let Some((prefix, v4cidr)) = left[0..left.len() - 1].rsplit_once('/') {
393 let left_cidr: u8 = v4cidr.parse().map_err(|err| {
394 format!(
395 "invalid dual-cidr-length in {s}: parsing v4 cidr portion: {err}"
396 )
397 })?;
398 return Ok((
399 prefix,
400 Self {
401 v4: left_cidr,
402 v6: right_cidr,
403 },
404 ));
405 }
406 }
407 Ok((
408 left,
409 Self {
410 v4: right_cidr,
411 ..Self::default()
412 },
413 ))
414 }
415 None => Ok((s, Self::default())),
416 }
417 }
418}
419
420impl fmt::Display for DualCidrLength {
421 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422 if self.v4 == 32 && self.v6 == 128 {
423 return Ok(());
424 }
425
426 write!(f, "/{}", self.v4)?;
427 if self.v6 != 128 {
428 write!(f, "/{}", self.v6)?;
429 }
430
431 Ok(())
432 }
433}
434
435#[derive(Debug)]
436enum Mechanism {
437 All,
438 Include {
439 domain: MacroSpec,
440 },
441 A {
442 domain: Option<MacroSpec>,
443 cidr_len: DualCidrLength,
444 },
445 Mx {
446 domain: Option<MacroSpec>,
447 cidr_len: DualCidrLength,
448 },
449 Ptr {
450 domain: Option<MacroSpec>,
451 },
452 Ip4 {
453 ip4_network: Ipv4Addr,
454 cidr_len: u8,
455 },
456 Ip6 {
457 ip6_network: Ipv6Addr,
458 cidr_len: u8,
459 },
460 Exists {
461 domain: MacroSpec,
462 },
463}
464
465impl fmt::Display for Mechanism {
466 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
467 match self {
468 Self::All => write!(f, "all"),
469 Self::Include { domain } => write!(f, "include:{}", domain),
470 Self::A { domain, cidr_len } => {
471 write!(f, "a")?;
472 if let Some(domain) = domain {
473 write!(f, ":{}", domain)?;
474 }
475 write!(f, "{}", cidr_len)
476 }
477 Self::Mx { domain, cidr_len } => {
478 write!(f, "mx")?;
479 if let Some(domain) = domain {
480 write!(f, ":{}", domain)?;
481 }
482 write!(f, "{}", cidr_len)
483 }
484 Self::Ptr { domain } => {
485 write!(f, "ptr")?;
486 if let Some(domain) = domain {
487 write!(f, ":{}", domain)?;
488 }
489 Ok(())
490 }
491 Self::Ip4 {
492 ip4_network,
493 cidr_len,
494 } => write!(f, "ip4:{}/{}", ip4_network, cidr_len),
495 Self::Ip6 {
496 ip6_network,
497 cidr_len,
498 } => write!(f, "ip6:{}/{}", ip6_network, cidr_len),
499 Self::Exists { domain } => write!(f, "exists:{}", domain),
500 }
501 }
502}
503
504fn starts_with_ident<'a>(s: &'a str, ident: &str) -> Option<&'a str> {
505 if s.len() < ident.len() {
506 return None;
507 }
508
509 if s[0..ident.len()].eq_ignore_ascii_case(ident) {
510 Some(&s[ident.len()..])
511 } else {
512 None
513 }
514}
515
516impl Mechanism {
517 fn parse(s: &str) -> Result<Self, String> {
518 if s.eq_ignore_ascii_case("all") {
519 return Ok(Self::All);
520 }
521
522 if let Some(spec) = starts_with_ident(s, "include:") {
523 return Ok(Self::Include {
524 domain: MacroSpec::parse(spec)?,
525 });
526 }
527
528 if let Some(remain) = starts_with_ident(s, "a") {
529 let (remain, cidr_len) = DualCidrLength::parse_from_end(remain)?;
530
531 let domain = if let Some(spec) = remain.strip_prefix(":") {
532 Some(MacroSpec::parse(spec)?)
533 } else if remain.is_empty() {
534 None
535 } else {
536 return Err(format!("invalid 'a' mechanism: {s}"));
537 };
538
539 return Ok(Self::A { domain, cidr_len });
540 }
541 if let Some(remain) = starts_with_ident(s, "mx") {
542 let (remain, cidr_len) = DualCidrLength::parse_from_end(remain)?;
543
544 let domain = if let Some(spec) = remain.strip_prefix(":") {
545 Some(MacroSpec::parse(spec)?)
546 } else if remain.is_empty() {
547 None
548 } else {
549 return Err(format!("invalid 'mx' mechanism: {s}"));
550 };
551
552 return Ok(Self::Mx { domain, cidr_len });
553 }
554 if let Some(remain) = starts_with_ident(s, "ptr") {
555 let domain = if let Some(spec) = remain.strip_prefix(":") {
556 Some(MacroSpec::parse(spec)?)
557 } else if remain.is_empty() {
558 None
559 } else {
560 return Err(format!("invalid 'ptr' mechanism: {s}"));
561 };
562
563 return Ok(Self::Ptr { domain });
564 }
565 if let Some(remain) = starts_with_ident(s, "ip4:") {
566 let (addr, len) = if let Some((addr, len)) = remain.split_once('/') {
567 (addr, len)
568 } else {
569 (remain, "32")
571 };
572 let ip4_network = addr
573 .parse()
574 .map_err(|err| format!("invalid 'ip4' mechanism: {s}: {err}"))?;
575 let cidr_len = len
576 .parse()
577 .map_err(|err| format!("invalid 'ip4' mechanism: {s}: {err}"))?;
578
579 return Ok(Self::Ip4 {
580 ip4_network,
581 cidr_len,
582 });
583 }
584 if let Some(remain) = starts_with_ident(s, "ip6:") {
585 let (addr, len) = if let Some((addr, len)) = remain.split_once('/') {
586 (addr, len)
587 } else {
588 (remain, "128")
590 };
591 let ip6_network = addr
592 .parse()
593 .map_err(|err| format!("invalid 'ip6' mechanism: {s}: {err}"))?;
594 let cidr_len = len
595 .parse()
596 .map_err(|err| format!("invalid 'ip6' mechanism: {s}: {err}"))?;
597
598 return Ok(Self::Ip6 {
599 ip6_network,
600 cidr_len,
601 });
602 }
603 if let Some(spec) = starts_with_ident(s, "exists:") {
604 return Ok(Self::Exists {
605 domain: MacroSpec::parse(spec)?,
606 });
607 }
608
609 Err(format!("invalid mechanism {s}"))
610 }
611}
612
613#[derive(Debug)]
614enum Modifier {
615 Redirect(MacroSpec),
616 Explanation(MacroSpec),
617 Unknown,
618}
619
620impl Modifier {
621 fn parse(s: &str) -> Result<Self, String> {
622 if let Some(spec) = starts_with_ident(s, "redirect=") {
623 return Ok(Self::Redirect(MacroSpec::parse(spec)?));
624 }
625 if let Some(spec) = starts_with_ident(s, "exp=") {
626 return Ok(Self::Explanation(MacroSpec::parse(spec)?));
627 }
628
629 let (name, _) = s
630 .split_once('=')
631 .ok_or_else(|| format!("invalid modifier {s}"))?;
632
633 let valid = !name.is_empty()
634 && name
635 .chars()
636 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
637 && name.chars().next().unwrap().is_ascii_alphabetic();
638 if !valid {
639 return Err(format!("modifier name '{name}' is invalid"));
640 }
641
642 Ok(Self::Unknown)
643 }
644}
645
646#[cfg(test)]
647mod test {
648 use super::*;
649
650 fn parse(s: &str) -> Record {
651 eprintln!("**\n{s}");
652 match Record::parse(s) {
653 Ok(r) => r,
654 Err(err) => panic!("{err}: {s}"),
655 }
656 }
657
658 #[test]
659 fn test_parse() {
660 k9::snapshot!(
661 Record::parse("v=spf1 -exists:%(ir).sbl.example.org").unwrap_err(),
662 r#"invalid token '-exists:%(ir).sbl.example.org'"#
663 );
664 k9::snapshot!(
665 Record::parse("v=spf1 -exists:%{ir.sbl.example.org").unwrap_err(),
666 r#"invalid token '-exists:%{ir.sbl.example.org'"#
667 );
668 k9::snapshot!(
669 Record::parse("v=spf1 -exists:%{ir").unwrap_err(),
670 r#"invalid token '-exists:%{ir'"#
671 );
672
673 k9::snapshot!(
676 Record::parse("v=spf1 ").unwrap(),
677 "
678Record {
679 directives: [],
680 redirect: None,
681 explanation: None,
682}
683"
684 );
685
686 k9::snapshot!(
687 parse("v=spf1 mx -all exp=explain._spf.%{d}"),
688 r#"
689Record {
690 directives: [
691 Directive {
692 qualifier: Pass,
693 mechanism: Mx {
694 domain: None,
695 cidr_len: DualCidrLength {
696 v4: 32,
697 v6: 128,
698 },
699 },
700 },
701 Directive {
702 qualifier: Fail,
703 mechanism: All,
704 },
705 ],
706 redirect: None,
707 explanation: Some(
708 MacroSpec {
709 elements: [
710 Literal(
711 "explain._spf.",
712 ),
713 Macro(
714 MacroTerm {
715 name: Domain,
716 transformer_digits: None,
717 url_escape: false,
718 reverse: false,
719 delimiters: "",
720 },
721 ),
722 ],
723 },
724 ),
725}
726"#
727 );
728
729 k9::snapshot!(
730 parse("v=spf1 -exists:%{ir}.sbl.example.org"),
731 r#"
732Record {
733 directives: [
734 Directive {
735 qualifier: Fail,
736 mechanism: Exists {
737 domain: MacroSpec {
738 elements: [
739 Macro(
740 MacroTerm {
741 name: Ip,
742 transformer_digits: None,
743 url_escape: false,
744 reverse: true,
745 delimiters: "",
746 },
747 ),
748 Literal(
749 ".sbl.example.org",
750 ),
751 ],
752 },
753 },
754 },
755 ],
756 redirect: None,
757 explanation: None,
758}
759"#
760 );
761
762 k9::snapshot!(
763 parse("v=spf1 +all"),
764 "
765Record {
766 directives: [
767 Directive {
768 qualifier: Pass,
769 mechanism: All,
770 },
771 ],
772 redirect: None,
773 explanation: None,
774}
775"
776 );
777 k9::snapshot!(
778 parse("v=spf1 a -all"),
779 "
780Record {
781 directives: [
782 Directive {
783 qualifier: Pass,
784 mechanism: A {
785 domain: None,
786 cidr_len: DualCidrLength {
787 v4: 32,
788 v6: 128,
789 },
790 },
791 },
792 Directive {
793 qualifier: Fail,
794 mechanism: All,
795 },
796 ],
797 redirect: None,
798 explanation: None,
799}
800"
801 );
802 k9::snapshot!(
803 parse("v=spf1 a:example.org -all"),
804 r#"
805Record {
806 directives: [
807 Directive {
808 qualifier: Pass,
809 mechanism: A {
810 domain: Some(
811 MacroSpec {
812 elements: [
813 Literal(
814 "example.org",
815 ),
816 ],
817 },
818 ),
819 cidr_len: DualCidrLength {
820 v4: 32,
821 v6: 128,
822 },
823 },
824 },
825 Directive {
826 qualifier: Fail,
827 mechanism: All,
828 },
829 ],
830 redirect: None,
831 explanation: None,
832}
833"#
834 );
835 k9::snapshot!(
836 parse("v=spf1 mx -all"),
837 "
838Record {
839 directives: [
840 Directive {
841 qualifier: Pass,
842 mechanism: Mx {
843 domain: None,
844 cidr_len: DualCidrLength {
845 v4: 32,
846 v6: 128,
847 },
848 },
849 },
850 Directive {
851 qualifier: Fail,
852 mechanism: All,
853 },
854 ],
855 redirect: None,
856 explanation: None,
857}
858"
859 );
860 k9::snapshot!(
861 parse("v=spf1 mx:example.org -all"),
862 r#"
863Record {
864 directives: [
865 Directive {
866 qualifier: Pass,
867 mechanism: Mx {
868 domain: Some(
869 MacroSpec {
870 elements: [
871 Literal(
872 "example.org",
873 ),
874 ],
875 },
876 ),
877 cidr_len: DualCidrLength {
878 v4: 32,
879 v6: 128,
880 },
881 },
882 },
883 Directive {
884 qualifier: Fail,
885 mechanism: All,
886 },
887 ],
888 redirect: None,
889 explanation: None,
890}
891"#
892 );
893 k9::snapshot!(
894 parse("v=spf1 mx mx:example.org -all"),
895 r#"
896Record {
897 directives: [
898 Directive {
899 qualifier: Pass,
900 mechanism: Mx {
901 domain: None,
902 cidr_len: DualCidrLength {
903 v4: 32,
904 v6: 128,
905 },
906 },
907 },
908 Directive {
909 qualifier: Pass,
910 mechanism: Mx {
911 domain: Some(
912 MacroSpec {
913 elements: [
914 Literal(
915 "example.org",
916 ),
917 ],
918 },
919 ),
920 cidr_len: DualCidrLength {
921 v4: 32,
922 v6: 128,
923 },
924 },
925 },
926 Directive {
927 qualifier: Fail,
928 mechanism: All,
929 },
930 ],
931 redirect: None,
932 explanation: None,
933}
934"#
935 );
936 k9::snapshot!(
937 parse("v=spf1 mx/30 -all"),
938 "
939Record {
940 directives: [
941 Directive {
942 qualifier: Pass,
943 mechanism: Mx {
944 domain: None,
945 cidr_len: DualCidrLength {
946 v4: 30,
947 v6: 128,
948 },
949 },
950 },
951 Directive {
952 qualifier: Fail,
953 mechanism: All,
954 },
955 ],
956 redirect: None,
957 explanation: None,
958}
959"
960 );
961 k9::snapshot!(
962 parse("v=spf1 mx/30 mx:example.org/30 -all"),
963 r#"
964Record {
965 directives: [
966 Directive {
967 qualifier: Pass,
968 mechanism: Mx {
969 domain: None,
970 cidr_len: DualCidrLength {
971 v4: 30,
972 v6: 128,
973 },
974 },
975 },
976 Directive {
977 qualifier: Pass,
978 mechanism: Mx {
979 domain: Some(
980 MacroSpec {
981 elements: [
982 Literal(
983 "example.org",
984 ),
985 ],
986 },
987 ),
988 cidr_len: DualCidrLength {
989 v4: 30,
990 v6: 128,
991 },
992 },
993 },
994 Directive {
995 qualifier: Fail,
996 mechanism: All,
997 },
998 ],
999 redirect: None,
1000 explanation: None,
1001}
1002"#
1003 );
1004 k9::snapshot!(
1005 parse("v=spf1 ptr -all"),
1006 "
1007Record {
1008 directives: [
1009 Directive {
1010 qualifier: Pass,
1011 mechanism: Ptr {
1012 domain: None,
1013 },
1014 },
1015 Directive {
1016 qualifier: Fail,
1017 mechanism: All,
1018 },
1019 ],
1020 redirect: None,
1021 explanation: None,
1022}
1023"
1024 );
1025 k9::snapshot!(
1026 parse("v=spf1 ip4:192.0.2.128/28 -all"),
1027 "
1028Record {
1029 directives: [
1030 Directive {
1031 qualifier: Pass,
1032 mechanism: Ip4 {
1033 ip4_network: 192.0.2.128,
1034 cidr_len: 28,
1035 },
1036 },
1037 Directive {
1038 qualifier: Fail,
1039 mechanism: All,
1040 },
1041 ],
1042 redirect: None,
1043 explanation: None,
1044}
1045"
1046 );
1047 k9::snapshot!(
1048 parse("v=spf1 include:example.com include:example.net -all"),
1049 r#"
1050Record {
1051 directives: [
1052 Directive {
1053 qualifier: Pass,
1054 mechanism: Include {
1055 domain: MacroSpec {
1056 elements: [
1057 Literal(
1058 "example.com",
1059 ),
1060 ],
1061 },
1062 },
1063 },
1064 Directive {
1065 qualifier: Pass,
1066 mechanism: Include {
1067 domain: MacroSpec {
1068 elements: [
1069 Literal(
1070 "example.net",
1071 ),
1072 ],
1073 },
1074 },
1075 },
1076 Directive {
1077 qualifier: Fail,
1078 mechanism: All,
1079 },
1080 ],
1081 redirect: None,
1082 explanation: None,
1083}
1084"#
1085 );
1086 k9::snapshot!(
1087 parse("v=spf1 redirect=example.org"),
1088 r#"
1089Record {
1090 directives: [],
1091 redirect: Some(
1092 MacroSpec {
1093 elements: [
1094 Literal(
1095 "example.org",
1096 ),
1097 ],
1098 },
1099 ),
1100 explanation: None,
1101}
1102"#
1103 );
1104 }
1105}