smtp_codec/parse/
command.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::separated_list1,
7    sequence::{delimited, preceded, tuple},
8    IResult,
9};
10
11use crate::{
12    parse::{address::address_literal, base64, Atom, Domain, Quoted_string, String},
13    types::{Command, DomainOrAddress, Parameter},
14};
15
16pub fn command(input: &[u8]) -> IResult<&[u8], Command> {
17    alt((
18        helo, ehlo, mail, rcpt, data, rset, vrfy, expn, help, noop, quit,
19        starttls,   // Extensions
20        auth_login, // https://interoperability.blob.core.windows.net/files/MS-XLOGIN/[MS-XLOGIN].pdf
21        auth_plain, // RFC 4616
22    ))(input)
23}
24
25/// helo = "HELO" SP Domain CRLF
26pub fn helo(input: &[u8]) -> IResult<&[u8], Command> {
27    let mut parser = tuple((
28        tag_no_case(b"HELO"),
29        SP,
30        alt((
31            map(Domain, |domain| DomainOrAddress::Domain(domain.into())),
32            map(address_literal, |address| {
33                DomainOrAddress::Address(address.into())
34            }),
35        )),
36        CRLF,
37    ));
38
39    let (remaining, (_, _, domain_or_address, _)) = parser(input)?;
40
41    Ok((remaining, Command::Helo { domain_or_address }))
42}
43
44/// ehlo = "EHLO" SP ( Domain / address-literal ) CRLF
45pub fn ehlo(input: &[u8]) -> IResult<&[u8], Command> {
46    let mut parser = tuple((
47        tag_no_case(b"EHLO"),
48        SP,
49        alt((
50            map(Domain, |domain| DomainOrAddress::Domain(domain.into())),
51            map(address_literal, |address| {
52                DomainOrAddress::Address(address.into())
53            }),
54        )),
55        CRLF,
56    ));
57
58    let (remaining, (_, _, domain_or_address, _)) = parser(input)?;
59
60    Ok((remaining, Command::Ehlo { domain_or_address }))
61}
62
63/// mail = "MAIL FROM:" Reverse-path [SP Mail-parameters] CRLF
64pub fn mail(input: &[u8]) -> IResult<&[u8], Command> {
65    let mut parser = tuple((
66        tag_no_case(b"MAIL FROM:"),
67        opt(SP), // Out-of-Spec, but Outlook does it ...
68        Reverse_path,
69        opt(preceded(SP, Mail_parameters)),
70        CRLF,
71    ));
72
73    let (remaining, (_, _, data, maybe_params, _)) = parser(input)?;
74
75    Ok((
76        remaining,
77        Command::Mail {
78            reverse_path: data.into(),
79            parameters: maybe_params.unwrap_or_default(),
80        },
81    ))
82}
83
84/// Mail-parameters = esmtp-param *(SP esmtp-param)
85pub fn Mail_parameters(input: &[u8]) -> IResult<&[u8], Vec<Parameter>> {
86    separated_list1(SP, esmtp_param)(input)
87}
88
89/// esmtp-param = esmtp-keyword ["=" esmtp-value]
90pub fn esmtp_param(input: &[u8]) -> IResult<&[u8], Parameter> {
91    map(
92        tuple((esmtp_keyword, opt(preceded(tag(b"="), esmtp_value)))),
93        |(keyword, value)| Parameter::new(keyword, value),
94    )(input)
95}
96
97/// esmtp-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
98pub fn esmtp_keyword(input: &[u8]) -> IResult<&[u8], &str> {
99    let parser = tuple((
100        take_while_m_n(1, 1, |byte| is_ALPHA(byte) || is_DIGIT(byte)),
101        take_while(|byte| is_ALPHA(byte) || is_DIGIT(byte) || byte == b'-'),
102    ));
103
104    let (remaining, parsed) = map_res(recognize(parser), std::str::from_utf8)(input)?;
105
106    Ok((remaining, parsed))
107}
108
109/// Any CHAR excluding "=", SP, and control characters.
110/// If this string is an email address, i.e., a Mailbox,
111/// then the "xtext" syntax [32] SHOULD be used.
112///
113/// esmtp-value = 1*(%d33-60 / %d62-126)
114pub fn esmtp_value(input: &[u8]) -> IResult<&[u8], &str> {
115    fn is_value_character(byte: u8) -> bool {
116        matches!(byte, 33..=60 | 62..=126)
117    }
118
119    map_res(take_while1(is_value_character), std::str::from_utf8)(input)
120}
121
122/// rcpt = "RCPT TO:" ( "<Postmaster@" Domain ">" / "<Postmaster>" / Forward-path ) [SP Rcpt-parameters] CRLF
123///
124/// Note that, in a departure from the usual rules for
125/// local-parts, the "Postmaster" string shown above is
126/// treated as case-insensitive.
127pub fn rcpt(input: &[u8]) -> IResult<&[u8], Command> {
128    let mut parser = tuple((
129        tag_no_case(b"RCPT TO:"),
130        opt(SP), // Out-of-Spec, but Outlook does it ...
131        alt((
132            map_res(
133                recognize(tuple((tag_no_case("<Postmaster@"), Domain, tag(">")))),
134                std::str::from_utf8,
135            ),
136            map_res(tag_no_case("<Postmaster>"), std::str::from_utf8),
137            Forward_path,
138        )),
139        opt(preceded(SP, Rcpt_parameters)),
140        CRLF,
141    ));
142
143    let (remaining, (_, _, data, maybe_params, _)) = parser(input)?;
144
145    Ok((
146        remaining,
147        Command::Rcpt {
148            forward_path: data.into(),
149            parameters: maybe_params.unwrap_or_default(),
150        },
151    ))
152}
153
154/// Rcpt-parameters = esmtp-param *(SP esmtp-param)
155pub fn Rcpt_parameters(input: &[u8]) -> IResult<&[u8], Vec<Parameter>> {
156    separated_list1(SP, esmtp_param)(input)
157}
158
159/// data = "DATA" CRLF
160pub fn data(input: &[u8]) -> IResult<&[u8], Command> {
161    value(Command::Data, tuple((tag_no_case(b"DATA"), CRLF)))(input)
162}
163
164/// rset = "RSET" CRLF
165pub fn rset(input: &[u8]) -> IResult<&[u8], Command> {
166    value(Command::Rset, tuple((tag_no_case(b"RSET"), CRLF)))(input)
167}
168
169/// vrfy = "VRFY" SP String CRLF
170pub fn vrfy(input: &[u8]) -> IResult<&[u8], Command> {
171    let mut parser = tuple((tag_no_case(b"VRFY"), SP, String, CRLF));
172
173    let (remaining, (_, _, data, _)) = parser(input)?;
174
175    Ok((
176        remaining,
177        Command::Vrfy {
178            user_or_mailbox: data,
179        },
180    ))
181}
182
183/// expn = "EXPN" SP String CRLF
184pub fn expn(input: &[u8]) -> IResult<&[u8], Command> {
185    let mut parser = tuple((tag_no_case(b"EXPN"), SP, String, CRLF));
186
187    let (remaining, (_, _, data, _)) = parser(input)?;
188
189    Ok((remaining, Command::Expn { mailing_list: data }))
190}
191
192/// help = "HELP" [ SP String ] CRLF
193pub fn help(input: &[u8]) -> IResult<&[u8], Command> {
194    let mut parser = tuple((tag_no_case(b"HELP"), opt(preceded(SP, String)), CRLF));
195
196    let (remaining, (_, maybe_data, _)) = parser(input)?;
197
198    Ok((
199        remaining,
200        Command::Help {
201            argument: maybe_data,
202        },
203    ))
204}
205
206/// noop = "NOOP" [ SP String ] CRLF
207pub fn noop(input: &[u8]) -> IResult<&[u8], Command> {
208    let mut parser = tuple((tag_no_case(b"NOOP"), opt(preceded(SP, String)), CRLF));
209
210    let (remaining, (_, maybe_data, _)) = parser(input)?;
211
212    Ok((
213        remaining,
214        Command::Noop {
215            argument: maybe_data,
216        },
217    ))
218}
219
220/// quit = "QUIT" CRLF
221pub fn quit(input: &[u8]) -> IResult<&[u8], Command> {
222    value(Command::Quit, tuple((tag_no_case(b"QUIT"), CRLF)))(input)
223}
224
225pub fn starttls(input: &[u8]) -> IResult<&[u8], Command> {
226    value(Command::StartTLS, tuple((tag_no_case(b"STARTTLS"), CRLF)))(input)
227}
228
229/// https://interoperability.blob.core.windows.net/files/MS-XLOGIN/[MS-XLOGIN].pdf
230///
231/// username = 1*CHAR ; Base64-encoded username
232/// password = 1*CHAR ; Base64-encoded password
233///
234/// auth_login_command = "AUTH LOGIN" [SP username] CRLF
235///
236/// auth_login_username_challenge = "334 VXNlcm5hbWU6" CRLF
237/// auth_login_username_response  = username CRLF
238/// auth_login_password_challenge = "334 UGFzc3dvcmQ6" CRLF
239/// auth_login_password_response  = password CRLF
240pub fn auth_login(input: &[u8]) -> IResult<&[u8], Command> {
241    let mut parser = tuple((
242        tag_no_case(b"AUTH"),
243        SP,
244        tag_no_case("LOGIN"),
245        opt(preceded(SP, base64)),
246        CRLF,
247    ));
248
249    let (remaining, (_, _, _, maybe_username_b64, _)) = parser(input)?;
250
251    Ok((
252        remaining,
253        Command::AuthLogin(maybe_username_b64.map(|i| i.to_owned())),
254    ))
255}
256
257pub fn auth_plain(input: &[u8]) -> IResult<&[u8], Command> {
258    let mut parser = tuple((
259        tag_no_case(b"AUTH"),
260        SP,
261        tag_no_case("PLAIN"),
262        opt(preceded(SP, base64)),
263        CRLF,
264    ));
265
266    let (remaining, (_, _, _, maybe_credentials_b64, _)) = parser(input)?;
267
268    Ok((
269        remaining,
270        Command::AuthPlain(maybe_credentials_b64.map(|i| i.to_owned())),
271    ))
272}
273
274// ----- 4.1.2.  Command Argument Syntax (RFC 5321) -----
275
276/// Reverse-path = Path / "<>"
277pub fn Reverse_path(input: &[u8]) -> IResult<&[u8], &str> {
278    alt((Path, value("", tag("<>"))))(input)
279}
280
281/// Forward-path = Path
282pub fn Forward_path(input: &[u8]) -> IResult<&[u8], &str> {
283    Path(input)
284}
285
286// Path = "<" [ A-d-l ":" ] Mailbox ">"
287pub fn Path(input: &[u8]) -> IResult<&[u8], &str> {
288    delimited(
289        tag(b"<"),
290        map_res(
291            recognize(tuple((opt(tuple((A_d_l, tag(b":")))), Mailbox))),
292            std::str::from_utf8,
293        ),
294        tag(b">"),
295    )(input)
296}
297
298/// A-d-l = At-domain *( "," At-domain )
299///          ; Note that this form, the so-called "source
300///          ; route", MUST BE accepted, SHOULD NOT be
301///          ; generated, and SHOULD be ignored.
302pub fn A_d_l(input: &[u8]) -> IResult<&[u8], &[u8]> {
303    let parser = separated_list1(tag(b","), At_domain);
304
305    let (remaining, parsed) = recognize(parser)(input)?;
306
307    Ok((remaining, parsed))
308}
309
310/// At-domain = "@" Domain
311pub fn At_domain(input: &[u8]) -> IResult<&[u8], &[u8]> {
312    let parser = tuple((tag(b"@"), Domain));
313
314    let (remaining, parsed) = recognize(parser)(input)?;
315
316    Ok((remaining, parsed))
317}
318
319/// Mailbox = Local-part "@" ( Domain / address-literal )
320pub fn Mailbox(input: &[u8]) -> IResult<&[u8], &[u8]> {
321    let parser = tuple((Local_part, tag(b"@"), alt((Domain, address_literal))));
322
323    let (remaining, parsed) = recognize(parser)(input)?;
324
325    Ok((remaining, parsed))
326}
327
328/// Local-part = Dot-string / Quoted-string
329///               ; MAY be case-sensitive
330pub fn Local_part(input: &[u8]) -> IResult<&[u8], &[u8]> {
331    alt((recognize(Dot_string), recognize(Quoted_string)))(input)
332}
333
334/// Dot-string = Atom *("."  Atom)
335pub fn Dot_string(input: &[u8]) -> IResult<&[u8], &str> {
336    map_res(
337        recognize(separated_list1(tag(b"."), Atom)),
338        std::str::from_utf8,
339    )(input)
340}
341
342// Not used?
343/// Keyword = Ldh-str
344//pub fn Keyword(input: &[u8]) -> IResult<&[u8], &[u8]> {
345//    Ldh_str(input)
346//}
347
348// Not used?
349/// Argument = Atom
350//pub fn Argument(input: &[u8]) -> IResult<&[u8], &[u8]> {
351//    Atom(input)
352//}
353
354#[cfg(test)]
355mod test {
356    use super::{ehlo, helo, mail};
357    use crate::types::{Command, DomainOrAddress};
358
359    #[test]
360    fn test_ehlo() {
361        let (rem, parsed) = ehlo(b"EHLO [123.123.123.123]\r\n???").unwrap();
362        assert_eq!(
363            parsed,
364            Command::Ehlo {
365                domain_or_address: DomainOrAddress::Address("123.123.123.123".into()),
366            }
367        );
368        assert_eq!(rem, b"???");
369    }
370
371    #[test]
372    fn test_helo() {
373        let (rem, parsed) = helo(b"HELO example.com\r\n???").unwrap();
374        assert_eq!(
375            parsed,
376            Command::Helo {
377                domain_or_address: DomainOrAddress::Domain("example.com".into()),
378            }
379        );
380        assert_eq!(rem, b"???");
381    }
382
383    #[test]
384    fn test_mail() {
385        let (rem, parsed) = mail(b"MAIL FROM:<userx@y.foo.org>\r\n???").unwrap();
386        assert_eq!(
387            parsed,
388            Command::Mail {
389                reverse_path: "userx@y.foo.org".into(),
390                parameters: Vec::default(),
391            }
392        );
393        assert_eq!(rem, b"???");
394    }
395}