1use super::{
8 Directive, Macro, Mechanism, Qualifier, RR_FAIL, RR_NEUTRAL_NONE, RR_SOFTFAIL,
9 RR_TEMP_PERM_ERROR, Spf, Variable,
10};
11use crate::{
12 Error, Version,
13 common::parse::{TagParser, TxtRecordParser, V},
14};
15use std::{
16 net::{Ipv4Addr, Ipv6Addr},
17 slice::Iter,
18};
19
20impl TxtRecordParser for Spf {
21 fn parse(bytes: &[u8]) -> crate::Result<Spf> {
22 let mut record = bytes.iter();
23 if !matches!(record.key(), Some(k) if k == V)
24 || !record.match_bytes(b"spf1")
25 || record.next().is_some_and(|v| !v.is_ascii_whitespace())
26 {
27 return Err(Error::InvalidRecordType);
28 }
29
30 let mut spf = Spf {
31 version: Version::V1,
32 directives: Vec::new(),
33 redirect: None,
34 exp: None,
35 ra: None,
36 rp: 100,
37 rr: u8::MAX,
38 };
39
40 while let Some((term, qualifier, mut stop_char)) = record.next_term() {
41 match term {
42 A | MX => {
43 let mut ip4_cidr_length = 32;
44 let mut ip6_cidr_length = 128;
45 let mut macro_string = Macro::None;
46
47 match stop_char {
48 b' ' => (),
49 b':' | b'=' => {
50 let (ds, stop_char) = record.macro_string(false)?;
51 macro_string = ds;
52 if stop_char == b'/' {
53 let (l1, l2) = record.dual_cidr_length()?;
54 ip4_cidr_length = l1;
55 ip6_cidr_length = l2;
56 } else if stop_char != b' ' {
57 return Err(Error::ParseError);
58 }
59 }
60 b'/' => {
61 let (l1, l2) = record.dual_cidr_length()?;
62 ip4_cidr_length = l1;
63 ip6_cidr_length = l2;
64 }
65 _ => return Err(Error::ParseError),
66 }
67
68 spf.directives.push(Directive::new(
69 qualifier,
70 if term == A {
71 Mechanism::A {
72 macro_string,
73 ip4_mask: u32::MAX << (32 - ip4_cidr_length),
74 ip6_mask: u128::MAX << (128 - ip6_cidr_length),
75 }
76 } else {
77 Mechanism::Mx {
78 macro_string,
79 ip4_mask: u32::MAX << (32 - ip4_cidr_length),
80 ip6_mask: u128::MAX << (128 - ip6_cidr_length),
81 }
82 },
83 ));
84 }
85 ALL => {
86 if stop_char == b' ' {
87 spf.directives
88 .push(Directive::new(qualifier, Mechanism::All))
89 } else {
90 return Err(Error::ParseError);
91 }
92 }
93 INCLUDE | EXISTS => {
94 if stop_char != b':' {
95 return Err(Error::ParseError);
96 }
97 let (macro_string, stop_char) = record.macro_string(false)?;
98 if stop_char == b' ' {
99 spf.directives.push(Directive::new(
100 qualifier,
101 if term == INCLUDE {
102 Mechanism::Include { macro_string }
103 } else {
104 Mechanism::Exists { macro_string }
105 },
106 ));
107 } else {
108 return Err(Error::ParseError);
109 }
110 }
111 IP4 => {
112 if stop_char != b':' {
113 return Err(Error::ParseError);
114 }
115 let mut cidr_length = 32;
116 let (addr, stop_char) = record.ip4()?;
117 if stop_char == b'/' {
118 cidr_length = std::cmp::min(cidr_length, record.cidr_length()?);
119 } else if stop_char != b' ' {
120 return Err(Error::ParseError);
121 }
122 spf.directives.push(Directive::new(
123 qualifier,
124 Mechanism::Ip4 {
125 addr,
126 mask: u32::MAX << (32 - cidr_length),
127 },
128 ));
129 }
130 IP6 => {
131 if stop_char != b':' {
132 return Err(Error::ParseError);
133 }
134 let mut cidr_length = 128;
135 let (addr, stop_char) = record.ip6()?;
136 if stop_char == b'/' {
137 cidr_length = std::cmp::min(cidr_length, record.cidr_length()?);
138 } else if stop_char != b' ' {
139 return Err(Error::ParseError);
140 }
141 spf.directives.push(Directive::new(
142 qualifier,
143 Mechanism::Ip6 {
144 addr,
145 mask: u128::MAX << (128 - cidr_length),
146 },
147 ));
148 }
149 PTR => {
150 let mut macro_string = Macro::None;
151 if stop_char == b':' {
152 let (ds, stop_char_) = record.macro_string(false)?;
153 macro_string = ds;
154 stop_char = stop_char_;
155 }
156
157 if stop_char == b' ' {
158 spf.directives
159 .push(Directive::new(qualifier, Mechanism::Ptr { macro_string }));
160 } else {
161 return Err(Error::ParseError);
162 }
163 }
164 EXP | REDIRECT => {
165 if stop_char != b'=' {
166 return Err(Error::ParseError);
167 }
168 let (macro_string, stop_char) = record.macro_string(false)?;
169 if stop_char != b' ' {
170 return Err(Error::ParseError);
171 }
172 if term == REDIRECT {
173 if spf.redirect.is_none() {
174 spf.redirect = macro_string.into()
175 } else {
176 return Err(Error::ParseError);
177 }
178 } else if spf.exp.is_none() {
179 spf.exp = macro_string.into()
180 } else {
181 return Err(Error::ParseError);
182 };
183 }
184 RA => {
185 let ra = record.ra()?;
186 if !ra.is_empty() {
187 spf.ra = ra.into();
188 }
189 }
190 RP => {
191 spf.rp = std::cmp::min(record.cidr_length()?, 100);
192 }
193 RR => {
194 spf.rr = record.rr()?;
195 }
196 _ => {
197 let (_, stop_char) = record.macro_string(false)?;
198 if stop_char != b' ' {
199 return Err(Error::ParseError);
200 }
201 }
202 }
203 }
204
205 Ok(spf)
206 }
207}
208
209const A: u64 = b'a' as u64;
210const ALL: u64 = ((b'l' as u64) << 16) | ((b'l' as u64) << 8) | (b'a' as u64);
211const EXISTS: u64 = ((b's' as u64) << 40)
212 | ((b't' as u64) << 32)
213 | ((b's' as u64) << 24)
214 | ((b'i' as u64) << 16)
215 | ((b'x' as u64) << 8)
216 | (b'e' as u64);
217const EXP: u64 = ((b'p' as u64) << 16) | ((b'x' as u64) << 8) | (b'e' as u64);
218const INCLUDE: u64 = ((b'e' as u64) << 48)
219 | ((b'd' as u64) << 40)
220 | ((b'u' as u64) << 32)
221 | ((b'l' as u64) << 24)
222 | ((b'c' as u64) << 16)
223 | ((b'n' as u64) << 8)
224 | (b'i' as u64);
225const IP4: u64 = ((b'4' as u64) << 16) | ((b'p' as u64) << 8) | (b'i' as u64);
226const IP6: u64 = ((b'6' as u64) << 16) | ((b'p' as u64) << 8) | (b'i' as u64);
227const MX: u64 = ((b'x' as u64) << 8) | (b'm' as u64);
228const PTR: u64 = ((b'r' as u64) << 16) | ((b't' as u64) << 8) | (b'p' as u64);
229const REDIRECT: u64 = ((b't' as u64) << 56)
230 | ((b'c' as u64) << 48)
231 | ((b'e' as u64) << 40)
232 | ((b'r' as u64) << 32)
233 | ((b'i' as u64) << 24)
234 | ((b'd' as u64) << 16)
235 | ((b'e' as u64) << 8)
236 | (b'r' as u64);
237const RA: u64 = ((b'a' as u64) << 8) | (b'r' as u64);
238const RP: u64 = ((b'p' as u64) << 8) | (b'r' as u64);
239const RR: u64 = ((b'r' as u64) << 8) | (b'r' as u64);
240
241pub(crate) trait SPFParser: Sized {
242 fn next_term(&mut self) -> Option<(u64, Qualifier, u8)>;
243 fn macro_string(&mut self, is_exp: bool) -> crate::Result<(Macro, u8)>;
244 fn ip4(&mut self) -> crate::Result<(Ipv4Addr, u8)>;
245 fn ip6(&mut self) -> crate::Result<(Ipv6Addr, u8)>;
246 fn cidr_length(&mut self) -> crate::Result<u8>;
247 fn dual_cidr_length(&mut self) -> crate::Result<(u8, u8)>;
248 fn rr(&mut self) -> crate::Result<u8>;
249 fn ra(&mut self) -> crate::Result<Vec<u8>>;
250}
251
252impl SPFParser for Iter<'_, u8> {
253 fn next_term(&mut self) -> Option<(u64, Qualifier, u8)> {
254 let mut qualifier = Qualifier::Pass;
255 let mut stop_char = b' ';
256 let mut d = 0;
257 let mut shift = 0;
258
259 for &ch in self {
260 match ch {
261 b'a'..=b'z' | b'4' | b'6' if shift < 64 => {
262 d |= (ch as u64) << shift;
263 shift += 8;
264 }
265 b'A'..=b'Z' if shift < 64 => {
266 d |= ((ch - b'A' + b'a') as u64) << shift;
267 shift += 8;
268 }
269 b'+' if shift == 0 => {
270 qualifier = Qualifier::Pass;
271 }
272 b'-' if shift == 0 => {
273 qualifier = Qualifier::Fail;
274 }
275 b'~' if shift == 0 => {
276 qualifier = Qualifier::SoftFail;
277 }
278 b'?' if shift == 0 => {
279 qualifier = Qualifier::Neutral;
280 }
281 b':' | b'=' | b'/' => {
282 stop_char = ch;
283 break;
284 }
285 _ => {
286 if ch.is_ascii_whitespace() {
287 if shift != 0 {
288 stop_char = b' ';
289 break;
290 }
291 } else {
292 d = u64::MAX;
293 shift = 64;
294 }
295 }
296 }
297 }
298
299 if d != 0 {
300 (d, qualifier, stop_char).into()
301 } else {
302 None
303 }
304 }
305
306 #[allow(clippy::while_let_on_iterator)]
307 fn macro_string(&mut self, is_exp: bool) -> crate::Result<(Macro, u8)> {
308 let mut stop_char = b' ';
309 let mut last_is_pct = false;
310 let mut literal = Vec::with_capacity(16);
311 let mut macro_string = Vec::new();
312
313 while let Some(&ch) = self.next() {
314 match ch {
315 b'%' => {
316 if last_is_pct {
317 literal.push(b'%');
318 } else {
319 last_is_pct = true;
320 continue;
321 }
322 }
323 b'_' if last_is_pct => {
324 literal.push(b' ');
325 }
326 b'-' if last_is_pct => {
327 literal.extend_from_slice(b"%20");
328 }
329 b'{' if last_is_pct => {
330 if !literal.is_empty() {
331 macro_string.push(Macro::Literal(literal.to_vec()));
332 literal.clear();
333 }
334
335 let (letter, escape) = self
336 .next()
337 .copied()
338 .and_then(|l| {
339 if !is_exp {
340 Variable::parse(l)
341 } else {
342 Variable::parse_exp(l)
343 }
344 })
345 .ok_or(Error::ParseError)?;
346 let mut num_parts: u32 = 0;
347 let mut reverse = false;
348 let mut delimiters = 0;
349
350 while let Some(&ch) = self.next() {
351 match ch {
352 b'0'..=b'9' => {
353 num_parts = num_parts
354 .saturating_mul(10)
355 .saturating_add((ch - b'0') as u32);
356 }
357 b'r' | b'R' => {
358 reverse = true;
359 }
360 b'}' => {
361 break;
362 }
363 b'.' | b'-' | b'+' | b',' | b'/' | b'_' | b'=' => {
364 delimiters |= 1u64 << (ch - b'+');
365 }
366 _ => {
367 return Err(Error::ParseError);
368 }
369 }
370 }
371
372 if delimiters == 0 {
373 delimiters = 1u64 << (b'.' - b'+');
374 }
375
376 macro_string.push(Macro::Variable {
377 letter,
378 num_parts,
379 reverse,
380 escape,
381 delimiters,
382 });
383 }
384 b'/' if !is_exp => {
385 stop_char = ch;
386 break;
387 }
388 _ => {
389 if last_is_pct {
390 return Err(Error::ParseError);
391 } else if !ch.is_ascii_whitespace() || is_exp {
392 literal.push(ch);
393 } else {
394 break;
395 }
396 }
397 }
398
399 last_is_pct = false;
400 }
401
402 if !literal.is_empty() {
403 macro_string.push(Macro::Literal(literal));
404 }
405
406 match macro_string.len() {
407 1 => Ok((macro_string.pop().unwrap(), stop_char)),
408 0 => Err(Error::ParseError),
409 _ => Ok((Macro::List(macro_string), stop_char)),
410 }
411 }
412
413 fn ip4(&mut self) -> crate::Result<(Ipv4Addr, u8)> {
414 let mut stop_char = b' ';
415 let mut pos = 0;
416 let mut ip = [0u8; 4];
417
418 for &ch in self {
419 match ch {
420 b'0'..=b'9' => {
421 ip[pos] = (ip[pos].saturating_mul(10)).saturating_add(ch - b'0');
422 }
423 b'.' if pos < 3 => {
424 pos += 1;
425 }
426 _ => {
427 stop_char = if ch.is_ascii_whitespace() { b' ' } else { ch };
428 break;
429 }
430 }
431 }
432
433 if pos == 3 {
434 Ok((Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]), stop_char))
435 } else {
436 Err(Error::ParseError)
437 }
438 }
439
440 fn ip6(&mut self) -> crate::Result<(Ipv6Addr, u8)> {
441 let mut stop_char = b' ';
442 let mut ip = [0u16; 8];
443 let mut ip_pos = 0;
444 let mut ip4_pos = 0;
445 let mut ip_part = [0u8; 8];
446 let mut ip_part_pos = 0;
447 let mut zero_group_pos = usize::MAX;
448
449 for &ch in self {
450 match ch {
451 b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => {
452 if ip_part_pos < 4 {
453 ip_part[ip_part_pos] = ch;
454 ip_part_pos += 1;
455 } else {
456 return Err(Error::ParseError);
457 }
458 }
459 b':' => {
460 if ip_pos < 8 {
461 if ip_part_pos != 0 {
462 ip[ip_pos] = u16::from_str_radix(
463 std::str::from_utf8(&ip_part[..ip_part_pos]).unwrap(),
464 16,
465 )
466 .map_err(|_| Error::ParseError)?;
467 ip_part_pos = 0;
468 ip_pos += 1;
469 } else if zero_group_pos == usize::MAX {
470 zero_group_pos = ip_pos;
471 } else if zero_group_pos != ip_pos {
472 return Err(Error::ParseError);
473 }
474 } else {
475 return Err(Error::ParseError);
476 }
477 }
478 b'.' => {
479 if ip_pos < 8 && ip_part_pos > 0 {
480 let qnum = std::str::from_utf8(&ip_part[..ip_part_pos])
481 .unwrap()
482 .parse::<u8>()
483 .map_err(|_| Error::ParseError)?
484 as u16;
485 ip_part_pos = 0;
486 if ip4_pos % 2 == 1 {
487 ip[ip_pos] = (ip[ip_pos] << 8) | qnum;
488 ip_pos += 1;
489 } else {
490 ip[ip_pos] = qnum;
491 }
492 ip4_pos += 1;
493 } else {
494 return Err(Error::ParseError);
495 }
496 }
497 _ => {
498 stop_char = if ch.is_ascii_whitespace() { b' ' } else { ch };
499 break;
500 }
501 }
502 }
503
504 if ip_part_pos != 0 {
505 if ip_pos < 8 {
506 ip[ip_pos] = if ip4_pos == 0 {
507 u16::from_str_radix(std::str::from_utf8(&ip_part[..ip_part_pos]).unwrap(), 16)
508 .map_err(|_| Error::ParseError)?
509 } else if ip4_pos == 3 {
510 (ip[ip_pos] << 8)
511 | std::str::from_utf8(&ip_part[..ip_part_pos])
512 .unwrap()
513 .parse::<u8>()
514 .map_err(|_| Error::ParseError)? as u16
515 } else {
516 return Err(Error::ParseError);
517 };
518
519 ip_pos += 1;
520 } else {
521 return Err(Error::ParseError);
522 }
523 }
524 if zero_group_pos != usize::MAX && zero_group_pos < ip_pos {
525 if ip_pos <= 7 {
526 ip.copy_within(zero_group_pos..ip_pos, zero_group_pos + 8 - ip_pos);
527 ip[zero_group_pos..zero_group_pos + 8 - ip_pos].fill(0);
528 } else {
529 return Err(Error::ParseError);
530 }
531 }
532
533 if ip_pos != 0 || zero_group_pos != usize::MAX {
534 Ok((
535 Ipv6Addr::new(ip[0], ip[1], ip[2], ip[3], ip[4], ip[5], ip[6], ip[7]),
536 stop_char,
537 ))
538 } else {
539 Err(Error::ParseError)
540 }
541 }
542
543 fn cidr_length(&mut self) -> crate::Result<u8> {
544 let mut cidr_length: u8 = 0;
545 for &ch in self {
546 match ch {
547 b'0'..=b'9' => {
548 cidr_length = (cidr_length.saturating_mul(10)).saturating_add(ch - b'0');
549 }
550 _ => {
551 if ch.is_ascii_whitespace() {
552 break;
553 } else {
554 return Err(Error::ParseError);
555 }
556 }
557 }
558 }
559
560 Ok(cidr_length)
561 }
562
563 fn dual_cidr_length(&mut self) -> crate::Result<(u8, u8)> {
564 let mut ip4_length: u8 = u8::MAX;
565 let mut ip6_length: u8 = u8::MAX;
566 let mut in_ip6 = false;
567
568 for &ch in self {
569 match ch {
570 b'0'..=b'9' => {
571 if in_ip6 {
572 ip6_length = if ip6_length != u8::MAX {
573 (ip6_length.saturating_mul(10)).saturating_add(ch - b'0')
574 } else {
575 ch - b'0'
576 };
577 } else {
578 ip4_length = if ip4_length != u8::MAX {
579 (ip4_length.saturating_mul(10)).saturating_add(ch - b'0')
580 } else {
581 ch - b'0'
582 };
583 }
584 }
585 b'/' => {
586 if !in_ip6 {
587 in_ip6 = true;
588 } else if ip6_length != u8::MAX {
589 return Err(Error::ParseError);
590 }
591 }
592 _ => {
593 if ch.is_ascii_whitespace() {
594 break;
595 } else {
596 return Err(Error::ParseError);
597 }
598 }
599 }
600 }
601
602 Ok((
603 std::cmp::min(ip4_length, 32),
604 std::cmp::min(ip6_length, 128),
605 ))
606 }
607
608 fn rr(&mut self) -> crate::Result<u8> {
609 let mut flags: u8 = 0;
610
611 'outer: while let Some(&ch) = self.next() {
612 match ch {
613 b'a' | b'A' => {
614 for _ in 0..2 {
615 match self.next().unwrap_or(&0) {
616 b'l' | b'L' => {}
617 b' ' | b'\t' => {
618 return Ok(flags);
619 }
620 _ => {
621 continue 'outer;
622 }
623 }
624 }
625 flags = u8::MAX;
626 }
627 b'e' | b'E' => {
628 flags |= RR_TEMP_PERM_ERROR;
629 }
630 b'f' | b'F' => {
631 flags |= RR_FAIL;
632 }
633 b's' | b'S' => {
634 flags |= RR_SOFTFAIL;
635 }
636 b'n' | b'N' => {
637 flags |= RR_NEUTRAL_NONE;
638 }
639 b':' => {}
640 _ => {
641 if ch.is_ascii_whitespace() {
642 break;
643 } else if !ch.is_ascii_alphanumeric() {
644 return Err(Error::ParseError);
645 }
646 }
647 }
648 }
649
650 Ok(flags)
651 }
652
653 fn ra(&mut self) -> crate::Result<Vec<u8>> {
654 let mut ra = Vec::new();
655 for &ch in self {
656 if !ch.is_ascii_whitespace() {
657 ra.push(ch);
658 } else {
659 break;
660 }
661 }
662 Ok(ra)
663 }
664}
665
666impl Variable {
667 fn parse(ch: u8) -> Option<(Self, bool)> {
668 match ch {
669 b's' => (Variable::Sender, false),
670 b'l' => (Variable::SenderLocalPart, false),
671 b'o' => (Variable::SenderDomainPart, false),
672 b'd' => (Variable::Domain, false),
673 b'i' => (Variable::Ip, false),
674 b'p' => (Variable::ValidatedDomain, false),
675 b'v' => (Variable::IpVersion, false),
676 b'h' => (Variable::HeloDomain, false),
677
678 b'S' => (Variable::Sender, true),
679 b'L' => (Variable::SenderLocalPart, true),
680 b'O' => (Variable::SenderDomainPart, true),
681 b'D' => (Variable::Domain, true),
682 b'I' => (Variable::Ip, true),
683 b'P' => (Variable::ValidatedDomain, true),
684 b'V' => (Variable::IpVersion, true),
685 b'H' => (Variable::HeloDomain, true),
686 _ => return None,
687 }
688 .into()
689 }
690
691 fn parse_exp(ch: u8) -> Option<(Self, bool)> {
692 match ch {
693 b's' => (Variable::Sender, false),
694 b'l' => (Variable::SenderLocalPart, false),
695 b'o' => (Variable::SenderDomainPart, false),
696 b'd' => (Variable::Domain, false),
697 b'i' => (Variable::Ip, false),
698 b'p' => (Variable::ValidatedDomain, false),
699 b'v' => (Variable::IpVersion, false),
700 b'h' => (Variable::HeloDomain, false),
701 b'c' => (Variable::SmtpIp, false),
702 b'r' => (Variable::HostDomain, false),
703 b't' => (Variable::CurrentTime, false),
704
705 b'S' => (Variable::Sender, true),
706 b'L' => (Variable::SenderLocalPart, true),
707 b'O' => (Variable::SenderDomainPart, true),
708 b'D' => (Variable::Domain, true),
709 b'I' => (Variable::Ip, true),
710 b'P' => (Variable::ValidatedDomain, true),
711 b'V' => (Variable::IpVersion, true),
712 b'H' => (Variable::HeloDomain, true),
713 b'C' => (Variable::SmtpIp, true),
714 b'R' => (Variable::HostDomain, true),
715 b'T' => (Variable::CurrentTime, true),
716 _ => return None,
717 }
718 .into()
719 }
720}
721
722impl TxtRecordParser for Macro {
723 fn parse(record: &[u8]) -> crate::Result<Self> {
724 record.iter().macro_string(true).map(|(m, _)| m)
725 }
726}
727
728#[cfg(test)]
729mod test {
730 use std::net::{Ipv4Addr, Ipv6Addr};
731
732 use crate::{
733 common::parse::TxtRecordParser,
734 spf::{
735 Directive, Macro, Mechanism, Qualifier, RR_FAIL, RR_NEUTRAL_NONE, RR_SOFTFAIL,
736 RR_TEMP_PERM_ERROR, Spf, Variable, Version,
737 },
738 };
739
740 use super::SPFParser;
741
742 #[test]
743 fn parse_spf() {
744 for (record, expected_result) in [
745 (
746 "v=spf1 +mx a:colo.example.com/28 -all",
747 Spf {
748 version: Version::V1,
749 ra: None,
750 rp: 100,
751 rr: u8::MAX,
752 exp: None,
753 redirect: None,
754 directives: vec![
755 Directive::new(
756 Qualifier::Pass,
757 Mechanism::Mx {
758 macro_string: Macro::None,
759 ip4_mask: u32::MAX,
760 ip6_mask: u128::MAX,
761 },
762 ),
763 Directive::new(
764 Qualifier::Pass,
765 Mechanism::A {
766 macro_string: Macro::Literal(b"colo.example.com".to_vec()),
767 ip4_mask: u32::MAX << (32 - 28),
768 ip6_mask: u128::MAX,
769 },
770 ),
771 Directive::new(Qualifier::Fail, Mechanism::All),
772 ],
773 },
774 ),
775 (
776 "v=spf1 a:A.EXAMPLE.COM -all",
777 Spf {
778 version: Version::V1,
779 ra: None,
780 rp: 100,
781 rr: u8::MAX,
782 exp: None,
783 redirect: None,
784 directives: vec![
785 Directive::new(
786 Qualifier::Pass,
787 Mechanism::A {
788 macro_string: Macro::Literal(b"A.EXAMPLE.COM".to_vec()),
789 ip4_mask: u32::MAX,
790 ip6_mask: u128::MAX,
791 },
792 ),
793 Directive::new(Qualifier::Fail, Mechanism::All),
794 ],
795 },
796 ),
797 (
798 "v=spf1 +mx -all",
799 Spf {
800 version: Version::V1,
801 ra: None,
802 rp: 100,
803 rr: u8::MAX,
804 exp: None,
805 redirect: None,
806 directives: vec![
807 Directive::new(
808 Qualifier::Pass,
809 Mechanism::Mx {
810 macro_string: Macro::None,
811 ip4_mask: u32::MAX,
812 ip6_mask: u128::MAX,
813 },
814 ),
815 Directive::new(Qualifier::Fail, Mechanism::All),
816 ],
817 },
818 ),
819 (
820 "v=spf1 +mx redirect=_spf.example.com",
821 Spf {
822 version: Version::V1,
823 ra: None,
824 rp: 100,
825 rr: u8::MAX,
826 redirect: Macro::Literal(b"_spf.example.com".to_vec()).into(),
827 exp: None,
828 directives: vec![Directive::new(
829 Qualifier::Pass,
830 Mechanism::Mx {
831 macro_string: Macro::None,
832 ip4_mask: u32::MAX,
833 ip6_mask: u128::MAX,
834 },
835 )],
836 },
837 ),
838 (
839 "v=spf1 a mx -all",
840 Spf {
841 version: Version::V1,
842 ra: None,
843 rp: 100,
844 rr: u8::MAX,
845 exp: None,
846 redirect: None,
847 directives: vec![
848 Directive::new(
849 Qualifier::Pass,
850 Mechanism::A {
851 macro_string: Macro::None,
852 ip4_mask: u32::MAX,
853 ip6_mask: u128::MAX,
854 },
855 ),
856 Directive::new(
857 Qualifier::Pass,
858 Mechanism::Mx {
859 macro_string: Macro::None,
860 ip4_mask: u32::MAX,
861 ip6_mask: u128::MAX,
862 },
863 ),
864 Directive::new(Qualifier::Fail, Mechanism::All),
865 ],
866 },
867 ),
868 (
869 "v=spf1 include:example.com include:example.org -all",
870 Spf {
871 version: Version::V1,
872 ra: None,
873 rp: 100,
874 rr: u8::MAX,
875 exp: None,
876 redirect: None,
877 directives: vec![
878 Directive::new(
879 Qualifier::Pass,
880 Mechanism::Include {
881 macro_string: Macro::Literal(b"example.com".to_vec()),
882 },
883 ),
884 Directive::new(
885 Qualifier::Pass,
886 Mechanism::Include {
887 macro_string: Macro::Literal(b"example.org".to_vec()),
888 },
889 ),
890 Directive::new(Qualifier::Fail, Mechanism::All),
891 ],
892 },
893 ),
894 (
895 "v=spf1 exists:%{ir}.%{l1r+-}._spf.%{d} -all",
896 Spf {
897 version: Version::V1,
898 ra: None,
899 rp: 100,
900 rr: u8::MAX,
901 exp: None,
902 redirect: None,
903 directives: vec![
904 Directive::new(
905 Qualifier::Pass,
906 Mechanism::Exists {
907 macro_string: Macro::List(vec![
908 Macro::Variable {
909 letter: Variable::Ip,
910 num_parts: 0,
911 reverse: true,
912 escape: false,
913 delimiters: 1u64 << (b'.' - b'+'),
914 },
915 Macro::Literal(b".".to_vec()),
916 Macro::Variable {
917 letter: Variable::SenderLocalPart,
918 num_parts: 1,
919 reverse: true,
920 escape: false,
921 delimiters: (1u64 << (b'+' - b'+'))
922 | (1u64 << (b'-' - b'+')),
923 },
924 Macro::Literal(b"._spf.".to_vec()),
925 Macro::Variable {
926 letter: Variable::Domain,
927 num_parts: 0,
928 reverse: false,
929 escape: false,
930 delimiters: 1u64 << (b'.' - b'+'),
931 },
932 ]),
933 },
934 ),
935 Directive::new(Qualifier::Fail, Mechanism::All),
936 ],
937 },
938 ),
939 (
940 "v=spf1 mx -all exp=explain._spf.%{d}",
941 Spf {
942 version: Version::V1,
943 ra: None,
944 rp: 100,
945 rr: u8::MAX,
946 exp: Macro::List(vec![
947 Macro::Literal(b"explain._spf.".to_vec()),
948 Macro::Variable {
949 letter: Variable::Domain,
950 num_parts: 0,
951 reverse: false,
952 escape: false,
953 delimiters: 1u64 << (b'.' - b'+'),
954 },
955 ])
956 .into(),
957 redirect: None,
958 directives: vec![
959 Directive::new(
960 Qualifier::Pass,
961 Mechanism::Mx {
962 macro_string: Macro::None,
963 ip4_mask: u32::MAX,
964 ip6_mask: u128::MAX,
965 },
966 ),
967 Directive::new(Qualifier::Fail, Mechanism::All),
968 ],
969 },
970 ),
971 (
972 "v=spf1 ip4:192.0.2.1 ip4:192.0.2.129 -all",
973 Spf {
974 version: Version::V1,
975 ra: None,
976 rp: 100,
977 rr: u8::MAX,
978 exp: None,
979 redirect: None,
980 directives: vec![
981 Directive::new(
982 Qualifier::Pass,
983 Mechanism::Ip4 {
984 addr: "192.0.2.1".parse().unwrap(),
985 mask: u32::MAX,
986 },
987 ),
988 Directive::new(
989 Qualifier::Pass,
990 Mechanism::Ip4 {
991 addr: "192.0.2.129".parse().unwrap(),
992 mask: u32::MAX,
993 },
994 ),
995 Directive::new(Qualifier::Fail, Mechanism::All),
996 ],
997 },
998 ),
999 (
1000 "v=spf1 ip4:192.0.2.0/24 mx -all",
1001 Spf {
1002 version: Version::V1,
1003 ra: None,
1004 rp: 100,
1005 rr: u8::MAX,
1006 exp: None,
1007 redirect: None,
1008 directives: vec![
1009 Directive::new(
1010 Qualifier::Pass,
1011 Mechanism::Ip4 {
1012 addr: "192.0.2.0".parse().unwrap(),
1013 mask: u32::MAX << (32 - 24),
1014 },
1015 ),
1016 Directive::new(
1017 Qualifier::Pass,
1018 Mechanism::Mx {
1019 macro_string: Macro::None,
1020 ip4_mask: u32::MAX,
1021 ip6_mask: u128::MAX,
1022 },
1023 ),
1024 Directive::new(Qualifier::Fail, Mechanism::All),
1025 ],
1026 },
1027 ),
1028 (
1029 "v=spf1 mx/30 mx:example.org/30 -all",
1030 Spf {
1031 version: Version::V1,
1032 ra: None,
1033 rp: 100,
1034 rr: u8::MAX,
1035 exp: None,
1036 redirect: None,
1037 directives: vec![
1038 Directive::new(
1039 Qualifier::Pass,
1040 Mechanism::Mx {
1041 macro_string: Macro::None,
1042 ip4_mask: u32::MAX << (32 - 30),
1043 ip6_mask: u128::MAX,
1044 },
1045 ),
1046 Directive::new(
1047 Qualifier::Pass,
1048 Mechanism::Mx {
1049 macro_string: Macro::Literal(b"example.org".to_vec()),
1050 ip4_mask: u32::MAX << (32 - 30),
1051 ip6_mask: u128::MAX,
1052 },
1053 ),
1054 Directive::new(Qualifier::Fail, Mechanism::All),
1055 ],
1056 },
1057 ),
1058 (
1059 "v=spf1 ptr -all",
1060 Spf {
1061 version: Version::V1,
1062 ra: None,
1063 rp: 100,
1064 rr: u8::MAX,
1065 exp: None,
1066 redirect: None,
1067 directives: vec![
1068 Directive::new(
1069 Qualifier::Pass,
1070 Mechanism::Ptr {
1071 macro_string: Macro::None,
1072 },
1073 ),
1074 Directive::new(Qualifier::Fail, Mechanism::All),
1075 ],
1076 },
1077 ),
1078 (
1079 "v=spf1 exists:%{l1r+}.%{d}",
1080 Spf {
1081 version: Version::V1,
1082 ra: None,
1083 rp: 100,
1084 rr: u8::MAX,
1085 exp: None,
1086 redirect: None,
1087 directives: vec![Directive::new(
1088 Qualifier::Pass,
1089 Mechanism::Exists {
1090 macro_string: Macro::List(vec![
1091 Macro::Variable {
1092 letter: Variable::SenderLocalPart,
1093 num_parts: 1,
1094 reverse: true,
1095 escape: false,
1096 delimiters: 1u64 << (b'+' - b'+'),
1097 },
1098 Macro::Literal(b".".to_vec()),
1099 Macro::Variable {
1100 letter: Variable::Domain,
1101 num_parts: 0,
1102 reverse: false,
1103 escape: false,
1104 delimiters: 1u64 << (b'.' - b'+'),
1105 },
1106 ]),
1107 },
1108 )],
1109 },
1110 ),
1111 (
1112 "v=spf1 exists:%{ir}.%{l1r+}.%{d}",
1113 Spf {
1114 version: Version::V1,
1115 ra: None,
1116 rp: 100,
1117 rr: u8::MAX,
1118 exp: None,
1119 redirect: None,
1120 directives: vec![Directive::new(
1121 Qualifier::Pass,
1122 Mechanism::Exists {
1123 macro_string: Macro::List(vec![
1124 Macro::Variable {
1125 letter: Variable::Ip,
1126 num_parts: 0,
1127 reverse: true,
1128 escape: false,
1129 delimiters: 1u64 << (b'.' - b'+'),
1130 },
1131 Macro::Literal(b".".to_vec()),
1132 Macro::Variable {
1133 letter: Variable::SenderLocalPart,
1134 num_parts: 1,
1135 reverse: true,
1136 escape: false,
1137 delimiters: 1u64 << (b'+' - b'+'),
1138 },
1139 Macro::Literal(b".".to_vec()),
1140 Macro::Variable {
1141 letter: Variable::Domain,
1142 num_parts: 0,
1143 reverse: false,
1144 escape: false,
1145 delimiters: 1u64 << (b'.' - b'+'),
1146 },
1147 ]),
1148 },
1149 )],
1150 },
1151 ),
1152 (
1153 "v=spf1 exists:_h.%{h}._l.%{l}._o.%{o}._i.%{i}._spf.%{d} ?all",
1154 Spf {
1155 version: Version::V1,
1156 ra: None,
1157 rp: 100,
1158 rr: u8::MAX,
1159 exp: None,
1160 redirect: None,
1161 directives: vec![
1162 Directive::new(
1163 Qualifier::Pass,
1164 Mechanism::Exists {
1165 macro_string: Macro::List(vec![
1166 Macro::Literal(b"_h.".to_vec()),
1167 Macro::Variable {
1168 letter: Variable::HeloDomain,
1169 num_parts: 0,
1170 reverse: false,
1171 escape: false,
1172 delimiters: 1u64 << (b'.' - b'+'),
1173 },
1174 Macro::Literal(b"._l.".to_vec()),
1175 Macro::Variable {
1176 letter: Variable::SenderLocalPart,
1177 num_parts: 0,
1178 reverse: false,
1179 escape: false,
1180 delimiters: 1u64 << (b'.' - b'+'),
1181 },
1182 Macro::Literal(b"._o.".to_vec()),
1183 Macro::Variable {
1184 letter: Variable::SenderDomainPart,
1185 num_parts: 0,
1186 reverse: false,
1187 escape: false,
1188 delimiters: 1u64 << (b'.' - b'+'),
1189 },
1190 Macro::Literal(b"._i.".to_vec()),
1191 Macro::Variable {
1192 letter: Variable::Ip,
1193 num_parts: 0,
1194 reverse: false,
1195 escape: false,
1196 delimiters: 1u64 << (b'.' - b'+'),
1197 },
1198 Macro::Literal(b"._spf.".to_vec()),
1199 Macro::Variable {
1200 letter: Variable::Domain,
1201 num_parts: 0,
1202 reverse: false,
1203 escape: false,
1204 delimiters: 1u64 << (b'.' - b'+'),
1205 },
1206 ]),
1207 },
1208 ),
1209 Directive::new(Qualifier::Neutral, Mechanism::All),
1210 ],
1211 },
1212 ),
1213 (
1214 "v=spf1 mx ?exists:%{ir}.whitelist.example.org -all",
1215 Spf {
1216 version: Version::V1,
1217 ra: None,
1218 rp: 100,
1219 rr: u8::MAX,
1220 exp: None,
1221 redirect: None,
1222 directives: vec![
1223 Directive::new(
1224 Qualifier::Pass,
1225 Mechanism::Mx {
1226 macro_string: Macro::None,
1227 ip4_mask: u32::MAX,
1228 ip6_mask: u128::MAX,
1229 },
1230 ),
1231 Directive::new(
1232 Qualifier::Neutral,
1233 Mechanism::Exists {
1234 macro_string: Macro::List(vec![
1235 Macro::Variable {
1236 letter: Variable::Ip,
1237 num_parts: 0,
1238 reverse: true,
1239 escape: false,
1240 delimiters: 1u64 << (b'.' - b'+'),
1241 },
1242 Macro::Literal(b".whitelist.example.org".to_vec()),
1243 ]),
1244 },
1245 ),
1246 Directive::new(Qualifier::Fail, Mechanism::All),
1247 ],
1248 },
1249 ),
1250 (
1251 "v=spf1 mx exists:%{l}._%-spf_%_verify%%.%{d} -all",
1252 Spf {
1253 version: Version::V1,
1254 ra: None,
1255 rp: 100,
1256 rr: u8::MAX,
1257 exp: None,
1258 redirect: None,
1259 directives: vec![
1260 Directive::new(
1261 Qualifier::Pass,
1262 Mechanism::Mx {
1263 macro_string: Macro::None,
1264 ip4_mask: u32::MAX,
1265 ip6_mask: u128::MAX,
1266 },
1267 ),
1268 Directive::new(
1269 Qualifier::Pass,
1270 Mechanism::Exists {
1271 macro_string: Macro::List(vec![
1272 Macro::Variable {
1273 letter: Variable::SenderLocalPart,
1274 num_parts: 0,
1275 reverse: false,
1276 escape: false,
1277 delimiters: 1u64 << (b'.' - b'+'),
1278 },
1279 Macro::Literal(b"._%20spf_ verify%.".to_vec()),
1280 Macro::Variable {
1281 letter: Variable::Domain,
1282 num_parts: 0,
1283 reverse: false,
1284 escape: false,
1285 delimiters: 1u64 << (b'.' - b'+'),
1286 },
1287 ]),
1288 },
1289 ),
1290 Directive::new(Qualifier::Fail, Mechanism::All),
1291 ],
1292 },
1293 ),
1294 (
1295 "v=spf1 mx redirect=%{l1r+}._at_.%{o,=_/}._spf.%{d}",
1296 Spf {
1297 version: Version::V1,
1298 ra: None,
1299 rp: 100,
1300 rr: u8::MAX,
1301 exp: None,
1302 redirect: Macro::List(vec![
1303 Macro::Variable {
1304 letter: Variable::SenderLocalPart,
1305 num_parts: 1,
1306 reverse: true,
1307 escape: false,
1308 delimiters: 1u64 << (b'+' - b'+'),
1309 },
1310 Macro::Literal(b"._at_.".to_vec()),
1311 Macro::Variable {
1312 letter: Variable::SenderDomainPart,
1313 num_parts: 0,
1314 reverse: false,
1315 escape: false,
1316 delimiters: (1u64 << (b',' - b'+'))
1317 | (1u64 << (b'=' - b'+'))
1318 | (1u64 << (b'_' - b'+'))
1319 | (1u64 << (b'/' - b'+')),
1320 },
1321 Macro::Literal(b"._spf.".to_vec()),
1322 Macro::Variable {
1323 letter: Variable::Domain,
1324 num_parts: 0,
1325 reverse: false,
1326 escape: false,
1327 delimiters: 1u64 << (b'.' - b'+'),
1328 },
1329 ])
1330 .into(),
1331 directives: vec![Directive::new(
1332 Qualifier::Pass,
1333 Mechanism::Mx {
1334 macro_string: Macro::None,
1335 ip4_mask: u32::MAX,
1336 ip6_mask: u128::MAX,
1337 },
1338 )],
1339 },
1340 ),
1341 (
1342 "v=spf1 -ip4:192.0.2.0/24 a//96 +all",
1343 Spf {
1344 version: Version::V1,
1345 ra: None,
1346 rp: 100,
1347 rr: u8::MAX,
1348 exp: None,
1349 redirect: None,
1350 directives: vec![
1351 Directive::new(
1352 Qualifier::Fail,
1353 Mechanism::Ip4 {
1354 addr: "192.0.2.0".parse().unwrap(),
1355 mask: u32::MAX << (32 - 24),
1356 },
1357 ),
1358 Directive::new(
1359 Qualifier::Pass,
1360 Mechanism::A {
1361 macro_string: Macro::None,
1362 ip4_mask: u32::MAX,
1363 ip6_mask: u128::MAX << (128 - 96),
1364 },
1365 ),
1366 Directive::new(Qualifier::Pass, Mechanism::All),
1367 ],
1368 },
1369 ),
1370 (
1371 concat!(
1372 "v=spf1 +mx/11//100 ~a:domain.com/12/123 ?ip6:::1 ",
1373 "-ip6:a::b/111 ip6:1080::8:800:68.0.3.1/96 "
1374 ),
1375 Spf {
1376 version: Version::V1,
1377 ra: None,
1378 rp: 100,
1379 rr: u8::MAX,
1380 exp: None,
1381 redirect: None,
1382 directives: vec![
1383 Directive::new(
1384 Qualifier::Pass,
1385 Mechanism::Mx {
1386 macro_string: Macro::None,
1387 ip4_mask: u32::MAX << (32 - 11),
1388 ip6_mask: u128::MAX << (128 - 100),
1389 },
1390 ),
1391 Directive::new(
1392 Qualifier::SoftFail,
1393 Mechanism::A {
1394 macro_string: Macro::Literal(b"domain.com".to_vec()),
1395 ip4_mask: u32::MAX << (32 - 12),
1396 ip6_mask: u128::MAX << (128 - 123),
1397 },
1398 ),
1399 Directive::new(
1400 Qualifier::Neutral,
1401 Mechanism::Ip6 {
1402 addr: "::1".parse().unwrap(),
1403 mask: u128::MAX,
1404 },
1405 ),
1406 Directive::new(
1407 Qualifier::Fail,
1408 Mechanism::Ip6 {
1409 addr: "a::b".parse().unwrap(),
1410 mask: u128::MAX << (128 - 111),
1411 },
1412 ),
1413 Directive::new(
1414 Qualifier::Pass,
1415 Mechanism::Ip6 {
1416 addr: "1080::8:800:68.0.3.1".parse().unwrap(),
1417 mask: u128::MAX << (128 - 96),
1418 },
1419 ),
1420 ],
1421 },
1422 ),
1423 (
1424 "v=spf1 mx:example.org -all ra=postmaster rp=15 rr=e:f:s:n",
1425 Spf {
1426 version: Version::V1,
1427 ra: b"postmaster".to_vec().into(),
1428 rp: 15,
1429 rr: RR_FAIL | RR_NEUTRAL_NONE | RR_SOFTFAIL | RR_TEMP_PERM_ERROR,
1430 exp: None,
1431 redirect: None,
1432 directives: vec![
1433 Directive::new(
1434 Qualifier::Pass,
1435 Mechanism::Mx {
1436 macro_string: Macro::Literal(b"example.org".to_vec()),
1437 ip4_mask: u32::MAX,
1438 ip6_mask: u128::MAX,
1439 },
1440 ),
1441 Directive::new(Qualifier::Fail, Mechanism::All),
1442 ],
1443 },
1444 ),
1445 (
1446 "v=spf1 ip6:fe80:0000:0000::0000:0000:0000:1 -all",
1447 Spf {
1448 version: Version::V1,
1449 ra: None,
1450 rp: 100,
1451 rr: u8::MAX,
1452 exp: None,
1453 redirect: None,
1454 directives: vec![
1455 Directive::new(
1456 Qualifier::Pass,
1457 Mechanism::Ip6 {
1458 addr: "fe80:0000:0000::0000:0000:0000:1".parse().unwrap(),
1459 mask: u128::MAX,
1460 },
1461 ),
1462 Directive::new(Qualifier::Fail, Mechanism::All),
1463 ],
1464 },
1465 ),
1466 ] {
1467 assert_eq!(
1468 Spf::parse(record.as_bytes()).unwrap_or_else(|err| panic!("{record:?} : {err:?}")),
1469 expected_result,
1470 "{record}"
1471 );
1472 }
1473 }
1474
1475 #[test]
1476 fn parse_ip6() {
1477 for test in [
1478 "ABCD:EF01:2345:6789:ABCD:EF01:2345:6789",
1479 "2001:DB8:0:0:8:800:200C:417A",
1480 "FF01:0:0:0:0:0:0:101",
1481 "0:0:0:0:0:0:0:1",
1482 "0:0:0:0:0:0:0:0",
1483 "2001:DB8::8:800:200C:417A",
1484 "2001:DB8:0:0:8:800:200C::",
1485 "FF01::101",
1486 "1234::",
1487 "::1",
1488 "::",
1489 "a:b::c:d",
1490 "a::c:d",
1491 "a:b:c::d",
1492 "::c:d",
1493 "0:0:0:0:0:0:13.1.68.3",
1494 "0:0:0:0:0:FFFF:129.144.52.38",
1495 "::13.1.68.3",
1496 "::FFFF:129.144.52.38",
1497 "fe80::1",
1498 "fe80::0000:1",
1499 "fe80:0000::0000:1",
1500 "fe80:0000:0000:0000::1",
1501 "fe80:0000:0000:0000::0000:1",
1502 "fe80:0000:0000::0000:0000:0000:1",
1503 "fe80::0000:0000:0000:0000:0000:1",
1504 "fe80:0000:0000:0000:0000:0000:0000:1",
1505 ] {
1506 for test in [test.to_string(), format!("{test} ")] {
1507 let (ip, stop_char) = test
1508 .as_bytes()
1509 .iter()
1510 .ip6()
1511 .unwrap_or_else(|err| panic!("{test:?} : {err:?}"));
1512 assert_eq!(stop_char, b' ', "{test}");
1513 assert_eq!(ip, test.trim_end().parse::<Ipv6Addr>().unwrap())
1514 }
1515 }
1516
1517 for invalid_test in [
1518 "0:0:0:0:0:0:0:1:1",
1519 "0:0:0:0:0:0:13.1.68.3.4",
1520 "::0:0:0:0:0:0:0:0",
1521 "0:0:0:0::0:0:0:0",
1522 " ",
1523 "",
1524 ] {
1525 assert!(
1526 invalid_test.as_bytes().iter().ip6().is_err(),
1527 "{}",
1528 invalid_test
1529 );
1530 }
1531 }
1532
1533 #[test]
1534 fn parse_ip4() {
1535 for test in ["0.0.0.0", "255.255.255.255", "13.1.68.3", "129.144.52.38"] {
1536 for test in [test.to_string(), format!("{test} ")] {
1537 let (ip, stop_char) = test
1538 .as_bytes()
1539 .iter()
1540 .ip4()
1541 .unwrap_or_else(|err| panic!("{test:?} : {err:?}"));
1542 assert_eq!(stop_char, b' ', "{test}");
1543 assert_eq!(ip, test.trim_end().parse::<Ipv4Addr>().unwrap());
1544 }
1545 }
1546 }
1547}