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, SpfRecord, Variable,
10};
11use crate::DnsError;
12use crate::{
13    Error,
14    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 SpfRecord {
23    fn parse(bytes: &[u8]) -> crate::Result<SpfRecord> {
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::Parse);
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::Parse),
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::Parse);
89                    }
90                }
91                INCLUDE | EXISTS => {
92                    if stop_char != b':' {
93                        return Err(Error::Parse);
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::Parse);
107                    }
108                }
109                IP4 => {
110                    if stop_char != b':' {
111                        return Err(Error::Parse);
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::Parse);
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::Parse);
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::Parse);
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::Parse);
159                    }
160                }
161                EXP | REDIRECT => {
162                    if stop_char != b'=' {
163                        return Err(Error::Parse);
164                    }
165                    let (macro_string, stop_char) = record.macro_string(false)?;
166                    if stop_char != b' ' {
167                        return Err(Error::Parse);
168                    }
169                    if term == REDIRECT {
170                        if redirect.is_none() {
171                            redirect = macro_string.into()
172                        } else {
173                            return Err(Error::Parse);
174                        }
175                    } else if exp.is_none() {
176                        exp = macro_string.into()
177                    } else {
178                        return Err(Error::Parse);
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::Parse);
197                    }
198                }
199            }
200        }
201
202        Ok(SpfRecord {
203            directives: directives.into_boxed_slice(),
204            redirect,
205            exp,
206            ra,
207            rp,
208            rr,
209        })
210    }
211}
212
213#[inline(always)]
214fn ip4_mask(cidr_length: u8) -> u32 {
215    if cidr_length == 0 {
216        0
217    } else {
218        u32::MAX << (32 - cidr_length)
219    }
220}
221
222#[inline(always)]
223fn ip6_mask(cidr_length: u8) -> u128 {
224    if cidr_length == 0 {
225        0
226    } else {
227        u128::MAX << (128 - cidr_length)
228    }
229}
230
231const TERM_POISON: u8 = 0;
232const TERM_SPACE: u8 = 1;
233const TERM_STOP: u8 = 2;
234
235const TERM_CLASS: [u8; 256] = {
236    let mut table = [TERM_POISON; 256];
237    let mut ch = 0usize;
238    while ch < 256 {
239        table[ch] = match ch as u8 {
240            b'a'..=b'z' => ch as u8,
241            b'A'..=b'Z' => (ch as u8) - b'A' + b'a',
242            b'4' => b'4',
243            b'6' => b'6',
244            b':' | b'=' | b'/' => TERM_STOP,
245            b'\t' | b'\n' | b'\x0C' | b'\r' | b' ' => TERM_SPACE,
246            _ => TERM_POISON,
247        };
248        ch += 1;
249    }
250    table
251};
252
253const A: u64 = b'a' as u64;
254const ALL: u64 = ((b'l' as u64) << 16) | ((b'l' as u64) << 8) | (b'a' as u64);
255const EXISTS: u64 = ((b's' as u64) << 40)
256    | ((b't' as u64) << 32)
257    | ((b's' as u64) << 24)
258    | ((b'i' as u64) << 16)
259    | ((b'x' as u64) << 8)
260    | (b'e' as u64);
261const EXP: u64 = ((b'p' as u64) << 16) | ((b'x' as u64) << 8) | (b'e' as u64);
262const INCLUDE: u64 = ((b'e' as u64) << 48)
263    | ((b'd' as u64) << 40)
264    | ((b'u' as u64) << 32)
265    | ((b'l' as u64) << 24)
266    | ((b'c' as u64) << 16)
267    | ((b'n' as u64) << 8)
268    | (b'i' as u64);
269const IP4: u64 = ((b'4' as u64) << 16) | ((b'p' as u64) << 8) | (b'i' as u64);
270const IP6: u64 = ((b'6' as u64) << 16) | ((b'p' as u64) << 8) | (b'i' as u64);
271const MX: u64 = ((b'x' as u64) << 8) | (b'm' as u64);
272const PTR: u64 = ((b'r' as u64) << 16) | ((b't' as u64) << 8) | (b'p' as u64);
273const REDIRECT: u64 = ((b't' as u64) << 56)
274    | ((b'c' as u64) << 48)
275    | ((b'e' as u64) << 40)
276    | ((b'r' as u64) << 32)
277    | ((b'i' as u64) << 24)
278    | ((b'd' as u64) << 16)
279    | ((b'e' as u64) << 8)
280    | (b'r' as u64);
281const RA: u64 = ((b'a' as u64) << 8) | (b'r' as u64);
282const RP: u64 = ((b'p' as u64) << 8) | (b'r' as u64);
283const RR: u64 = ((b'r' as u64) << 8) | (b'r' as u64);
284
285pub(crate) trait SPFParser: Sized {
286    fn next_term(&mut self) -> Option<(u64, Qualifier, u8)>;
287    fn macro_string(&mut self, is_exp: bool) -> crate::Result<(Macro, u8)>;
288    fn ip4(&mut self) -> crate::Result<(Ipv4Addr, u8)>;
289    fn ip6(&mut self) -> crate::Result<(Ipv6Addr, u8)>;
290    fn cidr_length(&mut self) -> crate::Result<u8>;
291    fn dual_cidr_length(&mut self) -> crate::Result<(u8, u8)>;
292    fn rr(&mut self) -> crate::Result<u8>;
293    fn ra(&mut self) -> crate::Result<Vec<u8>>;
294}
295
296impl SPFParser for Iter<'_, u8> {
297    fn next_term(&mut self) -> Option<(u64, Qualifier, u8)> {
298        let input = self.as_slice();
299        let mut pos = 0;
300        let mut qualifier = Qualifier::Pass;
301
302        while let Some(&ch) = input.get(pos) {
303            match ch {
304                b'+' => qualifier = Qualifier::Pass,
305                b'-' => qualifier = Qualifier::Fail,
306                b'~' => qualifier = Qualifier::SoftFail,
307                b'?' => qualifier = Qualifier::Neutral,
308                b'\t' | b'\n' | b'\x0C' | b'\r' | b' ' => (),
309                _ => break,
310            }
311            pos += 1;
312        }
313
314        let mut d = 0u64;
315        let mut shift = 0;
316        let mut stop_char = b' ';
317
318        while let Some(&ch) = input.get(pos) {
319            pos += 1;
320            match TERM_CLASS[ch as usize] {
321                TERM_POISON => {
322                    d = u64::MAX;
323                    shift = 64;
324                }
325                TERM_SPACE => {
326                    stop_char = b' ';
327                    break;
328                }
329                TERM_STOP => {
330                    stop_char = ch;
331                    break;
332                }
333                lower if shift < 64 => {
334                    d |= (lower as u64) << shift;
335                    shift += 8;
336                }
337                _ => {
338                    d = u64::MAX;
339                    shift = 64;
340                }
341            }
342        }
343
344        *self = input.get(pos..).unwrap_or_default().iter();
345
346        if d != 0 {
347            (d, qualifier, stop_char).into()
348        } else {
349            None
350        }
351    }
352
353    fn macro_string(&mut self, is_exp: bool) -> crate::Result<(Macro, u8)> {
354        let input = self.as_slice();
355        let mut pos = 0;
356
357        loop {
358            let rest = input.get(pos..).unwrap_or_default();
359            let hit = pos
360                + if is_exp {
361                    memchr::memchr(b'%', rest).unwrap_or(rest.len())
362                } else {
363                    find_le_space_or2(rest, b'%', b'/')
364                };
365
366            match input.get(hit) {
367                None => {
368                    *self = [].iter();
369                    return literal_macro(input, b' ');
370                }
371                Some(&b'%') => break,
372                Some(&ch) if !is_exp && (ch == b'/' || ch.is_ascii_whitespace()) => {
373                    *self = input.get(hit + 1..).unwrap_or_default().iter();
374                    return literal_macro(
375                        input.get(..hit).unwrap_or_default(),
376                        if ch == b'/' { b'/' } else { b' ' },
377                    );
378                }
379                Some(_) => pos = hit + 1,
380            }
381        }
382
383        self.macro_string_slow(is_exp)
384    }
385
386    fn ip4(&mut self) -> crate::Result<(Ipv4Addr, u8)> {
387        let input = self.as_slice();
388        let mut pos = 0;
389        let mut stop_char = b' ';
390        let mut ip = [0u8; 4];
391        let mut octet = 0u8;
392        let mut group = 0usize;
393
394        while let Some(&ch) = input.get(pos) {
395            pos += 1;
396            match ch {
397                b'0'..=b'9' => {
398                    octet = octet.saturating_mul(10).saturating_add(ch - b'0');
399                }
400                b'.' if group < 3 => {
401                    ip[group] = octet;
402                    octet = 0;
403                    group += 1;
404                }
405                _ => {
406                    stop_char = if ch.is_ascii_whitespace() { b' ' } else { ch };
407                    break;
408                }
409            }
410        }
411
412        *self = input.get(pos..).unwrap_or_default().iter();
413
414        if group == 3 {
415            let [a, b, c, _] = ip;
416            Ok((Ipv4Addr::new(a, b, c, octet), stop_char))
417        } else {
418            Err(Error::Parse)
419        }
420    }
421
422    fn ip6(&mut self) -> crate::Result<(Ipv6Addr, u8)> {
423        let input = self.as_slice();
424        let (result, pos) = parse_ip6(input);
425        *self = input.get(pos..).unwrap_or_default().iter();
426        result
427    }
428
429    fn cidr_length(&mut self) -> crate::Result<u8> {
430        let input = self.as_slice();
431        let mut pos = 0;
432        let mut cidr_length = 0u8;
433
434        while let Some(&ch) = input.get(pos) {
435            pos += 1;
436            match ch {
437                b'0'..=b'9' => {
438                    cidr_length = cidr_length.saturating_mul(10).saturating_add(ch - b'0');
439                }
440                _ => {
441                    if !ch.is_ascii_whitespace() {
442                        *self = input.get(pos..).unwrap_or_default().iter();
443                        return Err(Error::Parse);
444                    }
445                    break;
446                }
447            }
448        }
449
450        *self = input.get(pos..).unwrap_or_default().iter();
451        Ok(cidr_length)
452    }
453
454    fn dual_cidr_length(&mut self) -> crate::Result<(u8, u8)> {
455        let input = self.as_slice();
456        let mut pos = 0;
457        let mut ip4_length = u8::MAX;
458        let mut ip6_length = u8::MAX;
459        let mut in_ip6 = false;
460
461        while let Some(&ch) = input.get(pos) {
462            pos += 1;
463            match ch {
464                b'0'..=b'9' => {
465                    let digit = ch - b'0';
466                    let length = if in_ip6 {
467                        &mut ip6_length
468                    } else {
469                        &mut ip4_length
470                    };
471                    *length = if *length != u8::MAX {
472                        length.saturating_mul(10).saturating_add(digit)
473                    } else {
474                        digit
475                    };
476                }
477                b'/' => {
478                    if !in_ip6 {
479                        in_ip6 = true;
480                    } else if ip6_length != u8::MAX {
481                        *self = input.get(pos..).unwrap_or_default().iter();
482                        return Err(Error::Parse);
483                    }
484                }
485                _ => {
486                    if !ch.is_ascii_whitespace() {
487                        *self = input.get(pos..).unwrap_or_default().iter();
488                        return Err(Error::Parse);
489                    }
490                    break;
491                }
492            }
493        }
494
495        *self = input.get(pos..).unwrap_or_default().iter();
496        Ok((
497            std::cmp::min(ip4_length, 32),
498            std::cmp::min(ip6_length, 128),
499        ))
500    }
501
502    fn rr(&mut self) -> crate::Result<u8> {
503        let mut flags: u8 = 0;
504
505        'outer: while let Some(&ch) = self.next() {
506            match ch {
507                b'a' | b'A' => {
508                    for _ in 0..2 {
509                        match self.next().unwrap_or(&0) {
510                            b'l' | b'L' => {}
511                            b' ' | b'\t' => {
512                                return Ok(flags);
513                            }
514                            _ => {
515                                continue 'outer;
516                            }
517                        }
518                    }
519                    flags = u8::MAX;
520                }
521                b'e' | b'E' => {
522                    flags |= RR_TEMP_PERM_ERROR;
523                }
524                b'f' | b'F' => {
525                    flags |= RR_FAIL;
526                }
527                b's' | b'S' => {
528                    flags |= RR_SOFTFAIL;
529                }
530                b'n' | b'N' => {
531                    flags |= RR_NEUTRAL_NONE;
532                }
533                b':' => {}
534                _ => {
535                    if ch.is_ascii_whitespace() {
536                        break;
537                    } else if !ch.is_ascii_alphanumeric() {
538                        return Err(Error::Parse);
539                    }
540                }
541            }
542        }
543
544        Ok(flags)
545    }
546
547    fn ra(&mut self) -> crate::Result<Vec<u8>> {
548        let input = self.as_slice();
549        let end = find_ascii_whitespace(input);
550        let ra = input.get(..end).unwrap_or_default().to_vec();
551        *self = input.get(end + 1..).unwrap_or_default().iter();
552        Ok(ra)
553    }
554}
555
556fn parse_ip6(input: &[u8]) -> (crate::Result<(Ipv6Addr, u8)>, usize) {
557    let mut pos = 0;
558    let mut stop_char = b' ';
559    let mut ip = [0u16; 8];
560    let mut ip_pos = 0;
561    let mut ip4_pos = 0;
562    let mut part = Ip6Part::default();
563    let mut zero_group_pos = usize::MAX;
564
565    while let Some(&ch) = input.get(pos) {
566        pos += 1;
567        match ch {
568            b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => {
569                if !part.push(ch) {
570                    return (Err(Error::Parse), pos);
571                }
572            }
573            b':' => {
574                if ip_pos < 8 {
575                    if !part.is_empty() {
576                        ip[ip_pos] = part.take_hex();
577                        ip_pos += 1;
578                    } else if zero_group_pos == usize::MAX {
579                        zero_group_pos = ip_pos;
580                    } else if zero_group_pos != ip_pos {
581                        return (Err(Error::Parse), pos);
582                    }
583                } else {
584                    return (Err(Error::Parse), pos);
585                }
586            }
587            b'.' => {
588                if ip_pos < 8 && !part.is_empty() {
589                    let Some(qnum) = part.take_octet() else {
590                        return (Err(Error::Parse), pos);
591                    };
592                    if ip4_pos % 2 == 1 {
593                        ip[ip_pos] = (ip[ip_pos] << 8) | qnum;
594                        ip_pos += 1;
595                    } else {
596                        ip[ip_pos] = qnum;
597                    }
598                    ip4_pos += 1;
599                } else {
600                    return (Err(Error::Parse), pos);
601                }
602            }
603            _ => {
604                stop_char = if ch.is_ascii_whitespace() { b' ' } else { ch };
605                break;
606            }
607        }
608    }
609
610    if !part.is_empty() {
611        if ip_pos < 8 {
612            ip[ip_pos] = if ip4_pos == 0 {
613                part.take_hex()
614            } else if ip4_pos == 3 {
615                match part.take_octet() {
616                    Some(qnum) => (ip[ip_pos] << 8) | qnum,
617                    None => return (Err(Error::Parse), pos),
618                }
619            } else {
620                return (Err(Error::Parse), pos);
621            };
622
623            ip_pos += 1;
624        } else {
625            return (Err(Error::Parse), pos);
626        }
627    }
628    if zero_group_pos != usize::MAX && zero_group_pos < ip_pos {
629        if ip_pos <= 7 {
630            ip.copy_within(zero_group_pos..ip_pos, zero_group_pos + 8 - ip_pos);
631            ip[zero_group_pos..zero_group_pos + 8 - ip_pos].fill(0);
632        } else {
633            return (Err(Error::Parse), pos);
634        }
635    }
636
637    if ip_pos != 0 || zero_group_pos != usize::MAX {
638        let [a, b, c, d, e, f, g, h] = ip;
639        (Ok((Ipv6Addr::new(a, b, c, d, e, f, g, h), stop_char)), pos)
640    } else {
641        (Err(Error::Parse), pos)
642    }
643}
644
645struct Ip6Part {
646    hex: u16,
647    decimal: u16,
648    len: u8,
649    is_decimal: bool,
650}
651
652impl Default for Ip6Part {
653    fn default() -> Self {
654        Ip6Part {
655            hex: 0,
656            decimal: 0,
657            len: 0,
658            is_decimal: true,
659        }
660    }
661}
662
663impl Ip6Part {
664    #[inline(always)]
665    fn push(&mut self, ch: u8) -> bool {
666        if self.len < 4 {
667            self.len += 1;
668            self.hex = (self.hex << 4) | HEX_NIBBLE[ch as usize] as u16;
669            if ch.is_ascii_digit() {
670                self.decimal = self.decimal * 10 + (ch - b'0') as u16;
671            } else {
672                self.is_decimal = false;
673            }
674            true
675        } else {
676            false
677        }
678    }
679
680    #[inline(always)]
681    fn is_empty(&self) -> bool {
682        self.len == 0
683    }
684
685    #[inline(always)]
686    fn take_hex(&mut self) -> u16 {
687        let value = self.hex;
688        *self = Ip6Part::default();
689        value
690    }
691
692    #[inline(always)]
693    fn take_octet(&mut self) -> Option<u16> {
694        let value = (self.is_decimal && self.decimal <= 255).then_some(self.decimal);
695        *self = Ip6Part::default();
696        value
697    }
698}
699
700const HEX_NIBBLE: [u8; 256] = {
701    let mut table = [0u8; 256];
702    let mut ch = 0usize;
703    while ch < 256 {
704        table[ch] = match ch as u8 {
705            b'0'..=b'9' => (ch as u8) - b'0',
706            b'a'..=b'f' => (ch as u8) - b'a' + 10,
707            b'A'..=b'F' => (ch as u8) - b'A' + 10,
708            _ => 0,
709        };
710        ch += 1;
711    }
712    table
713};
714
715#[inline(always)]
716fn literal_macro(literal: &[u8], stop_char: u8) -> crate::Result<(Macro, u8)> {
717    if literal.is_empty() {
718        Err(Error::Parse)
719    } else {
720        Ok((Macro::Literal(literal.into()), stop_char))
721    }
722}
723
724trait SPFMacroParser {
725    fn macro_string_slow(&mut self, is_exp: bool) -> crate::Result<(Macro, u8)>;
726}
727
728impl SPFMacroParser for Iter<'_, u8> {
729    #[inline(never)]
730    #[allow(clippy::while_let_on_iterator)]
731    fn macro_string_slow(&mut self, is_exp: bool) -> crate::Result<(Macro, u8)> {
732        let mut stop_char = b' ';
733        let mut last_is_pct = false;
734        let mut literal = Vec::with_capacity(16);
735        let mut macro_string = Vec::new();
736
737        while let Some(&ch) = self.next() {
738            match ch {
739                b'%' => {
740                    if last_is_pct {
741                        literal.push(b'%');
742                    } else {
743                        last_is_pct = true;
744                        continue;
745                    }
746                }
747                b'_' if last_is_pct => {
748                    literal.push(b' ');
749                }
750                b'-' if last_is_pct => {
751                    literal.extend_from_slice(b"%20");
752                }
753                b'{' if last_is_pct => {
754                    if !literal.is_empty() {
755                        macro_string.push(Macro::Literal(literal.as_slice().into()));
756                        literal.clear();
757                    }
758
759                    let (letter, escape) = self
760                        .next()
761                        .copied()
762                        .and_then(|l| {
763                            if !is_exp {
764                                Variable::parse(l)
765                            } else {
766                                Variable::parse_exp(l)
767                            }
768                        })
769                        .ok_or(Error::Parse)?;
770                    let mut num_parts: u32 = 0;
771                    let mut reverse = false;
772                    let mut delimiters = 0;
773
774                    while let Some(&ch) = self.next() {
775                        match ch {
776                            b'0'..=b'9' => {
777                                num_parts = num_parts
778                                    .saturating_mul(10)
779                                    .saturating_add((ch - b'0') as u32);
780                            }
781                            b'r' | b'R' => {
782                                reverse = true;
783                            }
784                            b'}' => {
785                                break;
786                            }
787                            b'.' | b'-' | b'+' | b',' | b'/' | b'_' | b'=' => {
788                                delimiters |= 1u64 << (ch - b'+');
789                            }
790                            _ => {
791                                return Err(Error::Parse);
792                            }
793                        }
794                    }
795
796                    if delimiters == 0 {
797                        delimiters = 1u64 << (b'.' - b'+');
798                    }
799
800                    macro_string.push(Macro::Variable {
801                        letter,
802                        num_parts,
803                        reverse,
804                        escape,
805                        delimiters,
806                    });
807                }
808                b'/' if !is_exp => {
809                    stop_char = ch;
810                    break;
811                }
812                _ => {
813                    if last_is_pct {
814                        return Err(Error::Parse);
815                    } else if !ch.is_ascii_whitespace() || is_exp {
816                        literal.push(ch);
817                    } else {
818                        break;
819                    }
820                }
821            }
822
823            last_is_pct = false;
824        }
825
826        if !literal.is_empty() {
827            macro_string.push(Macro::Literal(literal.into_boxed_slice()));
828        }
829
830        match macro_string.len() {
831            1 => macro_string
832                .pop()
833                .map(|m| (m, stop_char))
834                .ok_or(Error::Parse),
835            0 => Err(Error::Parse),
836            _ => Ok((Macro::List(macro_string.into_boxed_slice()), stop_char)),
837        }
838    }
839}
840
841impl Variable {
842    pub(crate) fn parse(ch: u8) -> Option<(Self, bool)> {
843        match ch {
844            b's' => (Variable::Sender, false),
845            b'l' => (Variable::SenderLocalPart, false),
846            b'o' => (Variable::SenderDomainPart, false),
847            b'd' => (Variable::Domain, false),
848            b'i' => (Variable::Ip, false),
849            b'p' => (Variable::ValidatedDomain, false),
850            b'v' => (Variable::IpVersion, false),
851            b'h' => (Variable::HeloDomain, false),
852
853            b'S' => (Variable::Sender, true),
854            b'L' => (Variable::SenderLocalPart, true),
855            b'O' => (Variable::SenderDomainPart, true),
856            b'D' => (Variable::Domain, true),
857            b'I' => (Variable::Ip, true),
858            b'P' => (Variable::ValidatedDomain, true),
859            b'V' => (Variable::IpVersion, true),
860            b'H' => (Variable::HeloDomain, true),
861            _ => return None,
862        }
863        .into()
864    }
865
866    pub(crate) fn parse_exp(ch: u8) -> Option<(Self, bool)> {
867        match ch {
868            b's' => (Variable::Sender, false),
869            b'l' => (Variable::SenderLocalPart, false),
870            b'o' => (Variable::SenderDomainPart, false),
871            b'd' => (Variable::Domain, false),
872            b'i' => (Variable::Ip, false),
873            b'p' => (Variable::ValidatedDomain, false),
874            b'v' => (Variable::IpVersion, false),
875            b'h' => (Variable::HeloDomain, false),
876            b'c' => (Variable::SmtpIp, false),
877            b'r' => (Variable::HostDomain, false),
878            b't' => (Variable::CurrentTime, false),
879
880            b'S' => (Variable::Sender, true),
881            b'L' => (Variable::SenderLocalPart, true),
882            b'O' => (Variable::SenderDomainPart, true),
883            b'D' => (Variable::Domain, true),
884            b'I' => (Variable::Ip, true),
885            b'P' => (Variable::ValidatedDomain, true),
886            b'V' => (Variable::IpVersion, true),
887            b'H' => (Variable::HeloDomain, true),
888            b'C' => (Variable::SmtpIp, true),
889            b'R' => (Variable::HostDomain, true),
890            b'T' => (Variable::CurrentTime, true),
891            _ => return None,
892        }
893        .into()
894    }
895}
896
897impl TxtRecordParser for Macro {
898    fn parse(record: &[u8]) -> crate::Result<Self> {
899        record.iter().macro_string(true).map(|(m, _)| m)
900    }
901}
902
903#[cfg(test)]
904mod tests;