smtp_codec/parse/
response.rs

1use abnf_core::streaming::{is_ALPHA, is_DIGIT, CRLF, SP};
2use nom::{
3    branch::alt,
4    bytes::streaming::{tag, tag_no_case, take_while, take_while1, take_while_m_n},
5    combinator::{map, map_res, opt, recognize, value},
6    multi::{many0, separated_list0},
7    sequence::{delimited, preceded, tuple},
8    IResult,
9};
10
11use crate::{
12    parse::{address::address_literal, number, Domain},
13    types::{AuthMechanism, Capability, Response},
14};
15
16/// Greeting = ( "220 " (Domain / address-literal) [ SP textstring ] CRLF ) /
17///            ( "220-" (Domain / address-literal) [ SP textstring ] CRLF
18///           *( "220-" [ textstring ] CRLF )
19///              "220" [ SP textstring ] CRLF )
20pub fn Greeting(input: &[u8]) -> IResult<&[u8], Response> {
21    let mut parser = alt((
22        map(
23            tuple((
24                tag(b"220 "),
25                alt((Domain, address_literal)),
26                opt(preceded(SP, textstring)),
27                CRLF,
28            )),
29            |(_, domain, maybe_text, _)| Response::Greeting {
30                domain: domain.to_owned(),
31                text: maybe_text
32                    .map(|str| str.to_string())
33                    .unwrap_or_else(|| "".to_string()),
34            },
35        ),
36        map(
37            tuple((
38                tag(b"220-"),
39                alt((Domain, address_literal)),
40                opt(preceded(SP, textstring)),
41                CRLF,
42                many0(delimited(tag(b"220-"), opt(textstring), CRLF)),
43                tag(b"220"),
44                opt(preceded(SP, textstring)),
45                CRLF,
46            )),
47            |(_, domain, maybe_text, _, more_text, _, moar_text, _)| Response::Greeting {
48                domain: domain.to_owned(),
49                text: {
50                    let mut res = maybe_text
51                        .map(|str| format!("{}\n", str))
52                        .unwrap_or_else(|| "\n".to_string());
53
54                    for text in more_text {
55                        let text = text
56                            .map(|str| format!("{}\n", str))
57                            .unwrap_or_else(|| "\n".to_string());
58                        res.push_str(&text);
59                    }
60
61                    let text = moar_text
62                        .map(|str| str.to_string())
63                        .unwrap_or_else(|| "".to_string());
64                    res.push_str(&text);
65
66                    res
67                },
68            },
69        ),
70    ));
71
72    let (remaining, parsed) = parser(input)?;
73
74    Ok((remaining, parsed))
75}
76
77/// HT, SP, Printable US-ASCII
78///
79/// textstring = 1*(%d09 / %d32-126)
80pub fn textstring(input: &[u8]) -> IResult<&[u8], &str> {
81    fn is_value(byte: u8) -> bool {
82        matches!(byte, 9 | 32..=126)
83    }
84
85    let (remaining, parsed) = map_res(take_while1(is_value), std::str::from_utf8)(input)?;
86
87    Ok((remaining, parsed))
88}
89
90// -------------------------------------------------------------------------------------------------
91
92/// Reply-line = *( Reply-code "-" [ textstring ] CRLF )
93///                 Reply-code [ SP textstring ] CRLF
94pub fn Reply_line(input: &[u8]) -> IResult<&[u8], &[u8]> {
95    let parser = tuple((
96        many0(tuple((Reply_code, tag(b"-"), opt(textstring), CRLF))),
97        Reply_code,
98        opt(tuple((SP, textstring))),
99        CRLF,
100    ));
101
102    let (remaining, parsed) = recognize(parser)(input)?;
103
104    Ok((remaining, parsed))
105}
106
107/// Reply-code = %x32-35 %x30-35 %x30-39
108///
109///   2345
110/// 012345
111/// 0123456789
112pub fn Reply_code(input: &[u8]) -> IResult<&[u8], u16> {
113    // FIXME: do not accept all codes.
114    map_res(
115        map_res(
116            take_while_m_n(3, 3, nom::character::is_digit),
117            std::str::from_utf8,
118        ),
119        |s| u16::from_str_radix(s, 10),
120    )(input)
121}
122
123// -------------------------------------------------------------------------------------------------
124
125/// ehlo-ok-rsp = ( "250 " Domain [ SP ehlo-greet ] CRLF ) /
126///               ( "250-" Domain [ SP ehlo-greet ] CRLF
127///              *( "250-" ehlo-line CRLF )
128///                 "250 " ehlo-line CRLF )
129///
130/// Edit: collapsed ("250" SP) to ("250 ")
131pub fn ehlo_ok_rsp(input: &[u8]) -> IResult<&[u8], Response> {
132    let mut parser = alt((
133        map(
134            tuple((tag(b"250 "), Domain, opt(preceded(SP, ehlo_greet)), CRLF)),
135            |(_, domain, maybe_ehlo, _)| Response::Ehlo {
136                domain: domain.to_owned(),
137                greet: maybe_ehlo.map(|ehlo| ehlo.to_owned()),
138                capabilities: Vec::new(),
139            },
140        ),
141        map(
142            tuple((
143                tag(b"250-"),
144                Domain,
145                opt(preceded(SP, ehlo_greet)),
146                CRLF,
147                many0(delimited(tag(b"250-"), ehlo_line, CRLF)),
148                tag(b"250 "),
149                ehlo_line,
150                CRLF,
151            )),
152            |(_, domain, maybe_ehlo, _, mut lines, _, line, _)| Response::Ehlo {
153                domain: domain.to_owned(),
154                greet: maybe_ehlo.map(|ehlo| ehlo.to_owned()),
155                capabilities: {
156                    lines.push(line);
157                    lines
158                },
159            },
160        ),
161    ));
162
163    let (remaining, parsed) = parser(input)?;
164
165    Ok((remaining, parsed))
166}
167
168/// String of any characters other than CR or LF.
169///
170/// ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127)
171pub fn ehlo_greet(input: &[u8]) -> IResult<&[u8], &str> {
172    fn is_valid_character(byte: u8) -> bool {
173        matches!(byte, 0..=9 | 11..=12 | 14..=127)
174    }
175
176    map_res(take_while1(is_valid_character), std::str::from_utf8)(input)
177}
178
179/// ehlo-line = ehlo-keyword *( SP ehlo-param )
180///
181/// TODO: SMTP servers often respond with "AUTH=LOGIN PLAIN". Why?
182pub fn ehlo_line(input: &[u8]) -> IResult<&[u8], Capability> {
183    let auth = tuple((
184        tag_no_case("AUTH"),
185        alt((tag_no_case(" "), tag_no_case("="))),
186        separated_list0(SP, auth_mechanism),
187    ));
188
189    let other = tuple((
190        map_res(ehlo_keyword, std::str::from_utf8),
191        opt(preceded(
192            alt((SP, tag("="))), // TODO: For Outlook?
193            separated_list0(SP, ehlo_param),
194        )),
195    ));
196
197    alt((
198        value(Capability::EXPN, tag_no_case("EXPN")),
199        value(Capability::Help, tag_no_case("HELP")),
200        value(Capability::EightBitMIME, tag_no_case("8BITMIME")),
201        map(preceded(tag_no_case("SIZE "), number), Capability::Size),
202        value(Capability::Chunking, tag_no_case("CHUNKING")),
203        value(Capability::BinaryMIME, tag_no_case("BINARYMIME")),
204        value(Capability::Checkpoint, tag_no_case("CHECKPOINT")),
205        value(Capability::DeliverBy, tag_no_case("DELIVERBY")),
206        value(Capability::Pipelining, tag_no_case("PIPELINING")),
207        value(Capability::DSN, tag_no_case("DSN")),
208        value(Capability::ETRN, tag_no_case("ETRN")),
209        value(
210            Capability::EnhancedStatusCodes,
211            tag_no_case("ENHANCEDSTATUSCODES"),
212        ),
213        value(Capability::StartTLS, tag_no_case("STARTTLS")),
214        // FIXME: NO-SOLICITING
215        value(Capability::MTRK, tag_no_case("MTRK")),
216        value(Capability::ATRN, tag_no_case("ATRN")),
217        map(auth, |(_, _, mechanisms)| Capability::Auth(mechanisms)),
218        value(Capability::BURL, tag_no_case("BURL")),
219        // FIXME: FUTURERELEASE
220        // FIXME: CONPERM
221        // FIXME: CONNEG
222        value(Capability::SMTPUTF8, tag_no_case("SMTPUTF8")),
223        // FIXME: MT-PRIORITY
224        value(Capability::RRVS, tag_no_case("RRVS")),
225        value(Capability::RequireTLS, tag_no_case("REQUIRETLS")),
226        map(other, |(keyword, params)| Capability::Other {
227            keyword: keyword.into(),
228            params: params
229                .map(|v| v.iter().map(|s| s.to_string()).collect())
230                .unwrap_or_default(),
231        }),
232    ))(input)
233}
234
235/// Additional syntax of ehlo-params depends on ehlo-keyword
236///
237/// ehlo-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
238pub fn ehlo_keyword(input: &[u8]) -> IResult<&[u8], &[u8]> {
239    let parser = tuple((
240        take_while_m_n(1, 1, |byte| is_ALPHA(byte) || is_DIGIT(byte)),
241        take_while(|byte| is_ALPHA(byte) || is_DIGIT(byte) || byte == b'-'),
242    ));
243
244    let (remaining, parsed) = recognize(parser)(input)?;
245
246    Ok((remaining, parsed))
247}
248
249/// Any CHAR excluding <SP> and all control characters
250/// (US-ASCII 0-31 and 127 inclusive)
251///
252/// ehlo-param = 1*(%d33-126)
253pub fn ehlo_param(input: &[u8]) -> IResult<&[u8], &str> {
254    fn is_valid_character(byte: u8) -> bool {
255        matches!(byte, 33..=126)
256    }
257
258    map_res(take_while1(is_valid_character), std::str::from_utf8)(input)
259}
260
261pub fn auth_mechanism(input: &[u8]) -> IResult<&[u8], AuthMechanism> {
262    alt((
263        value(AuthMechanism::Login, tag_no_case("LOGIN")),
264        value(AuthMechanism::Plain, tag_no_case("PLAIN")),
265        value(AuthMechanism::CramMD5, tag_no_case("CRAM-MD5")),
266        value(AuthMechanism::CramSHA1, tag_no_case("CRAM-SHA1")),
267        value(AuthMechanism::DigestMD5, tag_no_case("DIGEST-MD5")),
268        value(AuthMechanism::ScramMD5, tag_no_case("SCRAM-MD5")),
269        value(AuthMechanism::GSSAPI, tag_no_case("GSSAPI")),
270        value(AuthMechanism::NTLM, tag_no_case("NTLM")),
271        map(ehlo_param, |param| AuthMechanism::Other(param.to_string())),
272    ))(input)
273}
274
275// -------------------------------------------------------------------------------------------------
276
277#[cfg(test)]
278mod test {
279    use super::*;
280    use crate::types::AuthMechanism;
281
282    #[test]
283    fn test_Greeting() {
284        let greeting = b"220-example.org ESMTP Fake 4.93 #2 Thu, 16 Jul 2020 07:30:16 -0400\r\n\
285220-We do not authorize the use of this system to transport unsolicited,\r\n\
286220 and/or bulk e-mail.\r\n";
287
288        let (rem, out) = Greeting(greeting).unwrap();
289        assert_eq!(rem, b"");
290        assert_eq!(
291            out,
292            Response::Greeting {
293                domain: "example.org".into(),
294                text: "ESMTP Fake 4.93 #2 Thu, 16 Jul 2020 07:30:16 -0400\n\
295We do not authorize the use of this system to transport unsolicited,\n\
296and/or bulk e-mail."
297                    .into(),
298            }
299        )
300    }
301
302    #[test]
303    fn test_ehlo_ok_rsp() {
304        let (rem, out) = ehlo_ok_rsp(
305            b"250-example.org hello\r\n\
306250-AUTH LOGIN CRAM-MD5 PLAIN\r\n\
307250-AUTH=LOGIN CRAM-MD5 PLAIN\r\n\
308250-STARTTLS\r\n\
309250-SIZE 12345\r\n\
310250 8BITMIME\r\n",
311        )
312        .unwrap();
313        assert_eq!(rem, b"");
314        assert_eq!(
315            out,
316            Response::Ehlo {
317                domain: "example.org".into(),
318                greet: Some("hello".into()),
319                capabilities: vec![
320                    Capability::Auth(vec![
321                        AuthMechanism::Login,
322                        AuthMechanism::CramMD5,
323                        AuthMechanism::Plain
324                    ]),
325                    Capability::Auth(vec![
326                        AuthMechanism::Login,
327                        AuthMechanism::CramMD5,
328                        AuthMechanism::Plain
329                    ]),
330                    Capability::StartTLS,
331                    Capability::Size(12345),
332                    Capability::EightBitMIME,
333                ],
334            }
335        );
336    }
337
338    #[test]
339    fn test_ehlo_line() {
340        let (rem, capability) = ehlo_line(b"SIZE 123456\r\n").unwrap();
341        assert_eq!(rem, b"\r\n");
342        assert_eq!(capability, Capability::Size(123456));
343    }
344}