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