1use crate::command::{MailParam, SmtpCommand};
4use nom::{
5 branch::alt,
6 bytes::complete::{tag_no_case, take_while1},
7 character::complete::{char, space0, space1},
8 combinator::{map, opt, rest},
9 sequence::{delimited, preceded},
10 IResult, Parser,
11};
12use rusmes_proto::MailAddress;
13
14pub fn parse_command(input: &str) -> Result<SmtpCommand, String> {
16 let input = input.trim();
17
18 if let Ok((_, cmd)) = smtp_command(input) {
20 Ok(cmd)
21 } else {
22 Err(format!("Failed to parse command: {}", input))
23 }
24}
25
26fn smtp_command(input: &str) -> IResult<&str, SmtpCommand> {
28 alt((
29 helo_command,
30 ehlo_command,
31 mail_command,
32 rcpt_command,
33 data_command,
34 bdat_command,
35 rset_command,
36 noop_command,
37 quit_command,
38 vrfy_command,
39 expn_command,
40 help_command,
41 starttls_command,
42 auth_command,
43 ))
44 .parse(input)
45}
46
47fn helo_command(input: &str) -> IResult<&str, SmtpCommand> {
49 map(
50 preceded(tag_no_case("HELO"), preceded(space1, domain)),
51 SmtpCommand::Helo,
52 )
53 .parse(input)
54}
55
56fn ehlo_command(input: &str) -> IResult<&str, SmtpCommand> {
58 map(
59 preceded(tag_no_case("EHLO"), preceded(space1, domain)),
60 SmtpCommand::Ehlo,
61 )
62 .parse(input)
63}
64
65fn mail_command(input: &str) -> IResult<&str, SmtpCommand> {
67 let (input, _) = tag_no_case("MAIL FROM:").parse(input)?;
68 let (input, _) = space0(input)?;
69 let (input, from) = reverse_path(input)?;
70 let (input, params) = opt(preceded(space1, mail_parameters)).parse(input)?;
71
72 Ok((
73 input,
74 SmtpCommand::Mail {
75 from,
76 params: params.unwrap_or_default(),
77 },
78 ))
79}
80
81fn rcpt_command(input: &str) -> IResult<&str, SmtpCommand> {
83 let (input, _) = tag_no_case("RCPT TO:").parse(input)?;
84 let (input, _) = space0(input)?;
85 let (input, to) = forward_path(input)?;
86 let (input, params) = opt(preceded(space1, mail_parameters)).parse(input)?;
87
88 Ok((
89 input,
90 SmtpCommand::Rcpt {
91 to,
92 params: params.unwrap_or_default(),
93 },
94 ))
95}
96
97fn data_command(input: &str) -> IResult<&str, SmtpCommand> {
99 map(tag_no_case("DATA"), |_| SmtpCommand::Data).parse(input)
100}
101
102fn bdat_command(input: &str) -> IResult<&str, SmtpCommand> {
104 use nom::character::complete::digit1;
105
106 let (input, _) = tag_no_case("BDAT").parse(input)?;
107 let (input, _) = space1(input)?;
108 let (input, size_str) = digit1(input)?;
109 let (input, last) = opt(preceded(space1, tag_no_case("LAST"))).parse(input)?;
110
111 let chunk_size = size_str.parse::<usize>().map_err(|_| {
113 nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
114 })?;
115
116 Ok((
117 input,
118 SmtpCommand::Bdat {
119 chunk_size,
120 last: last.is_some(),
121 },
122 ))
123}
124
125fn rset_command(input: &str) -> IResult<&str, SmtpCommand> {
127 map(tag_no_case("RSET"), |_| SmtpCommand::Rset).parse(input)
128}
129
130fn noop_command(input: &str) -> IResult<&str, SmtpCommand> {
132 map(tag_no_case("NOOP"), |_| SmtpCommand::Noop).parse(input)
133}
134
135fn quit_command(input: &str) -> IResult<&str, SmtpCommand> {
137 map(tag_no_case("QUIT"), |_| SmtpCommand::Quit).parse(input)
138}
139
140fn vrfy_command(input: &str) -> IResult<&str, SmtpCommand> {
142 map(
143 preceded(tag_no_case("VRFY"), preceded(space1, rest)),
144 |s: &str| SmtpCommand::Vrfy(s.to_string()),
145 )
146 .parse(input)
147}
148
149fn expn_command(input: &str) -> IResult<&str, SmtpCommand> {
151 map(
152 preceded(tag_no_case("EXPN"), preceded(space1, rest)),
153 |s: &str| SmtpCommand::Expn(s.to_string()),
154 )
155 .parse(input)
156}
157
158fn help_command(input: &str) -> IResult<&str, SmtpCommand> {
160 map(
161 preceded(tag_no_case("HELP"), opt(preceded(space1, rest))),
162 |s: Option<&str>| SmtpCommand::Help(s.map(|x| x.to_string())),
163 )
164 .parse(input)
165}
166
167fn starttls_command(input: &str) -> IResult<&str, SmtpCommand> {
169 map(tag_no_case("STARTTLS"), |_| SmtpCommand::StartTls).parse(input)
170}
171
172fn auth_command(input: &str) -> IResult<&str, SmtpCommand> {
174 let (input, _) = tag_no_case("AUTH").parse(input)?;
175 let (input, _) = space1(input)?;
176 let (input, mechanism) =
177 take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-').parse(input)?;
178 let (input, initial_response) = opt(preceded(space1, rest)).parse(input)?;
179
180 Ok((
181 input,
182 SmtpCommand::Auth {
183 mechanism: mechanism.to_string(),
184 initial_response: initial_response.map(|s| s.to_string()),
185 },
186 ))
187}
188
189fn reverse_path(input: &str) -> IResult<&str, MailAddress> {
191 delimited(char('<'), mailbox, char('>')).parse(input)
192}
193
194fn forward_path(input: &str) -> IResult<&str, MailAddress> {
196 delimited(char('<'), mailbox, char('>')).parse(input)
197}
198
199fn mailbox(input: &str) -> IResult<&str, MailAddress> {
201 let (input, addr_str) = take_while1(|c: char| {
202 c.is_ascii_alphanumeric() || c == '@' || c == '.' || c == '-' || c == '_' || c == '+'
203 })
204 .parse(input)?;
205
206 match addr_str.parse::<MailAddress>() {
208 Ok(addr) => Ok((input, addr)),
209 Err(_) => Err(nom::Err::Error(nom::error::Error::new(
210 input,
211 nom::error::ErrorKind::Verify,
212 ))),
213 }
214}
215
216fn domain(input: &str) -> IResult<&str, String> {
218 map(
219 take_while1(|c: char| c.is_ascii_alphanumeric() || c == '.' || c == '-'),
220 |s: &str| s.to_string(),
221 )
222 .parse(input)
223}
224
225fn mail_parameters(input: &str) -> IResult<&str, Vec<MailParam>> {
227 let mut params = Vec::new();
228 let mut remaining = input;
229
230 while let Ok((rest, param)) = mail_parameter(remaining) {
231 params.push(param);
232 remaining = rest;
233
234 remaining = remaining.trim_start();
236
237 if remaining.is_empty() {
239 break;
240 }
241 }
242
243 Ok((remaining, params))
244}
245
246fn mail_parameter(input: &str) -> IResult<&str, MailParam> {
248 let (input, keyword) =
249 take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-').parse(input)?;
250 let (input, value) = opt(preceded(char('='), parameter_value)).parse(input)?;
251
252 Ok((
253 input,
254 MailParam::new(keyword.to_string(), value.map(|s| s.to_string())),
255 ))
256}
257
258fn parameter_value(input: &str) -> IResult<&str, String> {
260 map(
261 take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '.'),
262 |s: &str| s.to_string(),
263 )
264 .parse(input)
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn test_parse_helo() {
273 let cmd = parse_command("HELO example.com").expect("HELO command parse");
274 assert!(matches!(cmd, SmtpCommand::Helo(domain) if domain == "example.com"));
275 }
276
277 #[test]
278 fn test_parse_ehlo() {
279 let cmd = parse_command("EHLO mail.example.com").expect("EHLO command parse");
280 assert!(matches!(cmd, SmtpCommand::Ehlo(domain) if domain == "mail.example.com"));
281 }
282
283 #[test]
284 fn test_parse_mail_from() {
285 let cmd = parse_command("MAIL FROM:<user@example.com>").expect("MAIL FROM parse");
286 match cmd {
287 SmtpCommand::Mail { from, .. } => {
288 assert_eq!(from.as_string(), "user@example.com");
289 }
290 _ => panic!("Expected Mail command"),
291 }
292 }
293
294 #[test]
295 fn test_parse_rcpt_to() {
296 let cmd = parse_command("RCPT TO:<recipient@example.com>").expect("RCPT TO parse");
297 match cmd {
298 SmtpCommand::Rcpt { to, .. } => {
299 assert_eq!(to.as_string(), "recipient@example.com");
300 }
301 _ => panic!("Expected Rcpt command"),
302 }
303 }
304
305 #[test]
306 fn test_parse_data() {
307 let cmd = parse_command("DATA").expect("DATA command parse");
308 assert!(matches!(cmd, SmtpCommand::Data));
309 }
310
311 #[test]
312 fn test_parse_quit() {
313 let cmd = parse_command("QUIT").expect("QUIT command parse");
314 assert!(matches!(cmd, SmtpCommand::Quit));
315 }
316
317 #[test]
318 fn test_parse_rset() {
319 let cmd = parse_command("RSET").expect("RSET command parse");
320 assert!(matches!(cmd, SmtpCommand::Rset));
321 }
322
323 #[test]
324 fn test_parse_starttls() {
325 let cmd = parse_command("STARTTLS").expect("STARTTLS command parse");
326 assert!(matches!(cmd, SmtpCommand::StartTls));
327 }
328
329 #[test]
330 fn test_parse_auth() {
331 let cmd = parse_command("AUTH PLAIN dGVzdA==").expect("AUTH PLAIN command parse");
332 match cmd {
333 SmtpCommand::Auth {
334 mechanism,
335 initial_response,
336 } => {
337 assert_eq!(mechanism, "PLAIN");
338 assert_eq!(initial_response, Some("dGVzdA==".to_string()));
339 }
340 _ => panic!("Expected Auth command"),
341 }
342 }
343
344 #[test]
345 fn test_parse_case_insensitive() {
346 let cmd1 = parse_command("quit").expect("lowercase quit parse");
347 let cmd2 = parse_command("QUIT").expect("uppercase QUIT parse");
348 let cmd3 = parse_command("QuIt").expect("mixed-case QuIt parse");
349
350 assert!(matches!(cmd1, SmtpCommand::Quit));
351 assert!(matches!(cmd2, SmtpCommand::Quit));
352 assert!(matches!(cmd3, SmtpCommand::Quit));
353 }
354
355 #[test]
356 fn test_parse_mail_with_size() {
357 let cmd = parse_command("MAIL FROM:<user@example.com> SIZE=12345")
358 .expect("MAIL FROM with SIZE param parse");
359 match cmd {
360 SmtpCommand::Mail { from, params } => {
361 assert_eq!(from.as_string(), "user@example.com");
362 assert_eq!(params.len(), 1);
363 assert_eq!(params[0].keyword, "SIZE");
364 assert_eq!(params[0].value, Some("12345".to_string()));
365 }
366 _ => panic!("Expected Mail command"),
367 }
368 }
369
370 #[test]
371 fn test_parse_mail_with_body() {
372 let cmd = parse_command("MAIL FROM:<user@example.com> BODY=8BITMIME")
373 .expect("MAIL FROM with BODY param parse");
374 match cmd {
375 SmtpCommand::Mail { from, params } => {
376 assert_eq!(from.as_string(), "user@example.com");
377 assert_eq!(params.len(), 1);
378 assert_eq!(params[0].keyword, "BODY");
379 assert_eq!(params[0].value, Some("8BITMIME".to_string()));
380 }
381 _ => panic!("Expected Mail command"),
382 }
383 }
384
385 #[test]
386 fn test_parse_mail_with_smtputf8() {
387 let cmd = parse_command("MAIL FROM:<user@example.com> SMTPUTF8")
388 .expect("MAIL FROM with SMTPUTF8 param parse");
389 match cmd {
390 SmtpCommand::Mail { from, params } => {
391 assert_eq!(from.as_string(), "user@example.com");
392 assert_eq!(params.len(), 1);
393 assert_eq!(params[0].keyword, "SMTPUTF8");
394 assert_eq!(params[0].value, None);
395 }
396 _ => panic!("Expected Mail command"),
397 }
398 }
399
400 #[test]
401 fn test_parse_mail_with_multiple_params() {
402 let cmd = parse_command("MAIL FROM:<user@example.com> SIZE=12345 BODY=8BITMIME SMTPUTF8")
403 .expect("MAIL FROM with multiple params parse");
404 match cmd {
405 SmtpCommand::Mail { from, params } => {
406 assert_eq!(from.as_string(), "user@example.com");
407 assert_eq!(params.len(), 3);
408 assert_eq!(params[0].keyword, "SIZE");
409 assert_eq!(params[0].value, Some("12345".to_string()));
410 assert_eq!(params[1].keyword, "BODY");
411 assert_eq!(params[1].value, Some("8BITMIME".to_string()));
412 assert_eq!(params[2].keyword, "SMTPUTF8");
413 assert_eq!(params[2].value, None);
414 }
415 _ => panic!("Expected Mail command"),
416 }
417 }
418
419 #[test]
420 fn test_parse_bdat() {
421 let cmd = parse_command("BDAT 1024").expect("BDAT without LAST parse");
422 match cmd {
423 SmtpCommand::Bdat { chunk_size, last } => {
424 assert_eq!(chunk_size, 1024);
425 assert!(!last);
426 }
427 _ => panic!("Expected Bdat command"),
428 }
429 }
430
431 #[test]
432 fn test_parse_bdat_last() {
433 let cmd = parse_command("BDAT 512 LAST").expect("BDAT with LAST parse");
434 match cmd {
435 SmtpCommand::Bdat { chunk_size, last } => {
436 assert_eq!(chunk_size, 512);
437 assert!(last);
438 }
439 _ => panic!("Expected Bdat command"),
440 }
441 }
442
443 #[test]
444 fn test_parse_bdat_case_insensitive() {
445 let cmd1 = parse_command("bdat 100").expect("lowercase bdat parse");
446 let cmd2 = parse_command("BDAT 100").expect("uppercase BDAT parse");
447 let cmd3 = parse_command("BdAt 100").expect("mixed-case BdAt parse");
448 let cmd4 = parse_command("BDAT 256 last").expect("BDAT with lowercase last parse");
449 let cmd5 = parse_command("bdat 256 LAST").expect("bdat with uppercase LAST parse");
450
451 match (cmd1, cmd2, cmd3, cmd4, cmd5) {
452 (
453 SmtpCommand::Bdat {
454 chunk_size: s1,
455 last: l1,
456 },
457 SmtpCommand::Bdat {
458 chunk_size: s2,
459 last: l2,
460 },
461 SmtpCommand::Bdat {
462 chunk_size: s3,
463 last: l3,
464 },
465 SmtpCommand::Bdat {
466 chunk_size: s4,
467 last: l4,
468 },
469 SmtpCommand::Bdat {
470 chunk_size: s5,
471 last: l5,
472 },
473 ) => {
474 assert_eq!(s1, 100);
475 assert_eq!(s2, 100);
476 assert_eq!(s3, 100);
477 assert_eq!(s4, 256);
478 assert_eq!(s5, 256);
479 assert!(!l1);
480 assert!(!l2);
481 assert!(!l3);
482 assert!(l4);
483 assert!(l5);
484 }
485 _ => panic!("Expected Bdat commands"),
486 }
487 }
488}