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