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 self.mailbox.is_ascii()
346 }
347}
348
349impl ToString for MailPath {
350 fn to_string(&self) -> String {
351 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 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 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, }
628 }
629
630 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 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