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