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