smtp_message/
command.rs

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            // The below unsafe is OK, thanks to PARAMETER_NAME
50            // validating that `b` is proper ascii
51            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/// Note: This struct includes the leading ' '
70#[derive(Clone, Debug, Eq, PartialEq)]
71pub struct Parameters<S>(pub Vec<(ParameterName<S>, Option<MaybeUtf8<S>>)>);
72
73impl<S> Parameters<S> {
74    /// If term is the wanted terminator, then
75    /// term_with_sp_tab = term + b" \t"
76    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                                    // The below unsafe is OK, thanks
98                                    // to the regex having validated
99                                    // that it is pure ASCII
100                                    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                                    // The below unsafe is OK, thanks
111                                    // to the regex having validated
112                                    // that it is valid UTF-8
113                                    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 <CRLF>
150    Data,
151
152    /// EHLO <hostname> <CRLF>
153    Ehlo { hostname: Hostname<S> },
154
155    /// EXPN <name> <CRLF>
156    Expn { name: MaybeUtf8<S> },
157
158    /// HELO <hostname> <CRLF>
159    Helo { hostname: Hostname<S> },
160
161    /// HELP [<subject>] <CRLF>
162    Help { subject: MaybeUtf8<S> },
163
164    /// LHLO <hostname> <CRLF>
165    Lhlo { hostname: Hostname<S> },
166
167    /// MAIL FROM:<@ONE,@TWO:JOE@THREE> [SP <mail-parameters>] <CRLF>
168    Mail {
169        path: Option<Path<S>>,
170        email: Option<Email<S>>,
171        params: Parameters<S>,
172    },
173
174    /// NOOP [<string>] <CRLF>
175    Noop { string: MaybeUtf8<S> },
176
177    /// QUIT <CRLF>
178    Quit,
179
180    /// RCPT TO:<@ONE,@TWO:JOE@THREE> [SP <rcpt-parameters] <CRLF>
181    Rcpt {
182        path: Option<Path<S>>,
183        email: Email<S>,
184        params: Parameters<S>,
185    },
186
187    /// RSET <CRLF>
188    Rset,
189
190    /// STARTTLS <CRLF>
191    Starttls,
192
193    /// VRFY <name> <CRLF>
194    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    // TODO: test parameter (without an s) valid, incomplete, invalid and build
441
442    #[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    // TODO: test parameter incomplete, invalid and build
507
508    #[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        // TODO: add tests for all the variants (that could)
661        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}