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