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, auth_login, auth_plain, ))(input)
23}
24
25pub 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
44pub 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
63pub fn mail(input: &[u8]) -> IResult<&[u8], Command> {
65 let mut parser = tuple((
66 tag_no_case(b"MAIL FROM:"),
67 opt(SP), 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
84pub fn Mail_parameters(input: &[u8]) -> IResult<&[u8], Vec<Parameter>> {
86 separated_list1(SP, esmtp_param)(input)
87}
88
89pub 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
97pub 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
109pub 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
122pub fn rcpt(input: &[u8]) -> IResult<&[u8], Command> {
128 let mut parser = tuple((
129 tag_no_case(b"RCPT TO:"),
130 opt(SP), 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
154pub fn Rcpt_parameters(input: &[u8]) -> IResult<&[u8], Vec<Parameter>> {
156 separated_list1(SP, esmtp_param)(input)
157}
158
159pub fn data(input: &[u8]) -> IResult<&[u8], Command> {
161 value(Command::Data, tuple((tag_no_case(b"DATA"), CRLF)))(input)
162}
163
164pub fn rset(input: &[u8]) -> IResult<&[u8], Command> {
166 value(Command::Rset, tuple((tag_no_case(b"RSET"), CRLF)))(input)
167}
168
169pub 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
183pub 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
192pub 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
206pub 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
220pub 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
229pub 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
274pub fn Reverse_path(input: &[u8]) -> IResult<&[u8], &str> {
278 alt((Path, value("", tag("<>"))))(input)
279}
280
281pub fn Forward_path(input: &[u8]) -> IResult<&[u8], &str> {
283 Path(input)
284}
285
286pub 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
298pub 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
310pub 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
319pub 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
328pub fn Local_part(input: &[u8]) -> IResult<&[u8], &[u8]> {
331 alt((recognize(Dot_string), recognize(Quoted_string)))(input)
332}
333
334pub 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#[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}