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