smtp_codec/parse/
mod.rs

1#![allow(non_snake_case)]
2
3use std::{borrow::Cow, str::from_utf8};
4
5use abnf_core::streaming::{is_ALPHA, is_DIGIT, DQUOTE};
6use nom::{
7    branch::alt,
8    bytes::streaming::{tag, take_while, take_while1, take_while_m_n},
9    character::streaming::digit1,
10    combinator::{map, map_res, opt, recognize},
11    multi::{many0, separated_list1},
12    sequence::{delimited, tuple},
13    IResult,
14};
15
16use crate::{parse::imf::atom::is_atext, types::AtomOrQuoted, utils::unescape_quoted};
17
18pub mod address;
19pub mod command;
20pub mod imf;
21pub mod response;
22pub mod trace;
23pub mod utils;
24
25pub fn base64(input: &[u8]) -> IResult<&[u8], &str> {
26    let mut parser = map_res(
27        recognize(tuple((
28            take_while(is_base64_char),
29            opt(alt((tag("=="), tag("=")))),
30        ))),
31        from_utf8,
32    );
33
34    let (remaining, base64) = parser(input)?;
35
36    Ok((remaining, base64))
37}
38
39fn is_base64_char(i: u8) -> bool {
40    is_ALPHA(i) || is_DIGIT(i) || i == b'+' || i == b'/'
41}
42
43pub fn number(input: &[u8]) -> IResult<&[u8], u32> {
44    map_res(map_res(digit1, from_utf8), str::parse::<u32>)(input) // FIXME(perf): use from_utf8_unchecked
45}
46
47// -------------------------------------------------------------------------------------------------
48
49/// String = Atom / Quoted-string
50pub fn String(input: &[u8]) -> IResult<&[u8], AtomOrQuoted> {
51    alt((
52        map(Atom, |atom| AtomOrQuoted::Atom(atom.into())),
53        map(Quoted_string, |quoted| AtomOrQuoted::Quoted(quoted.into())),
54    ))(input)
55}
56
57/// Atom = 1*atext
58pub fn Atom(input: &[u8]) -> IResult<&[u8], &str> {
59    map_res(take_while1(is_atext), std::str::from_utf8)(input)
60}
61
62/// Quoted-string = DQUOTE *QcontentSMTP DQUOTE
63pub fn Quoted_string(input: &[u8]) -> IResult<&[u8], Cow<'_, str>> {
64    map(
65        delimited(
66            DQUOTE,
67            map_res(recognize(many0(QcontentSMTP)), std::str::from_utf8),
68            DQUOTE,
69        ),
70        |s| unescape_quoted(s),
71    )(input)
72}
73
74/// QcontentSMTP = qtextSMTP / quoted-pairSMTP
75pub fn QcontentSMTP(input: &[u8]) -> IResult<&[u8], &[u8]> {
76    let parser = alt((take_while_m_n(1, 1, is_qtextSMTP), quoted_pairSMTP));
77
78    let (remaining, parsed) = recognize(parser)(input)?;
79
80    Ok((remaining, parsed))
81}
82
83/// Within a quoted string, any ASCII graphic or space is permitted
84/// without blackslash-quoting except double-quote and the backslash itself.
85///
86/// qtextSMTP = %d32-33 / %d35-91 / %d93-126
87pub fn is_qtextSMTP(byte: u8) -> bool {
88    matches!(byte, 32..=33 | 35..=91 | 93..=126)
89}
90
91/// Backslash followed by any ASCII graphic (including itself) or SPace
92///
93/// quoted-pairSMTP = %d92 %d32-126
94///
95/// FIXME: How should e.g. "\a" be interpreted?
96pub fn quoted_pairSMTP(input: &[u8]) -> IResult<&[u8], &[u8]> {
97    //fn is_value(byte: u8) -> bool {
98    //    matches!(byte, 32..=126)
99    //}
100
101    // FIXME: Only allow "\\" and "\"" for now ...
102    fn is_value(byte: u8) -> bool {
103        byte == b'\\' || byte == b'\"'
104    }
105
106    let parser = tuple((tag("\\"), take_while_m_n(1, 1, is_value)));
107
108    let (remaining, parsed) = recognize(parser)(input)?;
109
110    Ok((remaining, parsed))
111}
112
113// -------------------------------------------------------------------------------------------------
114
115/// Domain = sub-domain *("." sub-domain)
116pub fn Domain(input: &[u8]) -> IResult<&[u8], &str> {
117    let parser = separated_list1(tag(b"."), sub_domain);
118
119    let (remaining, parsed) = map_res(recognize(parser), std::str::from_utf8)(input)?;
120
121    Ok((remaining, parsed))
122}
123
124/// sub-domain = Let-dig [Ldh-str]
125pub fn sub_domain(input: &[u8]) -> IResult<&[u8], &[u8]> {
126    let parser = tuple((take_while_m_n(1, 1, is_Let_dig), opt(Ldh_str)));
127
128    let (remaining, parsed) = recognize(parser)(input)?;
129
130    Ok((remaining, parsed))
131}
132
133/// Let-dig = ALPHA / DIGIT
134pub fn is_Let_dig(byte: u8) -> bool {
135    is_ALPHA(byte) || is_DIGIT(byte)
136}
137
138/// Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig
139pub fn Ldh_str(input: &[u8]) -> IResult<&[u8], &[u8]> {
140    let parser = many0(alt((
141        take_while_m_n(1, 1, is_ALPHA),
142        take_while_m_n(1, 1, is_DIGIT),
143        recognize(tuple((tag(b"-"), take_while_m_n(1, 1, is_Let_dig)))),
144    )));
145
146    let (remaining, parsed) = recognize(parser)(input)?;
147
148    Ok((remaining, parsed))
149}
150
151// -------------------------------------------------------------------------------------------------
152
153#[cfg(test)]
154pub mod test {
155    use super::sub_domain;
156
157    #[test]
158    fn test_subdomain() {
159        let (rem, parsed) = sub_domain(b"example???").unwrap();
160        assert_eq!(parsed, b"example");
161        assert_eq!(rem, b"???");
162    }
163}