Skip to main content

mail_auth/spf/
parse.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use 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}