rfc5321/
parser.rs

1use crate::client_types::SmtpClientTimeouts;
2use pest::iterators::{Pair, Pairs};
3use pest::Parser as _;
4use pest_derive::*;
5use std::time::Duration;
6
7#[derive(Parser)]
8#[grammar = "rfc5321.pest"]
9struct Parser;
10
11impl Parser {
12    pub fn parse_command(text: &str) -> Result<Command, String> {
13        let result = Parser::parse(Rule::command, text)
14            .map_err(|err| format!("{err:#}"))?
15            .next()
16            .unwrap();
17
18        match result.as_rule() {
19            Rule::mail => Self::parse_mail(result.into_inner()),
20            Rule::rcpt => Self::parse_rcpt(result.into_inner()),
21            Rule::ehlo => Self::parse_ehlo(result.into_inner()),
22            Rule::helo => Self::parse_helo(result.into_inner()),
23            Rule::data => Ok(Command::Data),
24            Rule::rset => Ok(Command::Rset),
25            Rule::quit => Ok(Command::Quit),
26            Rule::starttls => Ok(Command::StartTls),
27            Rule::vrfy => Self::parse_vrfy(result.into_inner()),
28            Rule::expn => Self::parse_expn(result.into_inner()),
29            Rule::help => Self::parse_help(result.into_inner()),
30            Rule::noop => Self::parse_noop(result.into_inner()),
31            Rule::auth => Self::parse_auth(result.into_inner()),
32            Rule::xclient => Self::parse_xclient(result.into_inner()),
33            _ => Err(format!("unexpected {result:?}")),
34        }
35    }
36
37    fn parse_ehlo(mut pairs: Pairs<Rule>) -> Result<Command, String> {
38        let domain = pairs.next().unwrap();
39        Ok(Command::Ehlo(Self::parse_domain(domain)?))
40    }
41
42    fn parse_helo(mut pairs: Pairs<Rule>) -> Result<Command, String> {
43        let domain = pairs.next().unwrap();
44        Ok(Command::Helo(Self::parse_domain(domain)?))
45    }
46
47    fn parse_vrfy(mut pairs: Pairs<Rule>) -> Result<Command, String> {
48        let param = pairs.next().unwrap().as_str().to_string();
49        Ok(Command::Vrfy(param))
50    }
51
52    fn parse_expn(mut pairs: Pairs<Rule>) -> Result<Command, String> {
53        let param = pairs.next().unwrap().as_str().to_string();
54        Ok(Command::Expn(param))
55    }
56
57    fn parse_help(mut pairs: Pairs<Rule>) -> Result<Command, String> {
58        let param = pairs.next().map(|s| s.as_str().to_string());
59        Ok(Command::Help(param))
60    }
61
62    fn parse_noop(mut pairs: Pairs<Rule>) -> Result<Command, String> {
63        let param = pairs.next().map(|s| s.as_str().to_string());
64        Ok(Command::Noop(param))
65    }
66
67    fn parse_auth(mut pairs: Pairs<Rule>) -> Result<Command, String> {
68        let sasl_mech = pairs.next().map(|s| s.as_str().to_string()).unwrap();
69        let initial_response = pairs.next().map(|s| s.as_str().to_string());
70
71        Ok(Command::Auth {
72            sasl_mech,
73            initial_response,
74        })
75    }
76
77    fn parse_xclient(mut pairs: Pairs<Rule>) -> Result<Command, String> {
78        let mut params = vec![];
79
80        while let Some(param_name) = pairs.next() {
81            let name = param_name.as_str().to_string();
82            let value = xtext_decode(pairs.next().unwrap().as_str())?;
83            params.push(XClientParameter { name, value });
84        }
85
86        Ok(Command::XClient(params))
87    }
88
89    fn parse_rcpt(mut pairs: Pairs<Rule>) -> Result<Command, String> {
90        let forward_path = pairs.next().unwrap().into_inner().next().unwrap();
91        let mut no_angles = false;
92        let address = match forward_path.as_rule() {
93            Rule::path_no_angles => {
94                no_angles = true;
95                ForwardPath::Path(Self::parse_path(forward_path)?)
96            }
97            Rule::path => ForwardPath::Path(Self::parse_path(forward_path)?),
98            Rule::postmaster => ForwardPath::Postmaster,
99            wat => return Err(format!("unexpected {wat:?}")),
100        };
101
102        let mut parameters = vec![];
103
104        if let Some(params) = pairs.next() {
105            if no_angles {
106                return Err(
107                    "must enclose address in <> if you want to use ESMTP parameters".to_string(),
108                );
109            }
110            for param in params.into_inner() {
111                let mut iter = param.into_inner();
112                let name = iter.next().unwrap().as_str().to_string();
113                let value = iter.next().map(|p| p.as_str().to_string());
114                parameters.push(EsmtpParameter { name, value });
115            }
116        }
117
118        Ok(Command::RcptTo {
119            address,
120            parameters,
121        })
122    }
123
124    fn parse_mail(mut pairs: Pairs<Rule>) -> Result<Command, String> {
125        let reverse_path = pairs.next().unwrap().into_inner().next().unwrap();
126        let mut no_angles = false;
127        let address = match reverse_path.as_rule() {
128            Rule::path_no_angles => {
129                no_angles = true;
130                ReversePath::Path(Self::parse_path(reverse_path)?)
131            }
132            Rule::path => ReversePath::Path(Self::parse_path(reverse_path)?),
133            Rule::null_sender => ReversePath::NullSender,
134            wat => return Err(format!("unexpected {wat:?}")),
135        };
136
137        let mut parameters = vec![];
138
139        if let Some(params) = pairs.next() {
140            if no_angles {
141                return Err(
142                    "must enclose address in <> if you want to use ESMTP parameters".to_string(),
143                );
144            }
145            for param in params.into_inner() {
146                let mut iter = param.into_inner();
147                let name = iter.next().unwrap().as_str().to_string();
148                let value = iter.next().map(|p| p.as_str().to_string());
149                parameters.push(EsmtpParameter { name, value });
150            }
151        }
152
153        Ok(Command::MailFrom {
154            address,
155            parameters,
156        })
157    }
158
159    fn parse_path(path: Pair<Rule>) -> Result<MailPath, String> {
160        let mut at_domain_list: Vec<String> = vec![];
161        for p in path.into_inner() {
162            match p.as_rule() {
163                Rule::adl => {
164                    for pair in p.into_inner() {
165                        if let Some(dom) = pair.into_inner().next() {
166                            at_domain_list.push(dom.as_str().to_string());
167                        }
168                    }
169                }
170                Rule::mailbox => {
171                    let mailbox = Self::parse_mailbox(p.into_inner())?;
172                    return Ok(MailPath {
173                        at_domain_list,
174                        mailbox,
175                    });
176                }
177                _ => unreachable!(),
178            }
179        }
180        unreachable!()
181    }
182
183    fn parse_domain(domain: Pair<Rule>) -> Result<Domain, String> {
184        Ok(match domain.as_rule() {
185            Rule::domain => Domain::name(domain.as_str())?,
186            Rule::address_literal => {
187                let literal = domain.into_inner().next().unwrap();
188                match literal.as_rule() {
189                    Rule::ipv4_address_literal => Domain::V4(literal.as_str().to_string()),
190                    Rule::ipv6_address_literal => {
191                        Domain::V6(literal.into_inner().next().unwrap().as_str().to_string())
192                    }
193                    Rule::general_address_literal => {
194                        let mut literal = literal.into_inner();
195                        let tag = literal.next().unwrap().as_str().to_string();
196                        let literal = literal.next().unwrap().as_str().to_string();
197                        Domain::Tagged { tag, literal }
198                    }
199
200                    _ => unreachable!(),
201                }
202            }
203            _ => unreachable!(),
204        })
205    }
206
207    fn parse_mailbox(mut mailbox: Pairs<Rule>) -> Result<Mailbox, String> {
208        let local_part = mailbox.next().unwrap().as_str().to_string();
209        let domain = Self::parse_domain(mailbox.next().unwrap())?;
210        Ok(Mailbox { local_part, domain })
211    }
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
215pub enum ReversePath {
216    Path(MailPath),
217    NullSender,
218}
219
220impl ReversePath {
221    pub fn is_ascii(&self) -> bool {
222        match self {
223            Self::Path(path) => path.is_ascii(),
224            Self::NullSender => true,
225        }
226    }
227}
228
229impl TryFrom<&str> for ReversePath {
230    type Error = String;
231    fn try_from(s: &str) -> Result<Self, Self::Error> {
232        if s.is_empty() {
233            Ok(Self::NullSender)
234        } else {
235            let fields: Vec<&str> = s.split('@').collect();
236            if fields.len() == 2 {
237                Ok(Self::Path(MailPath {
238                    at_domain_list: vec![],
239                    mailbox: Mailbox {
240                        local_part: fields[0].to_string(),
241                        domain: Domain::name(fields[1])?,
242                    },
243                }))
244            } else {
245                Err(format!("{s} has the wrong number of @ signs"))
246            }
247        }
248    }
249}
250
251impl ToString for ReversePath {
252    fn to_string(&self) -> String {
253        match self {
254            Self::Path(p) => p.to_string(),
255            Self::NullSender => "".to_string(),
256        }
257    }
258}
259
260#[derive(Debug, Clone, PartialEq, Eq)]
261pub enum ForwardPath {
262    Path(MailPath),
263    Postmaster,
264}
265
266impl ForwardPath {
267    pub fn is_ascii(&self) -> bool {
268        match self {
269            Self::Path(p) => p.is_ascii(),
270            Self::Postmaster => true,
271        }
272    }
273}
274
275impl TryFrom<&str> for ForwardPath {
276    type Error = String;
277    fn try_from(s: &str) -> Result<Self, Self::Error> {
278        if s.is_empty() {
279            Err("cannot send to null sender".to_string())
280        } else if s.eq_ignore_ascii_case("postmaster") {
281            Ok(Self::Postmaster)
282        } else {
283            let fields: Vec<&str> = s.split('@').collect();
284            if fields.len() == 2 {
285                Ok(Self::Path(MailPath {
286                    at_domain_list: vec![],
287                    mailbox: Mailbox {
288                        local_part: fields[0].to_string(),
289                        domain: Domain::name(fields[1])?,
290                    },
291                }))
292            } else {
293                Err(format!("{s} has the wrong number of @ signs"))
294            }
295        }
296    }
297}
298
299impl ToString for ForwardPath {
300    fn to_string(&self) -> String {
301        match self {
302            Self::Path(p) => p.to_string(),
303            Self::Postmaster => "postmaster".to_string(),
304        }
305    }
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct MailPath {
310    pub at_domain_list: Vec<String>,
311    pub mailbox: Mailbox,
312}
313
314impl MailPath {
315    pub fn is_ascii(&self) -> bool {
316        // Note: ignoring at_domain_list here per the to_string()
317        // implementation
318        self.mailbox.is_ascii()
319    }
320}
321
322impl ToString for MailPath {
323    fn to_string(&self) -> String {
324        // Note: RFC5321 says about at_domain_list:
325        // Note that this form, the so-called "source
326        // route", MUST BE accepted, SHOULD NOT be
327        // generated, and SHOULD be ignored.
328        // So we don't include it in the stringified
329        // version of MailPath
330        self.mailbox.to_string()
331    }
332}
333
334#[derive(Debug, Clone, PartialEq, Eq)]
335pub struct Mailbox {
336    pub local_part: String,
337    pub domain: Domain,
338}
339
340impl Mailbox {
341    pub fn is_ascii(&self) -> bool {
342        if !self.local_part.is_ascii() {
343            return false;
344        }
345        match &self.domain {
346            Domain::V4(s) | Domain::V6(s) | Domain::Name(s) => s.is_ascii(),
347            Domain::Tagged { tag, literal } => tag.is_ascii() && literal.is_ascii(),
348        }
349    }
350}
351
352impl ToString for Mailbox {
353    fn to_string(&self) -> String {
354        let domain = self.domain.to_string();
355        format!("{}@{}", self.local_part, domain)
356    }
357}
358
359#[derive(Debug, Clone, PartialEq, Eq)]
360pub enum Domain {
361    Name(String),
362    V4(String),
363    V6(String),
364    Tagged { tag: String, literal: String },
365}
366
367impl Domain {
368    pub fn name(name: &str) -> Result<Self, String> {
369        match idna::domain_to_ascii(name) {
370            Ok(name) => Ok(Self::Name(name)),
371            Err(_empty_error_type) => Err(format!("invalid IDNA domain {name}")),
372        }
373    }
374}
375
376impl ToString for Domain {
377    fn to_string(&self) -> String {
378        match self {
379            Self::Name(name) => name.to_string(),
380            Self::V4(addr) => format!("[{addr}]"),
381            Self::V6(addr) => format!("[IPv6:{addr}]"),
382            Self::Tagged { tag, literal } => format!("[{tag}:{literal}]"),
383        }
384    }
385}
386
387fn xtext_encode(s: &str) -> Result<String, String> {
388    let mut result = String::new();
389
390    for c in s.chars() {
391        let ival = c as u32;
392        if (ival >= 33 && ival <= 126) && c != '+' && c != '=' {
393            result.push(c);
394            continue;
395        }
396
397        if ival > 0xff {
398            return Err(format!("xtext_encode: char {c} cannot be xtext encoded"));
399        }
400
401        result.push_str(&format!("+{ival:02x}"));
402    }
403    Ok(result)
404}
405
406fn xtext_decode(s: &str) -> Result<String, String> {
407    let mut bytes = vec![];
408
409    let mut iter = s.chars();
410    while let Some(c) = iter.next() {
411        if c == '+' {
412            // Decode a hex char
413            let hi = iter
414                .next()
415                .ok_or_else(|| "xtext_decode: missing high nybble of hexchar".to_string())?;
416            let lo = iter
417                .next()
418                .ok_or_else(|| "xtext_decode: missing low nybble of hexchar".to_string())?;
419
420            let hi = hi
421                .to_digit(16)
422                .ok_or_else(|| "xtext_decode: high nybble is not a valid hexchar".to_string())?;
423            let lo = lo
424                .to_digit(16)
425                .ok_or_else(|| "xtext_decode: low nybble is not a valid hexchar".to_string())?;
426
427            let byte = ((hi << 4) | lo) as u8;
428
429            bytes.push(byte);
430        } else {
431            let mut utf8 = [0u8; 4];
432            bytes.extend_from_slice(c.encode_utf8(&mut utf8).as_bytes());
433        }
434    }
435
436    String::from_utf8(bytes)
437        .map_err(|err| format!("xtext_decode: decoded bytes are not valid utf8: {err:#}"))
438}
439
440#[cfg(test)]
441#[test]
442fn test_xtext() {
443    for (input, expect) in [
444        ("hello", "hello"),
445        ("extra+", "extra+2b"),
446        ("1+1=2", "1+2b1+3d2"),
447    ] {
448        let encoded = xtext_encode(input).unwrap();
449        assert_eq!(encoded, expect, "encode error input={input}");
450
451        let decoded = xtext_decode(&encoded).unwrap();
452        assert_eq!(decoded, input, "decode error input={input}");
453    }
454
455    assert_eq!(
456        xtext_encode("space👾").unwrap_err(),
457        "xtext_encode: char 👾 cannot be xtext encoded"
458    );
459}
460
461#[derive(Debug, Clone, PartialEq, Eq)]
462pub struct EsmtpParameter {
463    pub name: String,
464    pub value: Option<String>,
465}
466
467impl ToString for EsmtpParameter {
468    fn to_string(&self) -> String {
469        match &self.value {
470            Some(value) => format!("{}={}", self.name, value),
471            None => self.name.to_string(),
472        }
473    }
474}
475
476#[derive(Debug, Clone, PartialEq, Eq)]
477pub struct XClientParameter {
478    pub name: String,
479    pub value: String,
480}
481
482impl ToString for XClientParameter {
483    fn to_string(&self) -> String {
484        let value = match xtext_encode(&self.value) {
485            Ok(s) => s,
486            Err(s) => s,
487        };
488        format!("{}={value}", self.name)
489    }
490}
491
492#[derive(Debug, Clone, PartialEq, Eq)]
493pub enum Command {
494    Ehlo(Domain),
495    Helo(Domain),
496    Lhlo(Domain),
497    MailFrom {
498        address: ReversePath,
499        parameters: Vec<EsmtpParameter>,
500    },
501    RcptTo {
502        address: ForwardPath,
503        parameters: Vec<EsmtpParameter>,
504    },
505    Data,
506    DataDot,
507    Rset,
508    Quit,
509    Vrfy(String),
510    Expn(String),
511    Help(Option<String>),
512    Noop(Option<String>),
513    StartTls,
514    Auth {
515        sasl_mech: String,
516        initial_response: Option<String>,
517    },
518    XClient(Vec<XClientParameter>),
519}
520
521impl Command {
522    pub fn parse(line: &str) -> Result<Self, String> {
523        Parser::parse_command(line)
524    }
525
526    pub fn encode(&self) -> String {
527        match self {
528            Self::Ehlo(domain) => format!("EHLO {}\r\n", domain.to_string()),
529            Self::Helo(domain) => format!("HELO {}\r\n", domain.to_string()),
530            Self::Lhlo(domain) => format!("LHLO {}\r\n", domain.to_string()),
531            Self::MailFrom {
532                address,
533                parameters,
534            } => {
535                let mut params = String::new();
536                for p in parameters {
537                    params.push(' ');
538                    params.push_str(&p.to_string());
539                }
540
541                format!("MAIL FROM:<{}>{params}\r\n", address.to_string())
542            }
543            Self::RcptTo {
544                address,
545                parameters,
546            } => {
547                let mut params = String::new();
548                for p in parameters {
549                    params.push(' ');
550                    params.push_str(&p.to_string());
551                }
552
553                format!("RCPT TO:<{}>{params}\r\n", address.to_string())
554            }
555            Self::Data => "DATA\r\n".to_string(),
556            Self::DataDot => ".\r\n".to_string(),
557            Self::Rset => "RSET\r\n".to_string(),
558            Self::Quit => "QUIT\r\n".to_string(),
559            Self::StartTls => "STARTTLS\r\n".to_string(),
560            Self::Vrfy(param) => format!("VRFY {param}\r\n"),
561            Self::Expn(param) => format!("EXPN {param}\r\n"),
562            Self::Help(Some(param)) => format!("HELP {param}\r\n"),
563            Self::Help(None) => "HELP\r\n".to_string(),
564            Self::Noop(Some(param)) => format!("NOOP {param}\r\n"),
565            Self::Noop(None) => "NOOP\r\n".to_string(),
566            Self::Auth {
567                sasl_mech,
568                initial_response: None,
569            } => format!("AUTH {sasl_mech}\r\n"),
570            Self::Auth {
571                sasl_mech,
572                initial_response: Some(resp),
573            } => format!("AUTH {sasl_mech} {resp}\r\n"),
574            Self::XClient(params) => {
575                let mut s = String::new();
576                for p in params {
577                    s.push(' ');
578                    s.push_str(&p.to_string());
579                }
580                format!("XCLIENT{s}\r\n")
581            }
582        }
583    }
584
585    /// Timeouts for reading the response
586    pub fn client_timeout(&self, timeouts: &SmtpClientTimeouts) -> Duration {
587        match self {
588            Self::Helo(_) | Self::Ehlo(_) | Self::Lhlo(_) => timeouts.ehlo_timeout,
589            Self::MailFrom { .. } => timeouts.mail_from_timeout,
590            Self::RcptTo { .. } => timeouts.rcpt_to_timeout,
591            Self::Data { .. } => timeouts.data_timeout,
592            Self::DataDot => timeouts.data_dot_timeout,
593            Self::Rset => timeouts.rset_timeout,
594            Self::StartTls => timeouts.starttls_timeout,
595            Self::Quit | Self::Vrfy(_) | Self::Expn(_) | Self::Help(_) | Self::Noop(_) => {
596                timeouts.idle_timeout
597            }
598            Self::Auth { .. } => timeouts.auth_timeout,
599            Self::XClient { .. } => timeouts.auth_timeout, // FIXME: xclient specific timeout
600        }
601    }
602
603    /// Timeouts for writing the request
604    pub fn client_timeout_request(&self, timeouts: &SmtpClientTimeouts) -> Duration {
605        let one_minute = Duration::from_secs(60);
606        self.client_timeout(timeouts).min(one_minute)
607    }
608}
609
610pub fn is_valid_domain(text: &str) -> bool {
611    Parser::parse(Rule::complete_domain, text).is_ok()
612}
613
614#[cfg(test)]
615mod test {
616    use super::*;
617
618    #[test]
619    fn parse_single_verbs() {
620        assert_eq!(Parser::parse_command("data").unwrap(), Command::Data,);
621        assert_eq!(Parser::parse_command("Quit").unwrap(), Command::Quit,);
622        assert_eq!(Parser::parse_command("rset").unwrap(), Command::Rset,);
623    }
624
625    #[test]
626    fn parse_vrfy() {
627        assert_eq!(
628            Parser::parse_command("VRFY someone").unwrap(),
629            Command::Vrfy("someone".to_string())
630        );
631    }
632
633    #[test]
634    fn parse_expn() {
635        assert_eq!(
636            Parser::parse_command("expn someone").unwrap(),
637            Command::Expn("someone".to_string())
638        );
639    }
640
641    #[test]
642    fn parse_help() {
643        assert_eq!(Parser::parse_command("help").unwrap(), Command::Help(None),);
644        assert_eq!(
645            Parser::parse_command("help me").unwrap(),
646            Command::Help(Some("me".to_string())),
647        );
648    }
649
650    #[test]
651    fn parse_noop() {
652        assert_eq!(Parser::parse_command("noop").unwrap(), Command::Noop(None),);
653        assert_eq!(
654            Parser::parse_command("noop param").unwrap(),
655            Command::Noop(Some("param".to_string())),
656        );
657    }
658
659    #[test]
660    fn parse_ehlo() {
661        assert_eq!(
662            Parser::parse_command("EHLO there").unwrap(),
663            Command::Ehlo(Domain::Name("there".to_string()))
664        );
665        assert_eq!(
666            Parser::parse_command("EHLO [127.0.0.1]").unwrap(),
667            Command::Ehlo(Domain::V4("127.0.0.1".to_string()))
668        );
669    }
670
671    #[test]
672    fn parse_helo() {
673        assert_eq!(
674            Parser::parse_command("HELO there").unwrap(),
675            Command::Helo(Domain::Name("there".to_string()))
676        );
677        // The spec says that we cannot use address literals with,
678        // HELO, but some tools will still submit it and some MTAs
679        // will accept it, so we do too.
680        assert_eq!(
681            Parser::parse_command("EHLO [127.0.0.1]").unwrap(),
682            Command::Ehlo(Domain::V4("127.0.0.1".to_string()))
683        );
684    }
685
686    #[test]
687    fn parse_auth() {
688        assert_eq!(
689            Parser::parse_command("AUTH PLAIN dGVzdAB0ZXN0ADEyMzQ=").unwrap(),
690            Command::Auth {
691                sasl_mech: "PLAIN".to_string(),
692                initial_response: Some("dGVzdAB0ZXN0ADEyMzQ=".to_string()),
693            }
694        );
695        assert_eq!(
696            Parser::parse_command("AUTH PLAIN").unwrap(),
697            Command::Auth {
698                sasl_mech: "PLAIN".to_string(),
699                initial_response: None,
700            }
701        );
702    }
703
704    #[test]
705    fn parse_rcpt_to_punycode() {
706        assert_eq!(
707            Parser::parse_command("Rcpt To:<user@4bed.xn--5dbhlacyps5bf4a.com>")
708                .unwrap_err()
709                .to_string(),
710            "invalid IDNA domain 4bed.xn--5dbhlacyps5bf4a.com"
711        );
712    }
713
714    #[test]
715    fn parse_rcpt_to() {
716        assert_eq!(
717            Parser::parse_command("Rcpt To:<user@host>").unwrap(),
718            Command::RcptTo {
719                address: ForwardPath::Path(MailPath {
720                    at_domain_list: vec![],
721                    mailbox: Mailbox {
722                        local_part: "user".to_string(),
723                        domain: Domain::Name("host".to_string())
724                    }
725                }),
726                parameters: vec![],
727            }
728        );
729        assert_eq!(
730            Parser::parse_command("Rcpt To:<user@éxample.com>").unwrap(),
731            Command::RcptTo {
732                address: ForwardPath::Path(MailPath {
733                    at_domain_list: vec![],
734                    mailbox: Mailbox {
735                        local_part: "user".to_string(),
736                        domain: Domain::Name("xn--xample-9ua.com".to_string())
737                    }
738                }),
739                parameters: vec![],
740            }
741        );
742        assert_eq!(
743            Parser::parse_command("Rcpt To:<rené@host>").unwrap(),
744            Command::RcptTo {
745                address: ForwardPath::Path(MailPath {
746                    at_domain_list: vec![],
747                    mailbox: Mailbox {
748                        local_part: "rené".to_string(),
749                        domain: Domain::Name("host".to_string())
750                    }
751                }),
752                parameters: vec![],
753            }
754        );
755        assert_eq!(
756            Parser::parse_command("Rcpt To:user@host").unwrap(),
757            Command::RcptTo {
758                address: ForwardPath::Path(MailPath {
759                    at_domain_list: vec![],
760                    mailbox: Mailbox {
761                        local_part: "user".to_string(),
762                        domain: Domain::Name("host".to_string())
763                    }
764                }),
765                parameters: vec![],
766            }
767        );
768
769        assert_eq!(
770            Parser::parse_command("Rcpt To:  user@host").unwrap(),
771            Command::RcptTo {
772                address: ForwardPath::Path(MailPath {
773                    at_domain_list: vec![],
774                    mailbox: Mailbox {
775                        local_part: "user".to_string(),
776                        domain: Domain::Name("host".to_string())
777                    }
778                }),
779                parameters: vec![],
780            }
781        );
782
783        assert_eq!(
784            Parser::parse_command("Rcpt To:<admin@[2001:aaaa:bbbbb]>").unwrap(),
785            Command::RcptTo {
786                address: ForwardPath::Path(MailPath {
787                    at_domain_list: vec![],
788                    mailbox: Mailbox {
789                        local_part: "admin".to_string(),
790                        domain: Domain::Tagged {
791                            tag: "2001".to_string(),
792                            literal: "aaaa:bbbbb".to_string()
793                        }
794                    }
795                }),
796                parameters: vec![],
797            }
798        );
799
800        assert_eq!(
801            Domain::Tagged {
802                tag: "2001".to_string(),
803                literal: "aaaa:bbbbb".to_string()
804            }
805            .to_string(),
806            "[2001:aaaa:bbbbb]".to_string()
807        );
808
809        assert_eq!(
810            Parser::parse_command("Rcpt To:<\"asking for trouble\"@host.name>").unwrap(),
811            Command::RcptTo {
812                address: ForwardPath::Path(MailPath {
813                    at_domain_list: vec![],
814                    mailbox: Mailbox {
815                        local_part: "\"asking for trouble\"".to_string(),
816                        domain: Domain::Name("host.name".to_string())
817                    }
818                }),
819                parameters: vec![],
820            }
821        );
822
823        assert_eq!(
824            Parser::parse_command("Rcpt To:<PostMastER>").unwrap(),
825            Command::RcptTo {
826                address: ForwardPath::Postmaster,
827                parameters: vec![],
828            }
829        );
830
831        assert_eq!(
832            Parser::parse_command("Rcpt To:<user@host> woot").unwrap(),
833            Command::RcptTo {
834                address: ForwardPath::Path(MailPath {
835                    at_domain_list: vec![],
836                    mailbox: Mailbox {
837                        local_part: "user".to_string(),
838                        domain: Domain::Name("host".to_string())
839                    }
840                }),
841                parameters: vec![EsmtpParameter {
842                    name: "woot".to_string(),
843                    value: None
844                }],
845            }
846        );
847
848        assert_eq!(
849            Parser::parse_command("Rcpt To:user@host woot").unwrap_err(),
850            "must enclose address in <> if you want to use ESMTP parameters".to_string()
851        );
852    }
853
854    #[test]
855    fn parse_mail_from() {
856        assert_eq!(
857            Parser::parse_command("Mail FROM:<user@host>").unwrap(),
858            Command::MailFrom {
859                address: ReversePath::Path(MailPath {
860                    at_domain_list: vec![],
861                    mailbox: Mailbox {
862                        local_part: "user".to_string(),
863                        domain: Domain::Name("host".to_string())
864                    }
865                }),
866                parameters: vec![],
867            }
868        );
869
870        assert_eq!(
871            Parser::parse_command("Mail FROM:user@host").unwrap(),
872            Command::MailFrom {
873                address: ReversePath::Path(MailPath {
874                    at_domain_list: vec![],
875                    mailbox: Mailbox {
876                        local_part: "user".to_string(),
877                        domain: Domain::Name("host".to_string())
878                    }
879                }),
880                parameters: vec![],
881            }
882        );
883
884        assert_eq!(
885            Parser::parse_command("Mail FROM:user@host foo bar=baz").unwrap_err(),
886            "must enclose address in <> if you want to use ESMTP parameters".to_string()
887        );
888
889        assert_eq!(
890            Parser::parse_command("Mail FROM:<user@host> foo bar=baz").unwrap(),
891            Command::MailFrom {
892                address: ReversePath::Path(MailPath {
893                    at_domain_list: vec![],
894                    mailbox: Mailbox {
895                        local_part: "user".to_string(),
896                        domain: Domain::Name("host".to_string())
897                    }
898                }),
899                parameters: vec![
900                    EsmtpParameter {
901                        name: "foo".to_string(),
902                        value: None,
903                    },
904                    EsmtpParameter {
905                        name: "bar".to_string(),
906                        value: Some("baz".to_string()),
907                    }
908                ],
909            }
910        );
911
912        assert_eq!(
913            Parser::parse_command("mail from:<user@[10.0.0.1]>").unwrap(),
914            Command::MailFrom {
915                address: ReversePath::Path(MailPath {
916                    at_domain_list: vec![],
917                    mailbox: Mailbox {
918                        local_part: "user".to_string(),
919                        domain: Domain::V4("10.0.0.1".to_string())
920                    }
921                }),
922                parameters: vec![],
923            }
924        );
925
926        assert_eq!(
927            Parser::parse_command("mail from:<user@[IPv6:::1]>").unwrap(),
928            Command::MailFrom {
929                address: ReversePath::Path(MailPath {
930                    at_domain_list: vec![],
931                    mailbox: Mailbox {
932                        local_part: "user".to_string(),
933                        domain: Domain::V6("::1".to_string())
934                    }
935                }),
936                parameters: vec![],
937            }
938        );
939
940        assert_eq!(
941            Mailbox {
942                local_part: "user".to_string(),
943                domain: Domain::V6("::1".to_string())
944            }
945            .to_string(),
946            "user@[IPv6:::1]".to_string()
947        );
948
949        assert_eq!(
950            Parser::parse_command("mail from:<user@[future:something]>").unwrap(),
951            Command::MailFrom {
952                address: ReversePath::Path(MailPath {
953                    at_domain_list: vec![],
954                    mailbox: Mailbox {
955                        local_part: "user".to_string(),
956                        domain: Domain::Tagged {
957                            tag: "future".to_string(),
958                            literal: "something".to_string()
959                        }
960                    }
961                }),
962                parameters: vec![],
963            }
964        );
965
966        assert_eq!(
967            Parser::parse_command("MAIL FROM:<@hosta.int,@jkl.org:userc@d.bar.org>").unwrap(),
968            Command::MailFrom {
969                address: ReversePath::Path(MailPath {
970                    at_domain_list: vec!["hosta.int".to_string(), "jkl.org".to_string()],
971                    mailbox: Mailbox {
972                        local_part: "userc".to_string(),
973                        domain: Domain::Name("d.bar.org".to_string())
974                    }
975                }),
976                parameters: vec![],
977            }
978        );
979    }
980
981    #[test]
982    fn parse_domain() {
983        assert!(is_valid_domain("hello"));
984        assert!(is_valid_domain("he-llo"));
985        assert!(is_valid_domain("he.llo"));
986        assert!(is_valid_domain("he.llo-"));
987    }
988
989    #[test]
990    fn parse_xclient() {
991        assert_eq!(
992            Parser::parse_command("XCLIENT NAME=spike.porcupine.org ADDR=10.0.0.1").unwrap(),
993            Command::XClient(vec![
994                XClientParameter {
995                    name: "NAME".to_string(),
996                    value: "spike.porcupine.org".to_string()
997                },
998                XClientParameter {
999                    name: "ADDR".to_string(),
1000                    value: "10.0.0.1".to_string()
1001                },
1002            ])
1003        );
1004    }
1005}
1006
1007/* ABNF from RFC 5321
1008
1009mail = "MAIL FROM:" Reverse-path [SP Mail-parameters] CRLF
1010
1011rcpt = "RCPT TO:" ( "<Postmaster@" Domain ">" / "<Postmaster>" /
1012                Forward-path ) [SP Rcpt-parameters] CRLF
1013
1014                Note that, in a departure from the usual rules for
1015                local-parts, the "Postmaster" string shown above is
1016                treated as case-insensitive.
1017
1018Reverse-path   = Path / "<>"
1019Forward-path   = Path
1020Path           = "<" [ A-d-l ":" ] Mailbox ">"
1021A-d-l          = At-domain *( "," At-domain )
1022                  ; Note that this form, the so-called "source
1023                  ; route", MUST BE accepted, SHOULD NOT be
1024                  ; generated, and SHOULD be ignored.
1025At-domain      = "@" Domain
1026Mail-parameters  = esmtp-param *(SP esmtp-param)
1027
1028   Rcpt-parameters  = esmtp-param *(SP esmtp-param)
1029
1030   esmtp-param    = esmtp-keyword ["=" esmtp-value]
1031
1032   esmtp-keyword  = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
1033
1034   esmtp-value    = 1*(%d33-60 / %d62-126)
1035                  ; any CHAR excluding "=", SP, and control
1036                  ; characters.  If this string is an email address,
1037                  ; i.e., a Mailbox, then the "xtext" syntax [32]
1038                  ; SHOULD be used.
1039
1040   Keyword        = Ldh-str
1041
1042   Argument       = Atom
1043
1044   Domain         = sub-domain *("." sub-domain)
1045   sub-domain     = Let-dig [Ldh-str]
1046
1047   Let-dig        = ALPHA / DIGIT
1048
1049   Ldh-str        = *( ALPHA / DIGIT / "-" ) Let-dig
1050
1051   address-literal  = "[" ( IPv4-address-literal /
1052                    IPv6-address-literal /
1053                    General-address-literal ) "]"
1054                    ; See Section 4.1.3
1055
1056   Mailbox        = Local-part "@" ( Domain / address-literal )
1057
1058   Local-part     = Dot-string / Quoted-string
1059                  ; MAY be case-sensitive
1060
1061
1062   Dot-string     = Atom *("."  Atom)
1063
1064   Atom           = 1*atext
1065
1066   Quoted-string  = DQUOTE *QcontentSMTP DQUOTE
1067
1068   QcontentSMTP   = qtextSMTP / quoted-pairSMTP
1069
1070   quoted-pairSMTP  = %d92 %d32-126
1071                    ; i.e., backslash followed by any ASCII
1072                    ; graphic (including itself) or SPace
1073
1074   qtextSMTP      = %d32-33 / %d35-91 / %d93-126
1075                  ; i.e., within a quoted string, any
1076                  ; ASCII graphic or space is permitted
1077                  ; without blackslash-quoting except
1078                  ; double-quote and the backslash itself.
1079
1080   String         = Atom / Quoted-string
1081
1082
1083      IPv4-address-literal  = Snum 3("."  Snum)
1084
1085   IPv6-address-literal  = "IPv6:" IPv6-addr
1086
1087   General-address-literal  = Standardized-tag ":" 1*dcontent
1088
1089   Standardized-tag  = Ldh-str
1090                     ; Standardized-tag MUST be specified in a
1091                     ; Standards-Track RFC and registered with IANA
1092
1093
1094   dcontent       = %d33-90 / ; Printable US-ASCII
1095                  %d94-126 ; excl. "[", "\", "]"
1096
1097   Snum           = 1*3DIGIT
1098                  ; representing a decimal integer
1099                  ; value in the range 0 through 255
1100
1101   IPv6-addr      = IPv6-full / IPv6-comp / IPv6v4-full / IPv6v4-comp
1102
1103   IPv6-hex       = 1*4HEXDIG
1104
1105   IPv6-full      = IPv6-hex 7(":" IPv6-hex)
1106
1107   IPv6-comp      = [IPv6-hex *5(":" IPv6-hex)] "::"
1108                  [IPv6-hex *5(":" IPv6-hex)]
1109                  ; The "::" represents at least 2 16-bit groups of
1110                  ; zeros.  No more than 6 groups in addition to the
1111                  ; "::" may be present.
1112
1113   IPv6v4-full    = IPv6-hex 5(":" IPv6-hex) ":" IPv4-address-literal
1114
1115   IPv6v4-comp    = [IPv6-hex *3(":" IPv6-hex)] "::"
1116                  [IPv6-hex *3(":" IPv6-hex) ":"]
1117                  IPv4-address-literal
1118                  ; The "::" represents at least 2 16-bit groups of
1119                  ; zeros.  No more than 4 groups in addition to the
1120                  ; "::" and IPv4-address-literal may be present.
1121
1122
1123   ehlo           = "EHLO" SP ( Domain / address-literal ) CRLF
1124   helo           = "HELO" SP Domain CRLF
1125
1126   ehlo-ok-rsp    = ( "250" SP Domain [ SP ehlo-greet ] CRLF )
1127                    / ( "250-" Domain [ SP ehlo-greet ] CRLF
1128                    *( "250-" ehlo-line CRLF )
1129                    "250" SP ehlo-line CRLF )
1130
1131   ehlo-greet     = 1*(%d0-9 / %d11-12 / %d14-127)
1132                    ; string of any characters other than CR or LF
1133
1134   ehlo-line      = ehlo-keyword *( SP ehlo-param )
1135
1136   ehlo-keyword   = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
1137                    ; additional syntax of ehlo-params depends on
1138                    ; ehlo-keyword
1139
1140   ehlo-param     = 1*(%d33-126)
1141                    ; any CHAR excluding <SP> and all
1142                    ; control characters (US-ASCII 0-31 and 127
1143                    ; inclusive)
1144
1145    data = "DATA" CRLF
1146    rset = "RSET" CRLF
1147    vrfy = "VRFY" SP String CRLF
1148    expn = "EXPN" SP String CRLF
1149    help = "HELP" [ SP String ] CRLF
1150    noop = "NOOP" [ SP String ] CRLF
1151quit = "QUIT" CRLF
1152
1153
1154*/