1use std::{io::IoSlice, iter, str};
2
3use auto_enums::auto_enum;
4use lazy_static::lazy_static;
5use nom::{
6 branch::alt,
7 bytes::streaming::{is_a, tag, tag_no_case, take_until},
8 character::streaming::one_of,
9 combinator::{map, map_res, opt, value},
10 multi::{many0, many1_count},
11 sequence::{pair, preceded, terminated, tuple},
12 IResult,
13};
14use regex_automata::{Regex, RegexBuilder};
15
16use crate::*;
17
18lazy_static! {
19 static ref PARAMETER_NAME: Regex = RegexBuilder::new()
20 .anchored(true)
21 .build(
22 r#"(?x)
23 [[:alnum:]] ( [[:alnum:]-] )*
24 "#
25 )
26 .unwrap();
27 static ref PARAMETER_VALUE_ASCII: Regex = RegexBuilder::new()
28 .anchored(true)
29 .build(r#"[[:ascii:]&&[^= [:cntrl:]]]+"#)
30 .unwrap();
31 static ref PARAMETER_VALUE_UTF8: Regex = RegexBuilder::new()
32 .anchored(true)
33 .build(r#"[^= [:cntrl:]]+"#)
34 .unwrap();
35}
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum ParameterName<S> {
39 Other(S),
40}
41
42impl<S> ParameterName<S> {
43 #[inline]
44 pub fn parse<'a>(buf: &'a [u8]) -> IResult<&'a [u8], ParameterName<S>>
45 where
46 S: From<&'a str>,
47 {
48 map(apply_regex(&PARAMETER_NAME), |b: &[u8]| {
49 let s = unsafe { str::from_utf8_unchecked(b) };
52 ParameterName::Other(s.into())
53 })(buf)
54 }
55}
56
57impl<S> ParameterName<S>
58where
59 S: AsRef<str>,
60{
61 #[inline]
62 pub fn as_io_slices(&self) -> impl Iterator<Item = IoSlice> {
63 iter::once(IoSlice::new(match self {
64 ParameterName::Other(s) => s.as_ref().as_ref(),
65 }))
66 }
67}
68
69#[derive(Clone, Debug, Eq, PartialEq)]
71pub struct Parameters<S>(pub Vec<(ParameterName<S>, Option<MaybeUtf8<S>>)>);
72
73impl<S> Parameters<S> {
74 pub fn parse_until<'a, 'b>(
77 term_with_sp_tab: &'b [u8],
78 ) -> impl 'b + FnMut(&'a [u8]) -> IResult<&'a [u8], Parameters<S>>
79 where
80 'a: 'b,
81 S: 'b + From<&'a str>,
82 {
83 map(
84 many0(preceded(
85 many1_count(one_of(" \t")),
86 pair(
87 ParameterName::parse,
88 opt(preceded(
89 tag(b"="),
90 alt((
91 map(
92 terminated(
93 apply_regex(&PARAMETER_VALUE_ASCII),
94 terminate(term_with_sp_tab),
95 ),
96 |b| {
97 let s = unsafe { str::from_utf8_unchecked(b) };
101 MaybeUtf8::Ascii(s.into())
102 },
103 ),
104 map(
105 terminated(
106 apply_regex(&PARAMETER_VALUE_UTF8),
107 terminate(term_with_sp_tab),
108 ),
109 |b| {
110 let s = unsafe { str::from_utf8_unchecked(b) };
114 MaybeUtf8::Utf8(s.into())
115 },
116 ),
117 )),
118 )),
119 ),
120 )),
121 Parameters,
122 )
123 }
124}
125
126impl<S> Parameters<S>
127where
128 S: AsRef<str>,
129{
130 #[inline]
131 #[auto_enum]
132 pub fn as_io_slices(&self) -> impl Iterator<Item = IoSlice> {
133 self.0.iter().flat_map(|(name, value)| {
134 iter::once(IoSlice::new(b" "))
135 .chain(name.as_io_slices())
136 .chain(
137 #[auto_enum(Iterator)]
138 match value {
139 None => iter::empty(),
140 Some(v) => iter::once(IoSlice::new(b"=")).chain(v.as_io_slices()),
141 },
142 )
143 })
144 }
145}
146
147#[derive(Clone, Debug, Eq, PartialEq)]
148pub enum Command<S> {
149 Data,
151
152 Ehlo { hostname: Hostname<S> },
154
155 Expn { name: MaybeUtf8<S> },
157
158 Helo { hostname: Hostname<S> },
160
161 Help { subject: MaybeUtf8<S> },
163
164 Lhlo { hostname: Hostname<S> },
166
167 Mail {
169 path: Option<Path<S>>,
170 email: Option<Email<S>>,
171 params: Parameters<S>,
172 },
173
174 Noop { string: MaybeUtf8<S> },
176
177 Quit,
179
180 Rcpt {
182 path: Option<Path<S>>,
183 email: Email<S>,
184 params: Parameters<S>,
185 },
186
187 Rset,
189
190 Starttls,
192
193 Vrfy { name: MaybeUtf8<S> },
195}
196
197impl<S> Command<S> {
198 pub fn parse<'a>(buf: &'a [u8]) -> IResult<&'a [u8], Command<S>>
199 where
200 S: From<&'a str>,
201 {
202 alt((
203 map(
204 tuple((tag_no_case(b"DATA"), opt(is_a(" \t")), tag(b"\r\n"))),
205 |_| Command::Data,
206 ),
207 map(
208 tuple((
209 tag_no_case(b"EHLO"),
210 is_a(" \t"),
211 Hostname::parse_until(b" \t\r"),
212 opt(is_a(" \t")),
213 tag(b"\r\n"),
214 )),
215 |(_, _, hostname, _, _)| Command::Ehlo { hostname },
216 ),
217 map_res(
218 tuple((
219 tag_no_case(b"EXPN"),
220 one_of(" \t"),
221 take_until("\r\n"),
222 tag(b"\r\n"),
223 )),
224 |(_, _, name, _)| {
225 str::from_utf8(name).map(|name| Command::Expn {
226 name: MaybeUtf8::from(name),
227 })
228 },
229 ),
230 map(
231 tuple((
232 tag_no_case(b"HELO"),
233 is_a(" \t"),
234 Hostname::parse_until(b" \t\r"),
235 opt(is_a(" \t")),
236 tag(b"\r\n"),
237 )),
238 |(_, _, hostname, _, _)| Command::Helo { hostname },
239 ),
240 map_res(
241 preceded(
242 tag_no_case(b"HELP"),
243 alt((
244 preceded(one_of(" \t"), terminated(take_until("\r\n"), tag(b"\r\n"))),
245 value(&b""[..], tag(b"\r\n")),
246 )),
247 ),
248 |s| {
249 str::from_utf8(s).map(|s| Command::Help {
250 subject: MaybeUtf8::from(s),
251 })
252 },
253 ),
254 map(
255 tuple((
256 tag_no_case(b"LHLO"),
257 is_a(" \t"),
258 Hostname::parse_until(b" \t\r"),
259 opt(is_a(" \t")),
260 tag(b"\r\n"),
261 )),
262 |(_, _, hostname, _, _)| Command::Lhlo { hostname },
263 ),
264 map(
265 tuple((
266 tag_no_case(b"MAIL FROM:"),
267 opt(is_a(" \t")),
268 alt((
269 map(tag(b"<>"), |_| None),
270 map(
271 email_with_path(b" \t\r", b" \t\r@", b" \t\r>", b" \t\r@>"),
272 Some,
273 ),
274 )),
275 Parameters::parse_until(b" \t\r"),
276 opt(is_a(" \t")),
277 tag("\r\n"),
278 )),
279 |(_, _, email, params, _, _)| match email {
280 None => Command::Mail {
281 path: None,
282 email: None,
283 params,
284 },
285 Some((path, email)) => Command::Mail {
286 path,
287 email: Some(email),
288 params,
289 },
290 },
291 ),
292 map_res(
293 preceded(
294 tag_no_case(b"NOOP"),
295 alt((
296 preceded(one_of(" \t"), terminated(take_until("\r\n"), tag(b"\r\n"))),
297 value(&b""[..], tag(b"\r\n")),
298 )),
299 ),
300 |s| {
301 str::from_utf8(s).map(|s| Command::Noop {
302 string: MaybeUtf8::from(s),
303 })
304 },
305 ),
306 map(
307 tuple((tag_no_case(b"QUIT"), opt(is_a(" \t")), tag(b"\r\n"))),
308 |_| Command::Quit,
309 ),
310 map(
311 tuple((
312 tag_no_case(b"RCPT TO:"),
313 opt(is_a(" \t")),
314 email_with_path(b" \t\r", b" \t\r@", b" \t\r>", b" \t\r@>"),
315 Parameters::parse_until(b" \t\r"),
316 opt(is_a(" \t")),
317 tag("\r\n"),
318 )),
319 |(_, _, (path, email), params, _, _)| Command::Rcpt {
320 path,
321 email,
322 params,
323 },
324 ),
325 map(
326 tuple((tag_no_case(b"RSET"), opt(is_a(" \t")), tag(b"\r\n"))),
327 |_| Command::Rset,
328 ),
329 map(
330 tuple((tag_no_case(b"STARTTLS"), opt(is_a(" \t")), tag(b"\r\n"))),
331 |_| Command::Starttls,
332 ),
333 map_res(
334 tuple((
335 tag_no_case(b"VRFY"),
336 one_of(" \t"),
337 take_until("\r\n"),
338 tag(b"\r\n"),
339 )),
340 |(_, _, s, _)| {
341 str::from_utf8(s).map(|s| Command::Vrfy {
342 name: MaybeUtf8::from(s),
343 })
344 },
345 ),
346 ))(buf)
347 }
348}
349
350impl<S> Command<S>
351where
352 S: AsRef<str>,
353{
354 #[auto_enum(Iterator)]
355 pub fn as_io_slices(&self) -> impl Iterator<Item = IoSlice> {
356 match self {
357 Command::Data => iter::once(IoSlice::new(b"DATA\r\n")),
358
359 Command::Ehlo { hostname } => iter::once(IoSlice::new(b"EHLO "))
360 .chain(hostname.as_io_slices())
361 .chain(iter::once(IoSlice::new(b"\r\n"))),
362
363 Command::Expn { name } => iter::once(IoSlice::new(b"EXPN "))
364 .chain(name.as_io_slices())
365 .chain(iter::once(IoSlice::new(b"\r\n"))),
366
367 Command::Helo { hostname } => iter::once(IoSlice::new(b"HELO "))
368 .chain(hostname.as_io_slices())
369 .chain(iter::once(IoSlice::new(b"\r\n"))),
370
371 Command::Help { subject } => iter::once(IoSlice::new(b"HELP "))
372 .chain(subject.as_io_slices())
373 .chain(iter::once(IoSlice::new(b"\r\n"))),
374
375 Command::Lhlo { hostname } => iter::once(IoSlice::new(b"LHLO "))
376 .chain(hostname.as_io_slices())
377 .chain(iter::once(IoSlice::new(b"\r\n"))),
378
379 Command::Mail {
380 path,
381 email,
382 params,
383 } => iter::once(IoSlice::new(b"MAIL FROM:<"))
384 .chain(
385 #[auto_enum(Iterator)]
386 match path {
387 Some(path) => path.as_io_slices().chain(iter::once(IoSlice::new(b":"))),
388 None => iter::empty(),
389 },
390 )
391 .chain(
392 #[auto_enum(Iterator)]
393 match email {
394 Some(email) => email.as_io_slices(),
395 None => iter::empty(),
396 },
397 )
398 .chain(iter::once(IoSlice::new(b">")))
399 .chain(params.as_io_slices())
400 .chain(iter::once(IoSlice::new(b"\r\n"))),
401
402 Command::Noop { string } => iter::once(IoSlice::new(b"NOOP "))
403 .chain(string.as_io_slices())
404 .chain(iter::once(IoSlice::new(b"\r\n"))),
405
406 Command::Quit => iter::once(IoSlice::new(b"QUIT\r\n")),
407
408 Command::Rcpt {
409 path,
410 email,
411 params,
412 } => iter::once(IoSlice::new(b"RCPT TO:<"))
413 .chain(
414 #[auto_enum(Iterator)]
415 match path {
416 Some(path) => path.as_io_slices().chain(iter::once(IoSlice::new(b":"))),
417 None => iter::empty(),
418 },
419 )
420 .chain(email.as_io_slices())
421 .chain(iter::once(IoSlice::new(b">")))
422 .chain(params.as_io_slices())
423 .chain(iter::once(IoSlice::new(b"\r\n"))),
424
425 Command::Rset => iter::once(IoSlice::new(b"RSET\r\n")),
426
427 Command::Starttls => iter::once(IoSlice::new(b"STARTTLS\r\n")),
428
429 Command::Vrfy { name } => iter::once(IoSlice::new(b"VRFY "))
430 .chain(name.as_io_slices())
431 .chain(iter::once(IoSlice::new(b"\r\n"))),
432 }
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 #[test]
443 fn parameters_valid() {
444 let tests: &[(&[u8], Parameters<&str>)] = &[
445 (
446 b" key=value\r\n",
447 Parameters(vec![(
448 ParameterName::Other("key"),
449 Some(MaybeUtf8::Ascii("value")),
450 )]),
451 ),
452 (
453 b"\tkey=value\tkey2=value2\r\n",
454 Parameters(vec![
455 (ParameterName::Other("key"), Some(MaybeUtf8::Ascii("value"))),
456 (
457 ParameterName::Other("key2"),
458 Some(MaybeUtf8::Ascii("value2")),
459 ),
460 ]),
461 ),
462 (
463 b" KeY2=V4\"l\\u@e.z\t0tterkeyz=very_muchWh4t3ver\r\n",
464 Parameters(vec![
465 (
466 ParameterName::Other("KeY2"),
467 Some(MaybeUtf8::Ascii("V4\"l\\u@e.z")),
468 ),
469 (
470 ParameterName::Other("0tterkeyz"),
471 Some(MaybeUtf8::Ascii("very_muchWh4t3ver")),
472 ),
473 ]),
474 ),
475 (
476 b" NoValueKey\r\n",
477 Parameters(vec![(ParameterName::Other("NoValueKey"), None)]),
478 ),
479 (
480 b" A B\r\n",
481 Parameters(vec![
482 (ParameterName::Other("A"), None),
483 (ParameterName::Other("B"), None),
484 ]),
485 ),
486 (
487 b" A=B C D=SP\r\n",
488 Parameters(vec![
489 (ParameterName::Other("A"), Some(MaybeUtf8::Ascii("B"))),
490 (ParameterName::Other("C"), None),
491 (ParameterName::Other("D"), Some(MaybeUtf8::Ascii("SP"))),
492 ]),
493 ),
494 ];
495 for (inp, out) in tests {
496 println!("Test: {:?}", show_bytes(inp));
497 let r = Parameters::parse_until(b" \t\r\n")(inp);
498 println!("Result: {:?}", r);
499 match r {
500 Ok((rest, res)) if rest == b"\r\n" && res == *out => (),
501 x => panic!("Unexpected result: {:?}", x),
502 }
503 }
504 }
505
506 #[test]
509 fn command_valid() {
510 let tests: &[(&[u8], Command<&str>)] = &[
511 (b"DATA \t \t \r\n", Command::Data),
512 (b"daTa\r\n", Command::Data),
513 (b"eHlO \t hello.world \t \r\n", Command::Ehlo {
514 hostname: Hostname::AsciiDomain { raw: "hello.world" },
515 }),
516 (b"EHLO hello.world\r\n", Command::Ehlo {
517 hostname: Hostname::AsciiDomain { raw: "hello.world" },
518 }),
519 (b"EXpN \t hello.world \t \r\n", Command::Expn {
520 name: MaybeUtf8::Ascii("\t hello.world \t "),
521 }),
522 (b"hElO\t hello.world \t \r\n", Command::Helo {
523 hostname: Hostname::AsciiDomain { raw: "hello.world" },
524 }),
525 (b"HELO hello.world\r\n", Command::Helo {
526 hostname: Hostname::AsciiDomain { raw: "hello.world" },
527 }),
528 (b"help \t hello.world \t \r\n", Command::Help {
529 subject: MaybeUtf8::Ascii("\t hello.world \t "),
530 }),
531 (b"HELP\r\n", Command::Help {
532 subject: MaybeUtf8::Ascii(""),
533 }),
534 (b"hElP \r\n", Command::Help {
535 subject: MaybeUtf8::Ascii(""),
536 }),
537 (b"lHlO \t hello.world \t \r\n", Command::Lhlo {
538 hostname: Hostname::AsciiDomain { raw: "hello.world" },
539 }),
540 (b"LHLO hello.world\r\n", Command::Lhlo {
541 hostname: Hostname::AsciiDomain { raw: "hello.world" },
542 }),
543 (b"Mail FROM:<@one,@two:foo@bar.baz>\r\n", Command::Mail {
544 path: Some(Path {
545 domains: vec![
546 Hostname::AsciiDomain { raw: "one" },
547 Hostname::AsciiDomain { raw: "two" },
548 ],
549 }),
550 email: Some(Email {
551 localpart: Localpart::Ascii { raw: "foo" },
552 hostname: Some(Hostname::AsciiDomain { raw: "bar.baz" }),
553 }),
554 params: Parameters(vec![]),
555 }),
556 (b"MaiL FrOm: quux@example.net \t \r\n", Command::Mail {
557 path: None,
558 email: Some(Email {
559 localpart: Localpart::Ascii { raw: "quux" },
560 hostname: Some(Hostname::AsciiDomain { raw: "example.net" }),
561 }),
562 params: Parameters(vec![]),
563 }),
564 (b"MaiL FrOm: quux@example.net\r\n", Command::Mail {
565 path: None,
566 email: Some(Email {
567 localpart: Localpart::Ascii { raw: "quux" },
568 hostname: Some(Hostname::AsciiDomain { raw: "example.net" }),
569 }),
570 params: Parameters(vec![]),
571 }),
572 (b"mail FROM:<>\r\n", Command::Mail {
573 path: None,
574 email: None,
575 params: Parameters(vec![]),
576 }),
577 (b"MAIL FROM:<> hello=world foo\r\n", Command::Mail {
578 path: None,
579 email: None,
580 params: Parameters(vec![
581 (
582 ParameterName::Other("hello"),
583 Some(MaybeUtf8::Ascii("world")),
584 ),
585 (ParameterName::Other("foo"), None),
586 ]),
587 }),
588 (b"NOOP \t hello.world \t \r\n", Command::Noop {
589 string: MaybeUtf8::Ascii("\t hello.world \t "),
590 }),
591 (b"nOoP\r\n", Command::Noop {
592 string: MaybeUtf8::Ascii(""),
593 }),
594 (b"noop \r\n", Command::Noop {
595 string: MaybeUtf8::Ascii(""),
596 }),
597 (b"QUIT \t \t \r\n", Command::Quit),
598 (b"quit\r\n", Command::Quit),
599 (b"RCPT TO:<@one,@two:foo@bar.baz>\r\n", Command::Rcpt {
600 path: Some(Path {
601 domains: vec![
602 Hostname::AsciiDomain { raw: "one" },
603 Hostname::AsciiDomain { raw: "two" },
604 ],
605 }),
606 email: Email {
607 localpart: Localpart::Ascii { raw: "foo" },
608 hostname: Some(Hostname::AsciiDomain { raw: "bar.baz" }),
609 },
610 params: Parameters(vec![]),
611 }),
612 (b"Rcpt tO: quux@example.net \t \r\n", Command::Rcpt {
613 path: None,
614 email: Email {
615 localpart: Localpart::Ascii { raw: "quux" },
616 hostname: Some(Hostname::AsciiDomain { raw: "example.net" }),
617 },
618 params: Parameters(vec![]),
619 }),
620 (b"rcpt TO:<Postmaster>\r\n", Command::Rcpt {
621 path: None,
622 email: Email {
623 localpart: Localpart::Ascii { raw: "Postmaster" },
624 hostname: None,
625 },
626 params: Parameters(vec![]),
627 }),
628 (b"RcPt TO: \t poStmaster\r\n", Command::Rcpt {
629 path: None,
630 email: Email {
631 localpart: Localpart::Ascii { raw: "poStmaster" },
632 hostname: None,
633 },
634 params: Parameters(vec![]),
635 }),
636 (b"RSET \t \t \r\n", Command::Rset),
637 (b"rSet\r\n", Command::Rset),
638 (b"STARTTLS \t \t \r\n", Command::Starttls),
639 (b"starttls\r\n", Command::Starttls),
640 (b"VrFY \t hello.world \t \r\n", Command::Vrfy {
641 name: MaybeUtf8::Ascii("\t hello.world \t "),
642 }),
643 ];
644 for (inp, out) in tests {
645 println!("Test: {:?}", show_bytes(inp));
646 let r = Command::parse(inp);
647 println!("Result: {:?}", r);
648 match r {
649 Ok((rest, res)) => {
650 assert_eq!(rest, b"");
651 assert_eq!(res, *out);
652 }
653 x => panic!("Unexpected result: {:?}", x),
654 }
655 }
656 }
657
658 #[test]
659 fn command_incomplete() {
660 let tests: &[&[u8]] = &[b"MAIL FROM:<foo@bar.com", b"mail from:foo@bar.com"];
662 for inp in tests {
663 let r = Command::<&str>::parse(inp);
664 println!("{:?}: {:?}", show_bytes(inp), r);
665 assert!(r.unwrap_err().is_incomplete());
666 }
667 }
668
669 #[test]
670 fn command_invalid() {
671 let tests: &[&[u8]] = &[b"HELPfoo"];
672 for inp in tests {
673 let r = Command::<&str>::parse(inp);
674 println!("{:?}: {:?}", show_bytes(inp), r);
675 assert!(!r.unwrap_err().is_incomplete());
676 }
677 }
678
679 #[test]
680 fn command_build() {
681 let tests: &[(Command<&str>, &[u8])] = &[
682 (Command::Data, b"DATA\r\n"),
683 (
684 Command::Ehlo {
685 hostname: Hostname::AsciiDomain {
686 raw: "test.foo.bar",
687 },
688 },
689 b"EHLO test.foo.bar\r\n",
690 ),
691 (
692 Command::Expn {
693 name: MaybeUtf8::Ascii("foobar"),
694 },
695 b"EXPN foobar\r\n",
696 ),
697 (
698 Command::Helo {
699 hostname: Hostname::AsciiDomain {
700 raw: "test.example.org",
701 },
702 },
703 b"HELO test.example.org\r\n",
704 ),
705 (
706 Command::Help {
707 subject: MaybeUtf8::Ascii("topic"),
708 },
709 b"HELP topic\r\n",
710 ),
711 (
712 Command::Lhlo {
713 hostname: Hostname::AsciiDomain {
714 raw: "test.example.org",
715 },
716 },
717 b"LHLO test.example.org\r\n",
718 ),
719 (
720 Command::Mail {
721 path: None,
722 email: Some(Email {
723 localpart: Localpart::Ascii { raw: "foo" },
724 hostname: Some(Hostname::AsciiDomain { raw: "bar.baz" }),
725 }),
726 params: Parameters(vec![]),
727 },
728 b"MAIL FROM:<foo@bar.baz>\r\n",
729 ),
730 (
731 Command::Mail {
732 path: Some(Path {
733 domains: vec![
734 Hostname::AsciiDomain { raw: "test" },
735 Hostname::AsciiDomain { raw: "foo.bar" },
736 ],
737 }),
738 email: Some(Email {
739 localpart: Localpart::Ascii { raw: "foo" },
740 hostname: Some(Hostname::AsciiDomain { raw: "bar.baz" }),
741 }),
742 params: Parameters(vec![]),
743 },
744 b"MAIL FROM:<@test,@foo.bar:foo@bar.baz>\r\n",
745 ),
746 (
747 Command::Mail {
748 path: None,
749 email: None,
750 params: Parameters(vec![]),
751 },
752 b"MAIL FROM:<>\r\n",
753 ),
754 (
755 Command::Mail {
756 path: None,
757 email: Some(Email {
758 localpart: Localpart::Ascii { raw: "hello" },
759 hostname: Some(Hostname::AsciiDomain {
760 raw: "world.example.org",
761 }),
762 }),
763 params: Parameters(vec![
764 (ParameterName::Other("foo"), Some(MaybeUtf8::Ascii("bar"))),
765 (ParameterName::Other("baz"), None),
766 (
767 ParameterName::Other("helloworld"),
768 Some(MaybeUtf8::Ascii("bleh")),
769 ),
770 ]),
771 },
772 b"MAIL FROM:<hello@world.example.org> foo=bar baz helloworld=bleh\r\n",
773 ),
774 (
775 Command::Noop {
776 string: MaybeUtf8::Ascii("useless string"),
777 },
778 b"NOOP useless string\r\n",
779 ),
780 (Command::Quit, b"QUIT\r\n"),
781 (
782 Command::Rcpt {
783 path: None,
784 email: Email {
785 localpart: Localpart::Ascii { raw: "foo" },
786 hostname: Some(Hostname::AsciiDomain { raw: "bar.com" }),
787 },
788 params: Parameters(vec![]),
789 },
790 b"RCPT TO:<foo@bar.com>\r\n",
791 ),
792 (
793 Command::Rcpt {
794 path: None,
795 email: Email {
796 localpart: Localpart::Ascii { raw: "Postmaster" },
797 hostname: None,
798 },
799 params: Parameters(vec![]),
800 },
801 b"RCPT TO:<Postmaster>\r\n",
802 ),
803 (Command::Rset, b"RSET\r\n"),
804 (Command::Starttls, b"STARTTLS\r\n"),
805 (
806 Command::Vrfy {
807 name: MaybeUtf8::Ascii("postmaster"),
808 },
809 b"VRFY postmaster\r\n",
810 ),
811 ];
812 for (inp, out) in tests {
813 println!("Test: {:?}", inp);
814 let res = inp
815 .as_io_slices()
816 .flat_map(|s| s.iter().cloned().collect::<Vec<_>>().into_iter())
817 .collect::<Vec<u8>>();
818 println!("Result : {:?}", show_bytes(&res));
819 println!("Expected: {:?}", show_bytes(out));
820 assert_eq!(&res, out);
821 }
822 }
823}