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