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 self.mailbox.is_ascii()
306 }
307}
308
309impl ToString for MailPath {
310 fn to_string(&self) -> String {
311 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 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 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 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