1use crate::client_types::SmtpClientTimeouts;
2#[cfg(test)]
3use bstr::BStr;
4use bstr::{BString, ByteSlice};
5#[cfg(feature = "lua")]
6use mlua::{FromLua, MetaMethod, UserData, UserDataFields, UserDataMethods};
7use nom::branch::alt;
8use nom::bytes::complete::{take_while1, take_while_m_n};
9use nom::combinator::{all_consuming, map, map_res, opt, recognize};
10use nom::error::context;
11use nom::multi::{many0, many1};
12use nom::sequence::pair;
13use nom::Parser;
14use nom_utils::{
15 domain_name, explain_nom, ipv4_address, ipv6_address, make_span, tag, tag_no_case,
16 utf8_non_ascii, DomainString, IResult, Span,
17};
18use pastey::paste;
19use std::borrow::Cow;
20use std::hash::{Hash, Hasher};
21use std::net::{Ipv4Addr, Ipv6Addr};
22use std::time::Duration;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum CommandVerb {
26 Ehlo,
27 Helo,
28 Lhlo,
29 Mail,
30 Rcpt,
31 Data,
32 Rset,
33 Quit,
34 Vrfy,
35 Expn,
36 Help,
37 Noop,
38 StartTls,
39 Auth,
40 XClient,
41 Unknown(BString),
42}
43
44#[derive(Clone, PartialEq, Eq, Hash)]
46pub enum Domain {
47 DomainName(DomainString),
49 V4(Ipv4Addr),
51 V6(Ipv6Addr),
53 Tagged(String),
57}
58
59impl std::fmt::Debug for Domain {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 Domain::DomainName(s) => write!(f, "{}", s),
63 Domain::V4(ip) => write!(f, "[{}]", ip),
64 Domain::V6(ip) => write!(f, "[IPv6:{}]", ip),
65 Domain::Tagged(s) => write!(f, "[{}]", s),
66 }
67 }
68}
69
70impl std::fmt::Display for Domain {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 match self {
73 Domain::DomainName(s) => write!(f, "{}", s),
74 Domain::V4(ip) => write!(f, "[{}]", ip),
75 Domain::V6(ip) => write!(f, "[IPv6:{}]", ip),
76 Domain::Tagged(s) => write!(f, "[{}]", s),
77 }
78 }
79}
80
81impl Domain {
82 pub fn is_ascii(&self) -> bool {
88 match self {
89 Domain::DomainName(_) | Domain::V4(_) | Domain::V6(_) => true,
90 Domain::Tagged(s) => s.is_ascii(),
91 }
92 }
93}
94
95#[derive(Clone, Debug)]
97pub struct Mailbox {
98 pub(crate) local_part: String,
99 pub domain: Domain,
100}
101
102impl PartialEq for Mailbox {
103 fn eq(&self, other: &Self) -> bool {
104 self.local_part() == other.local_part() && self.domain == other.domain
105 }
106}
107
108impl Eq for Mailbox {}
109
110impl Mailbox {
111 pub fn is_ascii(&self) -> bool {
113 self.local_part.is_ascii() && self.domain.is_ascii()
114 }
115
116 pub fn local_part(&self) -> Cow<'_, str> {
121 if self.local_part.starts_with('"') {
123 let mut result = String::new();
125 let mut chars = self.local_part.chars();
126 chars.next(); while let Some(c) = chars.next() {
128 match c {
129 '\\' => match chars.next() {
130 Some(c) => {
131 result.push(c);
132 }
133 None => {
134 result.push('\\');
135 }
136 },
137 '"' => {
138 continue;
141 }
142 c => {
143 result.push(c);
144 }
145 }
146 }
147 Cow::Owned(result)
148 } else {
149 Cow::Borrowed(self.local_part.as_str())
150 }
151 }
152}
153
154#[cfg(test)]
155mod mailbox_tests {
156 use super::*;
157
158 #[test]
159 fn test_mailbox_local_part_normalized() {
160 let mb1 = Mailbox {
162 local_part: String::from("foo"),
163 domain: Domain::DomainName("example.com".parse().unwrap()),
164 };
165 let mb2 = Mailbox {
166 local_part: String::from("\"foo\""),
167 domain: Domain::DomainName("example.com".parse().unwrap()),
168 };
169 let mb3 = Mailbox {
170 local_part: String::from("\"f\\oo\""),
171 domain: Domain::DomainName("example.com".parse().unwrap()),
172 };
173
174 k9::assert_equal!(mb1.local_part(), "foo");
176 k9::assert_equal!(mb2.local_part(), "foo");
177 k9::assert_equal!(mb3.local_part(), "foo");
178 }
179
180 #[test]
181 fn test_mailbox_local_part_eq_normalized() {
182 let mb1 = Mailbox {
184 local_part: String::from("foo"),
185 domain: Domain::DomainName("example.com".parse().unwrap()),
186 };
187 let mb2 = Mailbox {
188 local_part: String::from("\"foo\""),
189 domain: Domain::DomainName("example.com".parse().unwrap()),
190 };
191 let mb3 = Mailbox {
192 local_part: String::from("\"f\\oo\""),
193 domain: Domain::DomainName("example.com".parse().unwrap()),
194 };
195
196 k9::assert_equal!(mb1, mb2);
198 k9::assert_equal!(mb2, mb3);
199 k9::assert_equal!(mb1, mb3);
200 }
201
202 #[test]
203 fn test_mailbox_local_part_unquoted_borrowed() {
204 let mb = Mailbox {
206 local_part: String::from("foo"),
207 domain: Domain::DomainName("example.com".parse().unwrap()),
208 };
209
210 let local_part = mb.local_part();
211 match local_part {
212 Cow::Borrowed(_) => {}
213 Cow::Owned(_) => panic!("Expected Cow::Borrowed for unquoted valid UTF-8"),
214 }
215 }
216
217 #[test]
218 fn test_mailbox_local_part_quoted_unquoted_borrowed() {
219 let mb = Mailbox {
221 local_part: String::from("\"foo\""),
222 domain: Domain::DomainName("example.com".parse().unwrap()),
223 };
224
225 let local_part = mb.local_part();
226 match local_part {
227 Cow::Borrowed(_) => panic!("Expected Cow::Owned for quoted string"),
228 Cow::Owned(s) => {
229 k9::assert_equal!(s, "foo");
230 }
231 }
232 }
233}
234
235impl Hash for Mailbox {
236 fn hash<H: Hasher>(&self, state: &mut H) {
237 self.local_part().hash(state);
238 self.domain.hash(state);
239 }
240}
241
242#[derive(Clone, PartialEq, Eq, Hash)]
247pub struct MailPath {
248 pub at_domain_list: Vec<String>,
250 pub mailbox: Mailbox,
252}
253
254impl std::fmt::Debug for MailPath {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 let mut local_part = Vec::new();
257
258 if !self.at_domain_list.is_empty() {
260 for (i, domain) in self.at_domain_list.iter().enumerate() {
261 if i > 0 {
262 local_part.push(b',');
263 }
264 local_part.push(b'@'); local_part.extend_from_slice(domain.as_bytes());
266 }
267 local_part.push(b':');
268 }
269
270 local_part.extend_from_slice(self.mailbox.local_part.as_bytes());
272
273 write!(
275 f,
276 "MailPath(\"{}@{:?}\")",
277 local_part.escape_bytes(),
278 self.mailbox.domain
279 )
280 }
281}
282
283impl std::fmt::Display for MailPath {
284 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285 if !self.at_domain_list.is_empty() {
286 for (i, domain) in self.at_domain_list.iter().enumerate() {
287 if i > 0 {
288 f.write_str(",")?;
289 }
290 write!(f, "@{domain}")?;
291 }
292 f.write_str(":")?;
293 }
294 write!(f, "{}@{}", self.mailbox.local_part, self.mailbox.domain)
295 }
296}
297
298impl MailPath {
299 pub fn is_ascii(&self) -> bool {
304 self.mailbox.is_ascii()
305 }
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
310pub enum ReversePath {
311 Path(MailPath),
313 NullSender,
315}
316
317impl ReversePath {
318 pub fn is_ascii(&self) -> bool {
320 match self {
321 Self::NullSender => true,
322 Self::Path(p) => p.is_ascii(),
323 }
324 }
325}
326
327impl TryFrom<&str> for ReversePath {
328 type Error = anyhow::Error;
329 fn try_from(s: &str) -> Result<Self, Self::Error> {
330 EnvelopeAddress::parse(s)?
331 .try_into()
332 .map_err(|e: &'static str| anyhow::anyhow!(e))
333 }
334}
335
336impl std::fmt::Display for ReversePath {
337 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338 match self {
339 Self::NullSender => Ok(()),
340 Self::Path(p) => p.fmt(f),
341 }
342 }
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, Hash)]
347pub enum ForwardPath {
348 Path(MailPath),
350 Postmaster,
352}
353
354impl ForwardPath {
355 pub fn is_ascii(&self) -> bool {
357 match self {
358 Self::Postmaster => true,
359 Self::Path(p) => p.is_ascii(),
360 }
361 }
362}
363
364impl TryFrom<&str> for ForwardPath {
365 type Error = anyhow::Error;
366 fn try_from(s: &str) -> Result<Self, Self::Error> {
367 EnvelopeAddress::parse(s)?
368 .try_into()
369 .map_err(|e: &'static str| anyhow::anyhow!(e))
370 }
371}
372
373impl std::fmt::Display for ForwardPath {
374 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375 match self {
376 Self::Postmaster => write!(f, "Postmaster"),
377 Self::Path(p) => p.fmt(f),
378 }
379 }
380}
381
382#[derive(Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
384#[serde(try_from = "String", into = "String")]
385pub enum EnvelopeAddress {
386 Null,
388 Postmaster,
390 Path(MailPath),
392}
393
394impl std::fmt::Display for EnvelopeAddress {
395 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
396 match self {
397 EnvelopeAddress::Null => write!(f, ""),
398 EnvelopeAddress::Postmaster => write!(f, "Postmaster"),
399 EnvelopeAddress::Path(path) => {
402 write!(f, "{}@{}", path.mailbox.local_part, path.mailbox.domain)
403 }
404 }
405 }
406}
407
408impl std::fmt::Debug for EnvelopeAddress {
409 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410 write!(f, "<{}>", self)
411 }
412}
413
414impl From<MailPath> for EnvelopeAddress {
415 fn from(path: MailPath) -> Self {
416 EnvelopeAddress::Path(path)
417 }
418}
419
420impl TryFrom<String> for EnvelopeAddress {
421 type Error = anyhow::Error;
422
423 fn try_from(s: String) -> Result<Self, Self::Error> {
424 EnvelopeAddress::parse(&s)
425 }
426}
427
428impl From<EnvelopeAddress> for String {
429 fn from(addr: EnvelopeAddress) -> Self {
430 addr.to_string()
431 }
432}
433
434impl std::str::FromStr for EnvelopeAddress {
435 type Err = String;
436 fn from_str(s: &str) -> Result<Self, Self::Err> {
437 EnvelopeAddress::parse_impl(s)
438 }
439}
440
441impl From<ForwardPath> for EnvelopeAddress {
442 fn from(fp: ForwardPath) -> Self {
443 match fp {
444 ForwardPath::Postmaster => EnvelopeAddress::Postmaster,
445 ForwardPath::Path(p) => EnvelopeAddress::Path(p),
446 }
447 }
448}
449
450impl TryFrom<ReversePath> for EnvelopeAddress {
451 type Error = &'static str;
452 fn try_from(rp: ReversePath) -> Result<Self, Self::Error> {
453 match rp {
454 ReversePath::NullSender => Ok(EnvelopeAddress::Null),
455 ReversePath::Path(p) => Ok(EnvelopeAddress::Path(p)),
456 }
457 }
458}
459
460impl EnvelopeAddress {
461 fn parse_impl(input: &str) -> Result<EnvelopeAddress, String> {
467 if input.is_empty() {
473 return Ok(EnvelopeAddress::Null);
474 }
475
476 let input = make_span(input.as_bytes());
477 let (_, result) = all_consuming(alt((
478 map(tag_no_case("<>"), |_| EnvelopeAddress::Null),
479 map(tag_no_case("<Postmaster>"), |_| EnvelopeAddress::Postmaster),
480 map(path, EnvelopeAddress::Path),
481 map(mailbox, EnvelopeAddress::from),
482 map(tag_no_case("Postmaster"), |_| EnvelopeAddress::Postmaster),
483 )))
484 .parse(input)
485 .map_err(|e| explain_nom(input, e))?;
486 Ok(result)
487 }
488
489 pub fn parse(input: &str) -> anyhow::Result<Self> {
494 EnvelopeAddress::parse_impl(input).map_err(|e| anyhow::anyhow!(e))
495 }
496
497 pub fn user(&self) -> String {
500 match self {
501 EnvelopeAddress::Postmaster => "postmaster".to_string(),
502 EnvelopeAddress::Null => "".to_string(),
503 EnvelopeAddress::Path(path) => path.mailbox.local_part().into(),
504 }
505 }
506
507 pub fn domain(&self) -> String {
510 match self {
511 EnvelopeAddress::Postmaster | EnvelopeAddress::Null => "".to_string(),
512 EnvelopeAddress::Path(path) => path.mailbox.domain.to_string(),
513 }
514 }
515
516 pub fn null_sender() -> Self {
518 EnvelopeAddress::Null
519 }
520}
521
522#[cfg(feature = "lua")]
523impl FromLua for EnvelopeAddress {
524 fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> {
525 match value {
526 mlua::Value::String(s) => s
527 .to_str()?
528 .parse::<EnvelopeAddress>()
529 .map_err(|e: String| mlua::Error::RuntimeError(e)),
530 _ => {
531 let ud = mlua::UserDataRef::<EnvelopeAddress>::from_lua(value, lua)?;
532 Ok(ud.clone())
533 }
534 }
535 }
536}
537
538#[cfg(feature = "lua")]
539impl UserData for EnvelopeAddress {
540 fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
541 fields.add_field_method_get("user", |_, this| Ok(this.user()));
542 fields.add_field_method_get("domain", |_, this| Ok(this.domain()));
543 fields.add_field_method_get("email", |_, this| Ok(this.to_string()));
544 }
545
546 fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
547 methods.add_meta_method(MetaMethod::ToString, |_, this, _: ()| Ok(this.to_string()));
548 }
549}
550
551impl From<MailPath> for ReversePath {
552 fn from(path: MailPath) -> Self {
553 ReversePath::Path(path)
554 }
555}
556
557impl From<MailPath> for ForwardPath {
558 fn from(path: MailPath) -> Self {
559 ForwardPath::Path(path)
560 }
561}
562
563impl TryFrom<ReversePath> for MailPath {
564 type Error = &'static str;
565
566 fn try_from(path: ReversePath) -> Result<Self, Self::Error> {
567 match path {
568 ReversePath::Path(mailpath) => Ok(mailpath),
569 ReversePath::NullSender => Err("Cannot convert NullSender to MailPath"),
570 }
571 }
572}
573
574impl TryFrom<ForwardPath> for MailPath {
575 type Error = &'static str;
576
577 fn try_from(path: ForwardPath) -> Result<Self, Self::Error> {
578 match path {
579 ForwardPath::Path(mailpath) => Ok(mailpath),
580 ForwardPath::Postmaster => Err("Cannot convert Postmaster to MailPath"),
581 }
582 }
583}
584
585impl From<Mailbox> for MailPath {
591 fn from(mailbox: Mailbox) -> Self {
592 MailPath {
593 at_domain_list: vec![],
594 mailbox,
595 }
596 }
597}
598
599impl From<Mailbox> for EnvelopeAddress {
601 fn from(mailbox: Mailbox) -> Self {
602 EnvelopeAddress::Path(MailPath {
603 at_domain_list: vec![],
604 mailbox,
605 })
606 }
607}
608
609impl From<Mailbox> for ReversePath {
611 fn from(mailbox: Mailbox) -> Self {
612 ReversePath::Path(MailPath {
613 at_domain_list: vec![],
614 mailbox,
615 })
616 }
617}
618
619impl From<Mailbox> for ForwardPath {
621 fn from(mailbox: Mailbox) -> Self {
622 ForwardPath::Path(MailPath {
623 at_domain_list: vec![],
624 mailbox,
625 })
626 }
627}
628
629impl TryFrom<EnvelopeAddress> for Mailbox {
634 type Error = &'static str;
635
636 fn try_from(addr: EnvelopeAddress) -> Result<Self, Self::Error> {
637 match addr {
638 EnvelopeAddress::Path(path) => Ok(path.mailbox),
639 EnvelopeAddress::Null => Err("Cannot convert Null to Mailbox"),
640 EnvelopeAddress::Postmaster => Err("Cannot convert Postmaster to Mailbox"),
641 }
642 }
643}
644
645impl TryFrom<ReversePath> for Mailbox {
646 type Error = &'static str;
647
648 fn try_from(path: ReversePath) -> Result<Self, Self::Error> {
649 match path {
650 ReversePath::Path(path) => Ok(path.mailbox),
651 ReversePath::NullSender => Err("Cannot convert NullSender to Mailbox"),
652 }
653 }
654}
655
656impl TryFrom<ForwardPath> for Mailbox {
657 type Error = &'static str;
658
659 fn try_from(path: ForwardPath) -> Result<Self, Self::Error> {
660 match path {
661 ForwardPath::Path(path) => Ok(path.mailbox),
662 ForwardPath::Postmaster => Err("Cannot convert Postmaster to Mailbox"),
663 }
664 }
665}
666
667impl TryFrom<EnvelopeAddress> for MailPath {
672 type Error = &'static str;
673
674 fn try_from(addr: EnvelopeAddress) -> Result<Self, Self::Error> {
675 match addr {
676 EnvelopeAddress::Path(path) => Ok(path),
677 EnvelopeAddress::Null => Err("Cannot convert Null to MailPath"),
678 EnvelopeAddress::Postmaster => Err("Cannot convert Postmaster to MailPath"),
679 }
680 }
681}
682
683impl TryFrom<ReversePath> for ForwardPath {
684 type Error = &'static str;
685
686 fn try_from(path: ReversePath) -> Result<Self, Self::Error> {
687 match path {
688 ReversePath::Path(mailpath) => Ok(ForwardPath::Path(mailpath)),
689 ReversePath::NullSender => Err("Cannot convert NullSender to ForwardPath"),
690 }
691 }
692}
693
694impl TryFrom<ForwardPath> for ReversePath {
695 type Error = &'static str;
696
697 fn try_from(path: ForwardPath) -> Result<Self, Self::Error> {
698 match path {
699 ForwardPath::Path(mailpath) => Ok(ReversePath::Path(mailpath)),
700 ForwardPath::Postmaster => Err("Cannot convert Postmaster to ReversePath"),
701 }
702 }
703}
704
705impl TryFrom<EnvelopeAddress> for ReversePath {
706 type Error = &'static str;
707
708 fn try_from(addr: EnvelopeAddress) -> Result<Self, Self::Error> {
709 match addr {
710 EnvelopeAddress::Path(path) => Ok(ReversePath::Path(path)),
711 EnvelopeAddress::Null => Ok(ReversePath::NullSender),
712 EnvelopeAddress::Postmaster => Err("Cannot convert Postmaster to ReversePath"),
713 }
714 }
715}
716
717impl TryFrom<EnvelopeAddress> for ForwardPath {
718 type Error = &'static str;
719
720 fn try_from(addr: EnvelopeAddress) -> Result<Self, Self::Error> {
721 match addr {
722 EnvelopeAddress::Path(path) => Ok(ForwardPath::Path(path)),
723 EnvelopeAddress::Null => Err("Cannot convert Null to ForwardPath"),
724 EnvelopeAddress::Postmaster => Ok(ForwardPath::Postmaster),
725 }
726 }
727}
728
729#[derive(Clone, Debug, PartialEq, Eq)]
731pub struct EsmtpParameter {
732 pub name: String,
733 pub value: Option<String>,
734}
735
736#[derive(Debug, Clone, PartialEq, Eq)]
740pub struct XClientParameter {
741 pub name: String,
742 pub value: String,
743}
744
745impl XClientParameter {
746 pub fn is_name(&self, name: impl AsRef<str>) -> bool {
748 self.name.eq_ignore_ascii_case(name.as_ref())
749 }
750
751 pub fn parse<T>(&self) -> Result<T, String>
755 where
756 T: std::str::FromStr,
757 T::Err: std::fmt::Display,
758 {
759 let parsed: Result<T, T::Err> = self.value.parse();
760 parsed.map_err(|e| e.to_string())
761 }
762}
763
764#[derive(Clone, PartialEq, Eq)]
765pub enum Command {
766 Ehlo(Domain),
767 Helo(Domain),
768 Lhlo(Domain),
769 Noop(Option<String>),
770 Help(Option<String>),
771 Vrfy(Option<String>),
772 Expn(Option<String>),
773 Data,
774 DataDot,
781 Rset,
782 Quit,
783 StartTls,
784 MailFrom {
785 address: ReversePath,
786 parameters: Vec<EsmtpParameter>,
787 },
788 RcptTo {
789 address: ForwardPath,
790 parameters: Vec<EsmtpParameter>,
791 },
792 Auth {
793 sasl_mech: String,
794 initial_response: Option<String>,
795 },
796 XClient(Vec<XClientParameter>),
797 Unknown(BString),
798}
799
800impl std::fmt::Debug for Command {
801 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
802 let encoded = self.encode();
804 write!(f, "Command(\"{}\")", encoded.escape_bytes())
805 }
806}
807
808#[derive(Clone, Debug, PartialEq, Eq)]
810pub enum PartialReason {
811 Syntax,
813 InvalidSenderAddress,
816 InvalidRecipientAddress,
819}
820
821#[derive(Clone, PartialEq, Eq)]
822pub enum MaybePartialCommand {
823 Full(Command),
824 Partial {
825 verb: CommandVerb,
826 remainder: BString,
827 reason: PartialReason,
828 },
829}
830
831impl std::fmt::Debug for MaybePartialCommand {
832 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
833 match self {
834 MaybePartialCommand::Full(cmd) => write!(f, "Full({cmd:?})"),
835 MaybePartialCommand::Partial {
836 verb,
837 remainder,
838 reason,
839 } => {
840 write!(
841 f,
842 "Partial {{ verb: {verb:?}, remainder: {:?}, reason: {reason:?} }}",
843 remainder.escape_bytes()
844 )
845 }
846 }
847 }
848}
849
850macro_rules! parse_single {
851 ($func_name:ident, $token:literal, $verb:ident) => {
852 fn $func_name(input: Span) -> IResult<Span, MaybePartialCommand> {
853 context(
854 $token,
855 alt((
856 map(
857 all_consuming((tag_no_case($token), wsp, anything)),
858 |(_cmd, _space, remainder)| MaybePartialCommand::Partial {
859 verb: CommandVerb::$verb,
860 remainder: (*remainder).into(),
861 reason: PartialReason::Syntax,
862 },
863 ),
864 map(all_consuming(tag_no_case($token)), |_| {
865 MaybePartialCommand::Full(Command::$verb)
866 }),
867 )),
868 )
869 .parse(input)
870 }
871
872 paste! {
873 #[cfg(test)]
874 #[test]
875 fn [<test_ $func_name>]() {
876 k9::assert_equal!(
877 unwrapper(Command::parse($token)),
878 MaybePartialCommand::Full(Command::$verb)
879 );
880 k9::assert_equal!(
881 unwrapper(Command::parse($token.to_lowercase())),
882 MaybePartialCommand::Full(Command::$verb)
883 );
884 k9::assert_equal!(
885 unwrapper(Command::parse(format!("{} trailing garbage", $token))),
886 MaybePartialCommand::Partial {
887 verb: CommandVerb::$verb,
888 remainder:"trailing garbage".into(),
889 reason: PartialReason::Syntax,
890 }
891 );
892 }
893 }
894 };
895}
896
897macro_rules! parse_opt_arg {
898 ($func_name:ident, $token:literal, $verb:ident) => {
899 fn $func_name(input: Span) -> IResult<Span, MaybePartialCommand> {
900 context(
901 $token,
902 alt((
903 map(
904 all_consuming((tag_no_case($token), wsp, string)),
905 |(_cmd, _space, param)| match String::from_utf8(param.fragment().to_vec()) {
906 Ok(s) => MaybePartialCommand::Full(Command::$verb(Some(s))),
907 Err(_) => MaybePartialCommand::Partial {
908 verb: CommandVerb::$verb,
909 remainder: BString::default(),
910 reason: PartialReason::Syntax,
911 },
912 },
913 ),
914 map(
915 all_consuming((tag_no_case($token), wsp, anything)),
916 |(_cmd, _space, remainder)| MaybePartialCommand::Partial {
917 verb: CommandVerb::$verb,
918 remainder: (*remainder).into(),
919 reason: PartialReason::Syntax,
920 },
921 ),
922 map(all_consuming(tag_no_case($token)), |_| {
923 MaybePartialCommand::Full(Command::$verb(None))
924 }),
925 )),
926 )
927 .parse(input)
928 }
929
930 paste! {
931 #[cfg(test)]
932 #[test]
933 fn [<test_ $func_name>]() {
934 k9::assert_equal!(
935 unwrapper(Command::parse($token)),
936 MaybePartialCommand::Full(Command::$verb(None)),
937 "full no param"
938 );
939 k9::assert_equal!(
940 unwrapper(Command::parse($token.to_lowercase())),
941 MaybePartialCommand::Full(Command::$verb(None)),
942 "full no param, different case"
943 );
944 k9::assert_equal!(
945 unwrapper(Command::parse(format!("{} parameter", $token))),
946 MaybePartialCommand::Full(Command::$verb(Some("parameter".into()))),
947 "full with param"
948 );
949 k9::assert_equal!(
950 unwrapper(Command::parse(format!("{} trailing garbage", $token))),
951 MaybePartialCommand::Partial {
952 verb: CommandVerb::$verb,
953 remainder:"trailing garbage".into(),
954 reason: PartialReason::Syntax,
955 },
956 "should have partial"
957 );
958 }
959 }
960 };
961}
962
963#[cfg(test)]
969fn unwrapper<T, E: std::fmt::Display>(result: Result<T, E>) -> T {
970 match result {
971 Ok(r) => r,
972 Err(err) => panic!("{err}"),
973 }
974}
975
976parse_opt_arg!(parse_noop, "NOOP", Noop);
977parse_opt_arg!(parse_help, "HELP", Help);
978parse_opt_arg!(parse_vrfy, "VRFY", Vrfy);
979parse_opt_arg!(parse_expn, "EXPN", Expn);
980parse_single!(parse_data, "DATA", Data);
981parse_single!(parse_rset, "RSET", Rset);
982parse_single!(parse_quit, "QUIT", Quit);
983parse_single!(parse_starttls, "STARTTLS", StartTls);
984
985fn parse_with<'a, R, F>(text: &'a [u8], parser: F) -> Result<R, String>
986where
987 F: Fn(Span<'a>) -> IResult<'a, Span<'a>, R>,
988{
989 let input = make_span(text);
990 let (_, result) = all_consuming(parser)
991 .parse(input)
992 .map_err(|err| explain_nom(input, err))?;
993 Ok(result)
994}
995
996impl Command {
997 pub fn parse(input: impl AsRef<[u8]>) -> Result<MaybePartialCommand, String> {
998 let bytes = input.as_ref();
1002 let bytes = bytes
1003 .strip_suffix(b"\r\n")
1004 .or_else(|| bytes.strip_suffix(b"\n"))
1005 .unwrap_or(bytes);
1006 parse_with(bytes, Self::parse_span)
1007 }
1008
1009 fn parse_span(input: Span) -> IResult<Span, MaybePartialCommand> {
1010 context(
1011 "command-verb",
1012 alt((
1013 parse_ehlo,
1014 parse_helo,
1015 parse_lhlo,
1016 parse_help,
1017 parse_noop,
1018 parse_vrfy,
1019 parse_expn,
1020 parse_data,
1021 parse_rset,
1022 parse_quit,
1023 parse_starttls,
1024 parse_mail_from,
1025 parse_rcpt_to,
1026 parse_auth,
1027 parse_xclient,
1028 Self::parse_unknown,
1029 )),
1030 )
1031 .parse(input)
1032 }
1033
1034 pub fn encode(&self) -> BString {
1042 let mut buf: Vec<u8> = Vec::new();
1043 match self {
1044 Self::Ehlo(domain) => {
1045 buf.extend_from_slice(b"EHLO ");
1046 buf.extend_from_slice(encode_domain(domain).as_ref());
1047 }
1048 Self::Helo(domain) => {
1049 buf.extend_from_slice(b"HELO ");
1050 buf.extend_from_slice(encode_domain(domain).as_ref());
1051 }
1052 Self::Lhlo(domain) => {
1053 buf.extend_from_slice(b"LHLO ");
1054 buf.extend_from_slice(encode_domain(domain).as_ref());
1055 }
1056 Self::Noop(None) => buf.extend_from_slice(b"NOOP"),
1057 Self::Noop(Some(s)) => {
1058 buf.extend_from_slice(b"NOOP ");
1059 buf.extend_from_slice(s.as_bytes());
1060 }
1061 Self::Help(None) => buf.extend_from_slice(b"HELP"),
1062 Self::Help(Some(s)) => {
1063 buf.extend_from_slice(b"HELP ");
1064 buf.extend_from_slice(s.as_bytes());
1065 }
1066 Self::Vrfy(None) => buf.extend_from_slice(b"VRFY"),
1067 Self::Vrfy(Some(s)) => {
1068 buf.extend_from_slice(b"VRFY ");
1069 buf.extend_from_slice(s.as_bytes());
1070 }
1071 Self::Expn(None) => buf.extend_from_slice(b"EXPN"),
1072 Self::Expn(Some(s)) => {
1073 buf.extend_from_slice(b"EXPN ");
1074 buf.extend_from_slice(s.as_bytes());
1075 }
1076 Self::Data => buf.extend_from_slice(b"DATA"),
1077 Self::DataDot => return BString::from(".\r\n"),
1080 Self::Rset => buf.extend_from_slice(b"RSET"),
1081 Self::Quit => buf.extend_from_slice(b"QUIT"),
1082 Self::StartTls => buf.extend_from_slice(b"STARTTLS"),
1083 Self::MailFrom {
1084 address,
1085 parameters,
1086 } => {
1087 buf.extend_from_slice(b"MAIL FROM:<");
1088 buf.extend(encode_reverse_path(address));
1089 buf.push(b'>');
1090 buf.extend(encode_esmtp_params(parameters));
1091 }
1092 Self::RcptTo {
1093 address,
1094 parameters,
1095 } => {
1096 buf.extend_from_slice(b"RCPT TO:<");
1097 buf.extend(encode_forward_path(address));
1098 buf.push(b'>');
1099 buf.extend(encode_esmtp_params(parameters));
1100 }
1101 Self::Auth {
1102 sasl_mech,
1103 initial_response: None,
1104 } => {
1105 buf.extend_from_slice(b"AUTH ");
1106 buf.extend_from_slice(sasl_mech.as_bytes());
1107 }
1108 Self::Auth {
1109 sasl_mech,
1110 initial_response: Some(resp),
1111 } => {
1112 buf.extend_from_slice(b"AUTH ");
1113 buf.extend_from_slice(sasl_mech.as_bytes());
1114 buf.push(b' ');
1115 buf.extend_from_slice(resp.as_bytes());
1116 }
1117 Self::XClient(params) => {
1118 buf.extend_from_slice(b"XCLIENT");
1119 buf.extend(encode_xclient_params(params));
1120 }
1121 Self::Unknown(s) => {
1122 buf.extend_from_slice(s);
1123 }
1124 }
1125 buf.extend_from_slice(b"\r\n");
1126 BString::from(buf)
1127 }
1128
1129 pub fn client_timeout(&self, timeouts: &SmtpClientTimeouts) -> Duration {
1131 match self {
1132 Self::Helo(_) | Self::Ehlo(_) | Self::Lhlo(_) => timeouts.ehlo_timeout,
1133 Self::MailFrom { .. } => timeouts.mail_from_timeout,
1134 Self::RcptTo { .. } => timeouts.rcpt_to_timeout,
1135 Self::Data => timeouts.data_timeout,
1136 Self::DataDot => timeouts.data_dot_timeout,
1137 Self::Rset => timeouts.rset_timeout,
1138 Self::StartTls => timeouts.starttls_timeout,
1139 Self::Quit | Self::Vrfy(_) | Self::Expn(_) | Self::Help(_) | Self::Noop(_) => {
1140 timeouts.idle_timeout
1141 }
1142 Self::Auth { .. } => timeouts.auth_timeout,
1143 Self::XClient(_) => timeouts.auth_timeout, Self::Unknown(_) => timeouts.mail_from_timeout, }
1146 }
1147
1148 pub fn client_timeout_request(&self, timeouts: &SmtpClientTimeouts) -> Duration {
1150 self.client_timeout(timeouts).min(Duration::from_secs(60))
1151 }
1152
1153 fn parse_unknown(input: Span) -> IResult<Span, MaybePartialCommand> {
1154 context(
1155 "unknown-command",
1156 alt((
1157 map(
1158 all_consuming(recognize((command_word, wsp, anything))),
1159 |command| MaybePartialCommand::Full(Command::Unknown((*command).into())),
1160 ),
1161 map(all_consuming(command_word), |command| {
1162 MaybePartialCommand::Full(Command::Unknown((*command).into()))
1163 }),
1164 )),
1165 )
1166 .parse(input)
1167 }
1168}
1169
1170fn command_word(input: Span) -> IResult<Span, Span> {
1171 context(
1172 "command-word",
1173 take_while1(|c: u8| c.is_ascii_alphanumeric()),
1174 )
1175 .parse(input)
1176}
1177
1178fn wsp(input: Span) -> IResult<Span, Span> {
1179 context("wsp", take_while1(|c| c == b' ' || c == b'\t')).parse(input)
1180}
1181
1182fn anything(input: Span) -> IResult<Span, Span> {
1183 context("anything", take_while1(|_| true)).parse(input)
1184}
1185
1186fn atext(input: Span) -> IResult<Span, Span> {
1187 recognize(alt((
1188 take_while_m_n(1, 1, |c: u8| {
1189 matches!(
1190 c,
1191 b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'/' | b'=' | b'?'
1192 | b'^' | b'_' | b'`' | b'{' | b'|' | b'}' | b'~'
1193 | b'A'..=b'Z'
1194 | b'a'..=b'z'
1195 | b'0'..=b'9'
1196 )
1197 }),
1198 utf8_non_ascii,
1199 )))
1200 .parse(input)
1201}
1202
1203fn atom(input: Span) -> IResult<Span, Span> {
1204 context("atom", recognize(many1(atext))).parse(input)
1205}
1206
1207fn quoted_string(input: Span) -> IResult<Span, Span> {
1208 context(
1209 "quoted-string",
1210 recognize((
1211 tag("\""),
1212 many0(alt((
1213 recognize(pair(
1214 tag("\\"),
1215 take_while_m_n(1, 1, |c: u8| c >= 0x20 && c <= 0x7e),
1216 )),
1217 take_while_m_n(1, 1, |c: u8| {
1218 (c >= 0x20 && c <= 0x21) || (c >= 0x23 && c <= 0x5b) || (c >= 0x5d && c <= 0x7e)
1219 }),
1220 utf8_non_ascii,
1221 ))),
1222 tag("\""),
1223 )),
1224 )
1225 .parse(input)
1226}
1227
1228fn string(input: Span) -> IResult<Span, Span> {
1229 context("string", alt((atom, quoted_string))).parse(input)
1230}
1231
1232fn dot_string(input: Span) -> IResult<Span, Span> {
1238 context("dot-string", recognize((atom, many0(pair(tag("."), atom))))).parse(input)
1239}
1240
1241fn local_part(input: Span) -> IResult<Span, Span> {
1243 context("local-part", alt((dot_string, quoted_string))).parse(input)
1244}
1245
1246fn dcontent(input: Span) -> IResult<Span, Span> {
1248 take_while1(|c: u8| (c >= 33 && c <= 90) || (c >= 94 && c <= 126)).parse(input)
1249}
1250
1251fn address_literal_content(input: Span) -> IResult<Span, Domain> {
1257 let is_ipv6 = input
1258 .fragment()
1259 .get(..5)
1260 .map(|b| b.eq_ignore_ascii_case(b"IPv6:"))
1261 .unwrap_or(false);
1262
1263 if is_ipv6 {
1264 context(
1266 "ipv6-address-literal",
1267 map((tag_no_case("IPv6:"), ipv6_address), |(_, ip)| {
1268 Domain::V6(ip)
1269 }),
1270 )
1271 .parse(input)
1272 } else {
1273 alt((
1274 map(ipv4_address, Domain::V4),
1275 map_res(
1276 (
1277 recognize(take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'-')),
1278 tag(":"),
1279 recognize(many1(dcontent)),
1280 ),
1281 |(tag_s, _colon, lit): (Span, _, Span)| -> Result<Domain, String> {
1282 let mut s = String::from_utf8(tag_s.fragment().to_vec())
1284 .map_err(|_| "address_literal: invalid UTF-8 in tag".to_string())?;
1285 s.push(':');
1286 let lit_str = std::str::from_utf8(lit.fragment())
1287 .map_err(|_| "address_literal: invalid UTF-8 in literal".to_string())?;
1288 s.push_str(lit_str);
1289 Ok(Domain::Tagged(s))
1290 },
1291 ),
1292 ))
1293 .parse(input)
1294 }
1295}
1296
1297fn address_literal(input: Span) -> IResult<Span, Domain> {
1299 context(
1300 "address-literal",
1301 map((tag("["), address_literal_content, tag("]")), |(_, d, _)| d),
1302 )
1303 .parse(input)
1304}
1305
1306fn mailbox_domain(input: Span) -> IResult<Span, Domain> {
1308 context(
1309 "mailbox-domain",
1310 alt((address_literal, map(domain_name, Domain::DomainName))),
1311 )
1312 .parse(input)
1313}
1314
1315fn mailbox(input: Span) -> IResult<Span, Mailbox> {
1317 context(
1318 "mailbox",
1319 map_res(
1320 (local_part, tag("@"), mailbox_domain),
1321 |(lp, _, dom): (Span, _, Domain)| -> Result<Mailbox, String> {
1322 let local_part = String::from_utf8(lp.fragment().to_vec())
1325 .map_err(|_| "invalid UTF-8 in local-part".to_string())?;
1326 Ok(Mailbox {
1327 local_part,
1328 domain: dom,
1329 })
1330 },
1331 ),
1332 )
1333 .parse(input)
1334}
1335
1336fn at_domain(input: Span) -> IResult<Span, String> {
1338 map_res(
1339 (tag("@"), recognize(domain_name)),
1340 |(_, d): (Span, Span)| -> Result<String, String> {
1341 String::from_utf8(d.fragment().to_vec())
1342 .map_err(|_| "at_domain: invalid UTF-8 in domain".to_string())
1343 },
1344 )
1345 .parse(input)
1346}
1347
1348fn at_domain_list(input: Span) -> IResult<Span, Vec<String>> {
1350 context(
1351 "at-domain-list",
1352 map(
1353 (at_domain, many0((tag(","), at_domain)), tag(":")),
1354 |(first, rest, _)| {
1355 let mut v = vec![first];
1356 v.extend(rest.into_iter().map(|(_, d)| d));
1357 v
1358 },
1359 ),
1360 )
1361 .parse(input)
1362}
1363
1364fn null_sender(input: Span) -> IResult<Span, ReversePath> {
1366 context("null-sender", map(tag("<>"), |_| ReversePath::NullSender)).parse(input)
1367}
1368
1369fn path(input: Span) -> IResult<Span, MailPath> {
1375 context(
1376 "path",
1377 map(
1378 (tag("<"), opt(at_domain_list), mailbox, tag(">")),
1379 |(_, domains, mb, _)| MailPath {
1380 at_domain_list: domains.unwrap_or_default(),
1381 mailbox: mb,
1382 },
1383 ),
1384 )
1385 .parse(input)
1386}
1387
1388fn reverse_path(input: Span) -> IResult<Span, ReversePath> {
1394 context(
1395 "reverse-path",
1396 alt((
1397 null_sender,
1398 map(path, ReversePath::Path),
1399 map(mailbox, ReversePath::from),
1400 )),
1401 )
1402 .parse(input)
1403}
1404
1405fn postmaster_path(input: Span) -> IResult<Span, ForwardPath> {
1411 context(
1412 "postmaster",
1413 map(tag_no_case("<Postmaster>"), |_| ForwardPath::Postmaster),
1414 )
1415 .parse(input)
1416}
1417
1418fn forward_path(input: Span) -> IResult<Span, ForwardPath> {
1424 context(
1425 "forward-path",
1426 alt((
1427 postmaster_path,
1428 map(path, ForwardPath::Path),
1429 map(mailbox, ForwardPath::from),
1430 )),
1431 )
1432 .parse(input)
1433}
1434
1435fn esmtp_keyword(input: Span) -> IResult<Span, Span> {
1437 context(
1438 "esmtp-keyword",
1439 recognize((
1440 take_while_m_n(1, 1, |c: u8| c.is_ascii_alphanumeric()),
1441 many0(take_while_m_n(1, 1, |c: u8| {
1442 c.is_ascii_alphanumeric() || c == b'-'
1443 })),
1444 )),
1445 )
1446 .parse(input)
1447}
1448
1449fn esmtp_value(input: Span) -> IResult<Span, Span> {
1459 context(
1460 "esmtp-value",
1461 recognize(many1(alt((
1462 take_while1(|c: u8| (c >= 33 && c <= 60) || (c >= 62 && c <= 126)),
1463 utf8_non_ascii,
1464 )))),
1465 )
1466 .parse(input)
1467}
1468
1469fn esmtp_param(input: Span) -> IResult<Span, EsmtpParameter> {
1471 context(
1472 "esmtp-param",
1473 map_res(
1474 (esmtp_keyword, opt((tag("="), esmtp_value))),
1475 |(name, value): (Span, Option<(Span, Span)>)| -> Result<EsmtpParameter, String> {
1476 let name = String::from_utf8(name.fragment().to_vec())
1477 .map_err(|_| "esmtp_param: invalid UTF-8 in name".to_string())?;
1478 let value = value
1479 .map(|(_, v)| {
1480 String::from_utf8(v.fragment().to_vec())
1481 .map_err(|_| "esmtp_param: invalid UTF-8 in value".to_string())
1482 })
1483 .transpose()?;
1484 Ok(EsmtpParameter { name, value })
1485 },
1486 ),
1487 )
1488 .parse(input)
1489}
1490
1491fn mail_parameters(input: Span) -> IResult<Span, Vec<EsmtpParameter>> {
1493 context(
1494 "mail-parameters",
1495 map((esmtp_param, many0((wsp, esmtp_param))), |(first, rest)| {
1496 let mut params = vec![first];
1497 params.extend(rest.into_iter().map(|(_, p)| p));
1498 params
1499 }),
1500 )
1501 .parse(input)
1502}
1503
1504fn parse_ehlo(input: Span) -> IResult<Span, MaybePartialCommand> {
1514 context(
1515 "ehlo",
1516 alt((
1517 map(
1518 all_consuming((tag_no_case("EHLO"), wsp, mailbox_domain)),
1519 |(_, _, domain)| MaybePartialCommand::Full(Command::Ehlo(domain)),
1520 ),
1521 map(
1522 all_consuming((tag_no_case("EHLO"), wsp, anything)),
1523 |(_, _, remainder)| MaybePartialCommand::Partial {
1524 verb: CommandVerb::Ehlo,
1525 remainder: (*remainder).into(),
1526 reason: PartialReason::Syntax,
1527 },
1528 ),
1529 map(all_consuming(tag_no_case("EHLO")), |_| {
1530 MaybePartialCommand::Partial {
1531 verb: CommandVerb::Ehlo,
1532 remainder: BString::default(),
1533 reason: PartialReason::Syntax,
1534 }
1535 }),
1536 )),
1537 )
1538 .parse(input)
1539}
1540
1541fn parse_helo(input: Span) -> IResult<Span, MaybePartialCommand> {
1545 context(
1546 "helo",
1547 alt((
1548 map(
1549 all_consuming((tag_no_case("HELO"), wsp, mailbox_domain)),
1550 |(_, _, domain)| MaybePartialCommand::Full(Command::Helo(domain)),
1551 ),
1552 map(
1553 all_consuming((tag_no_case("HELO"), wsp, anything)),
1554 |(_, _, remainder)| MaybePartialCommand::Partial {
1555 verb: CommandVerb::Helo,
1556 remainder: (*remainder).into(),
1557 reason: PartialReason::Syntax,
1558 },
1559 ),
1560 map(all_consuming(tag_no_case("HELO")), |_| {
1561 MaybePartialCommand::Partial {
1562 verb: CommandVerb::Helo,
1563 remainder: BString::default(),
1564 reason: PartialReason::Syntax,
1565 }
1566 }),
1567 )),
1568 )
1569 .parse(input)
1570}
1571
1572fn parse_lhlo(input: Span) -> IResult<Span, MaybePartialCommand> {
1574 context(
1575 "lhlo",
1576 alt((
1577 map(
1578 all_consuming((tag_no_case("LHLO"), wsp, mailbox_domain)),
1579 |(_, _, domain)| MaybePartialCommand::Full(Command::Lhlo(domain)),
1580 ),
1581 map(
1582 all_consuming((tag_no_case("LHLO"), wsp, anything)),
1583 |(_, _, remainder)| MaybePartialCommand::Partial {
1584 verb: CommandVerb::Lhlo,
1585 remainder: (*remainder).into(),
1586 reason: PartialReason::Syntax,
1587 },
1588 ),
1589 map(all_consuming(tag_no_case("LHLO")), |_| {
1590 MaybePartialCommand::Partial {
1591 verb: CommandVerb::Lhlo,
1592 remainder: BString::default(),
1593 reason: PartialReason::Syntax,
1594 }
1595 }),
1596 )),
1597 )
1598 .parse(input)
1599}
1600
1601fn sasl_mechanism(input: Span) -> IResult<Span, String> {
1607 context(
1608 "sasl-mechanism",
1609 map(
1610 take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'-'),
1611 |s: Span| {
1612 String::from_utf8(s.fragment().to_vec()).expect("sasl_mechanism guaranteed ASCII")
1614 },
1615 ),
1616 )
1617 .parse(input)
1618}
1619
1620fn auth_initial_response(input: Span) -> IResult<Span, String> {
1626 context(
1627 "auth-initial-response",
1628 map(
1629 take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'+' || c == b'/' || c == b'='),
1630 |s: Span| {
1631 String::from_utf8(s.fragment().to_vec())
1633 .expect("auth_initial_response guaranteed ASCII")
1634 },
1635 ),
1636 )
1637 .parse(input)
1638}
1639
1640fn parse_auth(input: Span) -> IResult<Span, MaybePartialCommand> {
1642 context(
1643 "auth",
1644 alt((
1645 map(
1647 all_consuming((
1648 tag_no_case("AUTH"),
1649 wsp,
1650 sasl_mechanism,
1651 opt((wsp, auth_initial_response)),
1652 )),
1653 |(_, _, sasl_mech, resp)| match resp {
1654 Some((_, r)) => MaybePartialCommand::Full(Command::Auth {
1655 sasl_mech,
1656 initial_response: Some(r),
1657 }),
1658 None => MaybePartialCommand::Full(Command::Auth {
1659 sasl_mech,
1660 initial_response: None,
1661 }),
1662 },
1663 ),
1664 map(
1666 all_consuming((tag_no_case("AUTH"), wsp, anything)),
1667 |(_, _, remainder)| MaybePartialCommand::Partial {
1668 verb: CommandVerb::Auth,
1669 remainder: (*remainder).into(),
1670 reason: PartialReason::Syntax,
1671 },
1672 ),
1673 map(all_consuming(tag_no_case("AUTH")), |_| {
1675 MaybePartialCommand::Partial {
1676 verb: CommandVerb::Auth,
1677 remainder: BString::default(),
1678 reason: PartialReason::Syntax,
1679 }
1680 }),
1681 )),
1682 )
1683 .parse(input)
1684}
1685
1686fn xtext_decode(encoded: &[u8]) -> Result<String, String> {
1696 let mut result: Vec<u8> = Vec::with_capacity(encoded.len());
1697 let mut i = 0;
1698 while i < encoded.len() {
1699 if encoded[i] == b'+' {
1700 if i + 2 >= encoded.len() {
1701 return Err(format!("xtext_decode: truncated hex escape at byte {i}"));
1702 }
1703 let hi = hex_nibble(encoded[i + 1]).map_err(|e| format!("xtext_decode: {e}"))?;
1704 let lo = hex_nibble(encoded[i + 2]).map_err(|e| format!("xtext_decode: {e}"))?;
1705 result.push((hi << 4) | lo);
1706 i += 3;
1707 } else {
1708 result.push(encoded[i]);
1709 i += 1;
1710 }
1711 }
1712 String::from_utf8(result)
1713 .map_err(|_| "xtext_decode: invalid UTF-8 in decoded value".to_string())
1714}
1715
1716fn hex_nibble(b: u8) -> Result<u8, String> {
1717 match b {
1718 b'0'..=b'9' => Ok(b - b'0'),
1719 b'a'..=b'f' => Ok(b - b'a' + 10),
1720 b'A'..=b'F' => Ok(b - b'A' + 10),
1721 _ => Err(format!("invalid hex digit '{}'", b as char)),
1722 }
1723}
1724
1725fn xclient_xtext_value(input: Span) -> IResult<Span, Span> {
1729 context(
1730 "xclient-xtext-value",
1731 take_while1(|c: u8| c >= 33 && c <= 126),
1732 )
1733 .parse(input)
1734}
1735
1736fn xclient_param(input: Span) -> IResult<Span, XClientParameter> {
1741 context(
1742 "xclient-param",
1743 map_res(
1744 (esmtp_keyword, tag("="), xclient_xtext_value),
1745 |(name, _, value): (Span, _, Span)| -> Result<XClientParameter, String> {
1746 let name = String::from_utf8(name.fragment().to_vec())
1747 .map_err(|_| "xclient_param: invalid UTF-8 in name".to_string())?;
1748 let value = xtext_decode(value.fragment())?;
1749 Ok(XClientParameter { name, value })
1750 },
1751 ),
1752 )
1753 .parse(input)
1754}
1755
1756fn xclient_params(input: Span) -> IResult<Span, Vec<XClientParameter>> {
1758 context(
1759 "xclient-params",
1760 map(
1761 (xclient_param, many0((wsp, xclient_param))),
1762 |(first, rest)| {
1763 let mut params = vec![first];
1764 params.extend(rest.into_iter().map(|(_, p)| p));
1765 params
1766 },
1767 ),
1768 )
1769 .parse(input)
1770}
1771
1772fn parse_xclient(input: Span) -> IResult<Span, MaybePartialCommand> {
1774 context(
1775 "xclient",
1776 alt((
1777 map(
1779 all_consuming((tag_no_case("XCLIENT"), wsp, xclient_params)),
1780 |(_, _, params)| MaybePartialCommand::Full(Command::XClient(params)),
1781 ),
1782 map(
1784 all_consuming((tag_no_case("XCLIENT"), wsp, anything)),
1785 |(_, _, remainder)| MaybePartialCommand::Partial {
1786 verb: CommandVerb::XClient,
1787 remainder: (*remainder).into(),
1788 reason: PartialReason::Syntax,
1789 },
1790 ),
1791 map(all_consuming(tag_no_case("XCLIENT")), |_| {
1793 MaybePartialCommand::Partial {
1794 verb: CommandVerb::XClient,
1795 remainder: BString::default(),
1796 reason: PartialReason::Syntax,
1797 }
1798 }),
1799 )),
1800 )
1801 .parse(input)
1802}
1803
1804fn parse_mail_from(input: Span) -> IResult<Span, MaybePartialCommand> {
1814 context(
1815 "mail-from",
1816 alt((
1817 map(
1819 all_consuming((
1820 tag_no_case("MAIL"),
1821 wsp,
1822 tag_no_case("FROM:"),
1823 reverse_path,
1824 opt(map((wsp, mail_parameters), |(_, p)| p)),
1825 )),
1826 |(_, _, _, address, parameters)| {
1827 MaybePartialCommand::Full(Command::MailFrom {
1828 address,
1829 parameters: parameters.unwrap_or_default(),
1830 })
1831 },
1832 ),
1833 map(
1835 all_consuming((
1836 tag_no_case("MAIL"),
1837 wsp,
1838 tag_no_case("FROM:"),
1839 reverse_path,
1840 wsp,
1841 anything,
1842 )),
1843 |(_, _, _, _, _, remainder)| MaybePartialCommand::Partial {
1844 verb: CommandVerb::Mail,
1845 remainder: (*remainder).into(),
1846 reason: PartialReason::Syntax,
1847 },
1848 ),
1849 map(
1851 all_consuming((tag_no_case("MAIL"), wsp, tag_no_case("FROM:"), anything)),
1852 |(_, _, _, remainder)| MaybePartialCommand::Partial {
1853 verb: CommandVerb::Mail,
1854 remainder: (*remainder).into(),
1855 reason: PartialReason::InvalidSenderAddress,
1856 },
1857 ),
1858 map(
1860 all_consuming((tag_no_case("MAIL"), wsp, anything)),
1861 |(_, _, remainder)| MaybePartialCommand::Partial {
1862 verb: CommandVerb::Mail,
1863 remainder: (*remainder).into(),
1864 reason: PartialReason::Syntax,
1865 },
1866 ),
1867 map(all_consuming(tag_no_case("MAIL")), |_| {
1869 MaybePartialCommand::Partial {
1870 verb: CommandVerb::Mail,
1871 remainder: BString::default(),
1872 reason: PartialReason::Syntax,
1873 }
1874 }),
1875 )),
1876 )
1877 .parse(input)
1878}
1879
1880fn parse_rcpt_to(input: Span) -> IResult<Span, MaybePartialCommand> {
1890 context(
1891 "rcpt-to",
1892 alt((
1893 map(
1895 all_consuming((
1896 tag_no_case("RCPT"),
1897 wsp,
1898 tag_no_case("TO:"),
1899 forward_path,
1900 opt(map((wsp, mail_parameters), |(_, p)| p)),
1901 )),
1902 |(_, _, _, address, parameters)| {
1903 MaybePartialCommand::Full(Command::RcptTo {
1904 address,
1905 parameters: parameters.unwrap_or_default(),
1906 })
1907 },
1908 ),
1909 map(
1911 all_consuming((
1912 tag_no_case("RCPT"),
1913 wsp,
1914 tag_no_case("TO:"),
1915 forward_path,
1916 wsp,
1917 anything,
1918 )),
1919 |(_, _, _, _, _, remainder)| MaybePartialCommand::Partial {
1920 verb: CommandVerb::Rcpt,
1921 remainder: (*remainder).into(),
1922 reason: PartialReason::Syntax,
1923 },
1924 ),
1925 map(
1927 all_consuming((tag_no_case("RCPT"), wsp, tag_no_case("TO:"), anything)),
1928 |(_, _, _, remainder)| MaybePartialCommand::Partial {
1929 verb: CommandVerb::Rcpt,
1930 remainder: (*remainder).into(),
1931 reason: PartialReason::InvalidRecipientAddress,
1932 },
1933 ),
1934 map(
1936 all_consuming((tag_no_case("RCPT"), wsp, anything)),
1937 |(_, _, remainder)| MaybePartialCommand::Partial {
1938 verb: CommandVerb::Rcpt,
1939 remainder: (*remainder).into(),
1940 reason: PartialReason::Syntax,
1941 },
1942 ),
1943 map(all_consuming(tag_no_case("RCPT")), |_| {
1945 MaybePartialCommand::Partial {
1946 verb: CommandVerb::Rcpt,
1947 remainder: BString::default(),
1948 reason: PartialReason::Syntax,
1949 }
1950 }),
1951 )),
1952 )
1953 .parse(input)
1954}
1955
1956fn hex_nibble_lower(n: u8) -> u8 {
1962 if n < 10 {
1963 b'0' + n
1964 } else {
1965 b'a' + n - 10
1966 }
1967}
1968
1969fn xtext_encode_bytes(value: &[u8]) -> Vec<u8> {
1975 let mut result = Vec::with_capacity(value.len());
1976 for &b in value {
1977 if b >= 33 && b <= 126 && b != b'+' && b != b'=' {
1978 result.push(b);
1979 } else {
1980 result.push(b'+');
1981 result.push(hex_nibble_lower(b >> 4));
1982 result.push(hex_nibble_lower(b & 0x0f));
1983 }
1984 }
1985 result
1986}
1987
1988fn encode_domain(domain: &Domain) -> BString {
1995 match domain {
1996 Domain::DomainName(s) => BString::from(s.to_string()),
1997 Domain::V4(ip) => BString::from(format!("[{}]", ip)),
1998 Domain::V6(ip) => BString::from(format!("[IPv6:{}]", ip)),
1999 Domain::Tagged(s) => BString::from(format!("[{}]", s)),
2000 }
2001}
2002
2003fn encode_mail_path(path: &MailPath) -> Vec<u8> {
2009 let mut buf = path.mailbox.local_part.as_bytes().to_vec();
2010 buf.push(b'@');
2011 buf.extend_from_slice(encode_domain(&path.mailbox.domain).as_ref());
2012 buf
2013}
2014
2015fn encode_reverse_path(rp: &ReversePath) -> Vec<u8> {
2021 match rp {
2022 ReversePath::NullSender => vec![],
2023 ReversePath::Path(path) => encode_mail_path(path),
2024 }
2025}
2026
2027fn encode_forward_path(fp: &ForwardPath) -> Vec<u8> {
2033 match fp {
2034 ForwardPath::Postmaster => b"Postmaster".to_vec(),
2035 ForwardPath::Path(path) => encode_mail_path(path),
2036 }
2037}
2038
2039fn encode_esmtp_params(params: &[EsmtpParameter]) -> Vec<u8> {
2044 let mut buf = Vec::new();
2045 for p in params {
2046 buf.push(b' ');
2047 buf.extend_from_slice(p.name.as_bytes());
2048 if let Some(v) = &p.value {
2049 buf.push(b'=');
2050 buf.extend_from_slice(v.as_bytes());
2051 }
2052 }
2053 buf
2054}
2055
2056fn encode_xclient_params(params: &[XClientParameter]) -> Vec<u8> {
2060 let mut buf = Vec::new();
2061 for p in params {
2062 buf.push(b' ');
2063 buf.extend_from_slice(p.name.as_bytes());
2064 buf.push(b'=');
2065 buf.extend(xtext_encode_bytes(p.value.as_bytes()));
2066 }
2067 buf
2068}
2069
2070#[cfg(test)]
2071mod test {
2072 use super::*;
2073
2074 #[test]
2075 fn test_string() {
2076 k9::snapshot!(
2077 BStr::new(&parse_with("hello".as_bytes(), string).unwrap()),
2078 "hello"
2079 );
2080 k9::snapshot!(
2081 BStr::new(&parse_with("\"hello\"".as_bytes(), string).unwrap()),
2082 "\"hello\""
2083 );
2084 k9::snapshot!(
2085 BStr::new(&parse_with("\"hello world\"".as_bytes(), string).unwrap()),
2086 "\"hello world\""
2087 );
2088 k9::snapshot!(
2089 parse_with("hello world".as_bytes(), string),
2090 r#"
2091Err(
2092 "Error at line 1, in Eof:
2093hello world
2094 ^_____
2095
2096",
2097)
2098"#
2099 );
2100 }
2101
2102 #[test]
2103 fn test_bogus() {
2104 k9::snapshot!(
2105 Command::parse("bogus"),
2106 r#"
2107Ok(
2108 Full(Command("bogus\r
2109")),
2110)
2111"#
2112 );
2113 }
2114
2115 #[test]
2120 fn test_ehlo_domain_name() {
2121 k9::assert_equal!(
2122 unwrapper(Command::parse("EHLO example.com")),
2123 MaybePartialCommand::Full(Command::Ehlo(Domain::DomainName(
2124 "example.com".parse().unwrap()
2125 )))
2126 );
2127 }
2128
2129 #[test]
2130 fn test_ehlo_case_insensitive() {
2131 k9::assert_equal!(
2132 unwrapper(Command::parse("ehlo example.com")),
2133 MaybePartialCommand::Full(Command::Ehlo(Domain::DomainName(
2134 "example.com".parse().unwrap()
2135 )))
2136 );
2137 }
2138
2139 #[test]
2140 fn test_ehlo_ipv4_literal() {
2141 k9::assert_equal!(
2142 unwrapper(Command::parse("EHLO [10.0.0.1]")),
2143 MaybePartialCommand::Full(Command::Ehlo(Domain::V4("10.0.0.1".parse().unwrap())))
2144 );
2145 }
2146
2147 #[test]
2148 fn test_ehlo_ipv6_literal() {
2149 k9::assert_equal!(
2150 unwrapper(Command::parse("EHLO [IPv6:::1]")),
2151 MaybePartialCommand::Full(Command::Ehlo(Domain::V6("::1".parse().unwrap())))
2152 );
2153 }
2154
2155 #[test]
2156 fn test_ehlo_tagged_literal() {
2157 k9::assert_equal!(
2158 unwrapper(Command::parse("EHLO [future:something]")),
2159 MaybePartialCommand::Full(Command::Ehlo(Domain::Tagged("future:something".into(),)))
2160 );
2161 }
2162
2163 #[test]
2164 fn test_ehlo_invalid_ipv4_is_partial() {
2165 k9::assert_equal!(
2166 unwrapper(Command::parse("EHLO [999.999.999.999]")),
2167 MaybePartialCommand::Partial {
2168 verb: CommandVerb::Ehlo,
2169 remainder: "[999.999.999.999]".into(),
2170 reason: PartialReason::Syntax,
2171 }
2172 );
2173 }
2174
2175 #[test]
2176 fn test_ehlo_invalid_ipv6_is_partial() {
2177 k9::assert_equal!(
2178 unwrapper(Command::parse("EHLO [IPv6:not-an-ipv6]")),
2179 MaybePartialCommand::Partial {
2180 verb: CommandVerb::Ehlo,
2181 remainder: "[IPv6:not-an-ipv6]".into(),
2182 reason: PartialReason::Syntax,
2183 }
2184 );
2185 }
2186
2187 #[test]
2188 fn test_ehlo_alone_is_partial() {
2189 k9::assert_equal!(
2190 unwrapper(Command::parse("EHLO")),
2191 MaybePartialCommand::Partial {
2192 verb: CommandVerb::Ehlo,
2193 remainder: "".into(),
2194 reason: PartialReason::Syntax,
2195 }
2196 );
2197 }
2198
2199 #[test]
2200 fn test_ehlo_with_garbage_is_partial() {
2201 k9::assert_equal!(
2202 unwrapper(Command::parse("EHLO !!invalid!!")),
2203 MaybePartialCommand::Partial {
2204 verb: CommandVerb::Ehlo,
2205 remainder: "!!invalid!!".into(),
2206 reason: PartialReason::Syntax,
2207 }
2208 );
2209 }
2210
2211 #[test]
2216 fn test_helo_domain_name() {
2217 k9::assert_equal!(
2218 unwrapper(Command::parse("HELO example.com")),
2219 MaybePartialCommand::Full(Command::Helo(Domain::DomainName(
2220 "example.com".parse().unwrap()
2221 )))
2222 );
2223 }
2224
2225 #[test]
2226 fn test_helo_case_insensitive() {
2227 k9::assert_equal!(
2228 unwrapper(Command::parse("helo example.com")),
2229 MaybePartialCommand::Full(Command::Helo(Domain::DomainName(
2230 "example.com".parse().unwrap()
2231 )))
2232 );
2233 }
2234
2235 #[test]
2236 fn test_helo_ipv4_literal() {
2237 k9::assert_equal!(
2239 unwrapper(Command::parse("HELO [10.0.0.1]")),
2240 MaybePartialCommand::Full(Command::Helo(Domain::V4("10.0.0.1".parse().unwrap())))
2241 );
2242 }
2243
2244 #[test]
2245 fn test_helo_alone_is_partial() {
2246 k9::assert_equal!(
2247 unwrapper(Command::parse("HELO")),
2248 MaybePartialCommand::Partial {
2249 verb: CommandVerb::Helo,
2250 remainder: "".into(),
2251 reason: PartialReason::Syntax,
2252 }
2253 );
2254 }
2255
2256 #[test]
2257 fn test_helo_with_garbage_is_partial() {
2258 k9::assert_equal!(
2259 unwrapper(Command::parse("HELO !!invalid!!")),
2260 MaybePartialCommand::Partial {
2261 verb: CommandVerb::Helo,
2262 remainder: "!!invalid!!".into(),
2263 reason: PartialReason::Syntax,
2264 }
2265 );
2266 }
2267
2268 #[test]
2273 fn test_lhlo_domain_name() {
2274 k9::assert_equal!(
2275 unwrapper(Command::parse("LHLO example.com")),
2276 MaybePartialCommand::Full(Command::Lhlo(Domain::DomainName(
2277 "example.com".parse().unwrap()
2278 )))
2279 );
2280 }
2281
2282 #[test]
2283 fn test_lhlo_case_insensitive() {
2284 k9::assert_equal!(
2285 unwrapper(Command::parse("lhlo example.com")),
2286 MaybePartialCommand::Full(Command::Lhlo(Domain::DomainName(
2287 "example.com".parse().unwrap()
2288 )))
2289 );
2290 }
2291
2292 #[test]
2293 fn test_lhlo_ipv4_literal() {
2294 k9::assert_equal!(
2295 unwrapper(Command::parse("LHLO [10.0.0.1]")),
2296 MaybePartialCommand::Full(Command::Lhlo(Domain::V4("10.0.0.1".parse().unwrap())))
2297 );
2298 }
2299
2300 #[test]
2301 fn test_lhlo_ipv6_literal() {
2302 k9::assert_equal!(
2303 unwrapper(Command::parse("LHLO [IPv6:::1]")),
2304 MaybePartialCommand::Full(Command::Lhlo(Domain::V6("::1".parse().unwrap())))
2305 );
2306 }
2307
2308 #[test]
2309 fn test_lhlo_invalid_ipv4_is_partial() {
2310 k9::assert_equal!(
2311 unwrapper(Command::parse("LHLO [999.999.999.999]")),
2312 MaybePartialCommand::Partial {
2313 verb: CommandVerb::Lhlo,
2314 remainder: "[999.999.999.999]".into(),
2315 reason: PartialReason::Syntax,
2316 }
2317 );
2318 }
2319
2320 #[test]
2321 fn test_lhlo_alone_is_partial() {
2322 k9::assert_equal!(
2323 unwrapper(Command::parse("LHLO")),
2324 MaybePartialCommand::Partial {
2325 verb: CommandVerb::Lhlo,
2326 remainder: "".into(),
2327 reason: PartialReason::Syntax,
2328 }
2329 );
2330 }
2331
2332 #[test]
2337 fn test_auth_mechanism_only() {
2338 k9::assert_equal!(
2339 unwrapper(Command::parse("AUTH PLAIN")),
2340 MaybePartialCommand::Full(Command::Auth {
2341 sasl_mech: "PLAIN".into(),
2342 initial_response: None,
2343 })
2344 );
2345 }
2346
2347 #[test]
2348 fn test_auth_with_initial_response() {
2349 k9::assert_equal!(
2350 unwrapper(Command::parse("AUTH PLAIN dXNlcjpwYXNz")),
2351 MaybePartialCommand::Full(Command::Auth {
2352 sasl_mech: "PLAIN".into(),
2353 initial_response: Some("dXNlcjpwYXNz".into()),
2354 })
2355 );
2356 }
2357
2358 #[test]
2359 fn test_auth_empty_initial_response() {
2360 k9::assert_equal!(
2362 unwrapper(Command::parse("AUTH PLAIN =")),
2363 MaybePartialCommand::Full(Command::Auth {
2364 sasl_mech: "PLAIN".into(),
2365 initial_response: Some("=".into()),
2366 })
2367 );
2368 }
2369
2370 #[test]
2371 fn test_auth_hyphenated_mechanism() {
2372 k9::assert_equal!(
2373 unwrapper(Command::parse("AUTH CRAM-MD5")),
2374 MaybePartialCommand::Full(Command::Auth {
2375 sasl_mech: "CRAM-MD5".into(),
2376 initial_response: None,
2377 })
2378 );
2379 }
2380
2381 #[test]
2382 fn test_auth_case_insensitive() {
2383 k9::assert_equal!(
2384 unwrapper(Command::parse("auth plain")),
2385 MaybePartialCommand::Full(Command::Auth {
2386 sasl_mech: "plain".into(),
2387 initial_response: None,
2388 })
2389 );
2390 }
2391
2392 #[test]
2393 fn test_auth_alone_is_partial() {
2394 k9::assert_equal!(
2395 unwrapper(Command::parse("AUTH")),
2396 MaybePartialCommand::Partial {
2397 verb: CommandVerb::Auth,
2398 remainder: "".into(),
2399 reason: PartialReason::Syntax,
2400 }
2401 );
2402 }
2403
2404 #[test]
2405 fn test_auth_with_garbage_is_partial() {
2406 k9::assert_equal!(
2408 unwrapper(Command::parse("AUTH !!bad!!")),
2409 MaybePartialCommand::Partial {
2410 verb: CommandVerb::Auth,
2411 remainder: "!!bad!!".into(),
2412 reason: PartialReason::Syntax,
2413 }
2414 );
2415 }
2416
2417 #[test]
2422 fn test_xclient_single_param() {
2423 k9::assert_equal!(
2424 unwrapper(Command::parse("XCLIENT NAME=foo.example.com")),
2425 MaybePartialCommand::Full(Command::XClient(vec![XClientParameter {
2426 name: "NAME".into(),
2427 value: "foo.example.com".into(),
2428 }]))
2429 );
2430 }
2431
2432 #[test]
2433 fn test_xclient_multiple_params() {
2434 k9::assert_equal!(
2435 unwrapper(Command::parse("XCLIENT NAME=foo.example.com ADDR=10.0.0.1")),
2436 MaybePartialCommand::Full(Command::XClient(vec![
2437 XClientParameter {
2438 name: "NAME".into(),
2439 value: "foo.example.com".into(),
2440 },
2441 XClientParameter {
2442 name: "ADDR".into(),
2443 value: "10.0.0.1".into(),
2444 },
2445 ]))
2446 );
2447 }
2448
2449 #[test]
2450 fn test_xclient_xtext_hex_escape() {
2451 k9::assert_equal!(
2453 unwrapper(Command::parse("XCLIENT NAME=user+40example.com")),
2454 MaybePartialCommand::Full(Command::XClient(vec![XClientParameter {
2455 name: "NAME".into(),
2456 value: "user@example.com".into(),
2457 }]))
2458 );
2459 }
2460
2461 #[test]
2462 fn test_xclient_case_insensitive() {
2463 k9::assert_equal!(
2464 unwrapper(Command::parse("xclient NAME=host")),
2465 MaybePartialCommand::Full(Command::XClient(vec![XClientParameter {
2466 name: "NAME".into(),
2467 value: "host".into(),
2468 }]))
2469 );
2470 }
2471
2472 #[test]
2473 fn test_xclient_alone_is_partial() {
2474 k9::assert_equal!(
2475 unwrapper(Command::parse("XCLIENT")),
2476 MaybePartialCommand::Partial {
2477 verb: CommandVerb::XClient,
2478 remainder: "".into(),
2479 reason: PartialReason::Syntax,
2480 }
2481 );
2482 }
2483
2484 #[test]
2485 fn test_xclient_invalid_xtext_is_partial() {
2486 k9::assert_equal!(
2488 unwrapper(Command::parse("XCLIENT NAME=bad+ZZ")),
2489 MaybePartialCommand::Partial {
2490 verb: CommandVerb::XClient,
2491 remainder: "NAME=bad+ZZ".into(),
2492 reason: PartialReason::Syntax,
2493 }
2494 );
2495 }
2496
2497 #[test]
2498 fn test_xclient_garbage_is_partial() {
2499 k9::assert_equal!(
2501 unwrapper(Command::parse("XCLIENT noequals")),
2502 MaybePartialCommand::Partial {
2503 verb: CommandVerb::XClient,
2504 remainder: "noequals".into(),
2505 reason: PartialReason::Syntax,
2506 }
2507 );
2508 }
2509
2510 #[test]
2511 fn test_xclient_parameter_methods() {
2512 let cmd = unwrapper(Command::parse("XCLIENT ADDR=192.168.1.1"));
2514 let params = match cmd {
2515 MaybePartialCommand::Full(Command::XClient(params)) => params,
2516 _ => panic!("Expected XCLIENT command"),
2517 };
2518
2519 k9::assert_equal!(params[0].is_name("ADDR"), true);
2521 k9::assert_equal!(params[0].is_name("addr"), true);
2522 k9::assert_equal!(params[0].is_name("NAME"), false);
2523
2524 let ip: std::net::IpAddr = params[0].parse().expect("Failed to parse IP address");
2526 k9::assert_equal!(
2527 ip,
2528 std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 1, 1))
2529 );
2530 }
2531
2532 #[test]
2533 fn test_xclient_parameter_parse_invalid_ip() {
2534 let cmd = unwrapper(Command::parse("XCLIENT ADDR=not-an-ip"));
2536 let params = match cmd {
2537 MaybePartialCommand::Full(Command::XClient(params)) => params,
2538 _ => panic!("Expected XCLIENT command"),
2539 };
2540
2541 let result: Result<std::net::IpAddr, _> = params[0].parse();
2542 k9::assert_equal!(result.unwrap_err(), "invalid IP address syntax");
2543 }
2544
2545 fn mail_path(local: &str, domain: Domain) -> ReversePath {
2550 Mailbox {
2551 local_part: local.into(),
2552 domain,
2553 }
2554 .into()
2555 }
2556
2557 #[test]
2558 fn test_mail_from_domain_name() {
2559 k9::assert_equal!(
2560 unwrapper(Command::parse("MAIL FROM:<user@host>")),
2561 MaybePartialCommand::Full(Command::MailFrom {
2562 address: mail_path("user", Domain::DomainName("host".parse().unwrap())),
2563 parameters: vec![],
2564 })
2565 );
2566 k9::assert_equal!(
2568 unwrapper(Command::parse("mail from:<user@host>")),
2569 MaybePartialCommand::Full(Command::MailFrom {
2570 address: mail_path("user", Domain::DomainName("host".parse().unwrap())),
2571 parameters: vec![],
2572 })
2573 );
2574 }
2575
2576 #[test]
2577 fn test_mail_from_null_sender() {
2578 k9::assert_equal!(
2579 unwrapper(Command::parse("MAIL FROM:<>")),
2580 MaybePartialCommand::Full(Command::MailFrom {
2581 address: ReversePath::NullSender,
2582 parameters: vec![],
2583 })
2584 );
2585 }
2586
2587 #[test]
2588 fn test_mail_from_ipv4() {
2589 k9::assert_equal!(
2590 unwrapper(Command::parse("MAIL FROM:<user@[10.0.0.1]>")),
2591 MaybePartialCommand::Full(Command::MailFrom {
2592 address: mail_path("user", Domain::V4("10.0.0.1".parse().unwrap())),
2593 parameters: vec![],
2594 })
2595 );
2596 }
2597
2598 #[test]
2599 fn test_mail_from_ipv6() {
2600 k9::assert_equal!(
2601 unwrapper(Command::parse("MAIL FROM:<user@[IPv6:::1]>")),
2602 MaybePartialCommand::Full(Command::MailFrom {
2603 address: mail_path("user", Domain::V6("::1".parse().unwrap())),
2604 parameters: vec![],
2605 })
2606 );
2607 }
2608
2609 #[test]
2610 fn test_mail_from_tagged_literal() {
2611 k9::assert_equal!(
2612 unwrapper(Command::parse("MAIL FROM:<user@[future:something]>")),
2613 MaybePartialCommand::Full(Command::MailFrom {
2614 address: mail_path("user", Domain::Tagged("future:something".into())),
2615 parameters: vec![],
2616 })
2617 );
2618 }
2619
2620 #[test]
2621 fn test_mail_from_esmtp_params() {
2622 k9::assert_equal!(
2623 unwrapper(Command::parse("MAIL FROM:<user@host> foo bar=baz")),
2624 MaybePartialCommand::Full(Command::MailFrom {
2625 address: mail_path("user", Domain::DomainName("host".parse().unwrap())),
2626 parameters: vec![
2627 EsmtpParameter {
2628 name: "foo".into(),
2629 value: None,
2630 },
2631 EsmtpParameter {
2632 name: "bar".into(),
2633 value: Some("baz".into()),
2634 },
2635 ],
2636 })
2637 );
2638 }
2639
2640 #[test]
2641 fn test_mail_from_bare_address() {
2642 k9::assert_equal!(
2644 unwrapper(Command::parse("MAIL FROM:user@host")),
2645 MaybePartialCommand::Full(Command::MailFrom {
2646 address: mail_path("user", Domain::DomainName("host".parse().unwrap())),
2647 parameters: vec![],
2648 })
2649 );
2650 }
2651
2652 #[test]
2653 fn test_mail_from_at_domain_list() {
2654 k9::assert_equal!(
2655 unwrapper(Command::parse(
2656 "MAIL FROM:<@hosta.int,@jkl.org:userc@d.bar.org>"
2657 )),
2658 MaybePartialCommand::Full(Command::MailFrom {
2659 address: ReversePath::Path(MailPath {
2660 at_domain_list: vec!["hosta.int".into(), "jkl.org".into()],
2661 mailbox: Mailbox {
2662 local_part: "userc".into(),
2663 domain: Domain::DomainName("d.bar.org".parse().unwrap()),
2664 },
2665 }),
2666 parameters: vec![],
2667 })
2668 );
2669 }
2670
2671 #[test]
2672 fn test_mail_from_invalid_ipv4_is_partial() {
2673 k9::assert_equal!(
2675 unwrapper(Command::parse("MAIL FROM:<user@[999.999.999.999]>")),
2676 MaybePartialCommand::Partial {
2677 verb: CommandVerb::Mail,
2678 remainder: "<user@[999.999.999.999]>".into(),
2679 reason: PartialReason::InvalidSenderAddress,
2680 }
2681 );
2682 }
2683
2684 #[test]
2685 fn test_mail_from_invalid_ipv6_is_partial() {
2686 k9::assert_equal!(
2688 unwrapper(Command::parse("MAIL FROM:<user@[IPv6:not-an-ipv6]>")),
2689 MaybePartialCommand::Partial {
2690 verb: CommandVerb::Mail,
2691 remainder: "<user@[IPv6:not-an-ipv6]>".into(),
2692 reason: PartialReason::InvalidSenderAddress,
2693 }
2694 );
2695 }
2696
2697 #[test]
2698 fn test_mail_alone_is_partial() {
2699 k9::assert_equal!(
2700 unwrapper(Command::parse("MAIL")),
2701 MaybePartialCommand::Partial {
2702 verb: CommandVerb::Mail,
2703 remainder: "".into(),
2704 reason: PartialReason::Syntax,
2705 }
2706 );
2707 }
2708
2709 #[test]
2710 fn test_mail_with_garbage_is_partial() {
2711 k9::assert_equal!(
2712 unwrapper(Command::parse("MAIL garbage")),
2713 MaybePartialCommand::Partial {
2714 verb: CommandVerb::Mail,
2715 remainder: "garbage".into(),
2716 reason: PartialReason::Syntax,
2717 }
2718 );
2719 }
2720
2721 fn rcpt_path(local: &str, domain: Domain) -> ForwardPath {
2726 Mailbox {
2727 local_part: local.into(),
2728 domain,
2729 }
2730 .into()
2731 }
2732
2733 #[test]
2734 fn test_rcpt_to_domain_name() {
2735 k9::assert_equal!(
2736 unwrapper(Command::parse("RCPT TO:<user@host>")),
2737 MaybePartialCommand::Full(Command::RcptTo {
2738 address: rcpt_path("user", Domain::DomainName("host".parse().unwrap())),
2739 parameters: vec![],
2740 })
2741 );
2742 }
2743
2744 #[test]
2745 fn test_rcpt_to_case_insensitive() {
2746 k9::assert_equal!(
2748 unwrapper(Command::parse("rcpt to:<user@host>")),
2749 MaybePartialCommand::Full(Command::RcptTo {
2750 address: rcpt_path("user", Domain::DomainName("host".parse().unwrap())),
2751 parameters: vec![],
2752 })
2753 );
2754 }
2755
2756 #[test]
2757 fn test_rcpt_to_postmaster() {
2758 k9::assert_equal!(
2759 unwrapper(Command::parse("RCPT TO:<Postmaster>")),
2760 MaybePartialCommand::Full(Command::RcptTo {
2761 address: ForwardPath::Postmaster,
2762 parameters: vec![],
2763 })
2764 );
2765 }
2766
2767 #[test]
2768 fn test_rcpt_to_postmaster_lowercase() {
2769 k9::assert_equal!(
2771 unwrapper(Command::parse("RCPT TO:<postmaster>")),
2772 MaybePartialCommand::Full(Command::RcptTo {
2773 address: ForwardPath::Postmaster,
2774 parameters: vec![],
2775 })
2776 );
2777 }
2778
2779 #[test]
2780 fn test_rcpt_to_ipv4() {
2781 k9::assert_equal!(
2782 unwrapper(Command::parse("RCPT TO:<user@[10.0.0.1]>")),
2783 MaybePartialCommand::Full(Command::RcptTo {
2784 address: rcpt_path("user", Domain::V4("10.0.0.1".parse().unwrap())),
2785 parameters: vec![],
2786 })
2787 );
2788 }
2789
2790 #[test]
2791 fn test_rcpt_to_ipv6() {
2792 k9::assert_equal!(
2793 unwrapper(Command::parse("RCPT TO:<user@[IPv6:::1]>")),
2794 MaybePartialCommand::Full(Command::RcptTo {
2795 address: rcpt_path("user", Domain::V6("::1".parse().unwrap())),
2796 parameters: vec![],
2797 })
2798 );
2799 }
2800
2801 #[test]
2802 fn test_rcpt_to_tagged_literal() {
2803 k9::assert_equal!(
2804 unwrapper(Command::parse("RCPT TO:<user@[future:something]>")),
2805 MaybePartialCommand::Full(Command::RcptTo {
2806 address: rcpt_path("user", Domain::Tagged("future:something".into())),
2807 parameters: vec![],
2808 })
2809 );
2810 }
2811
2812 #[test]
2813 fn test_rcpt_to_esmtp_params() {
2814 k9::assert_equal!(
2815 unwrapper(Command::parse("RCPT TO:<user@host> foo bar=baz")),
2816 MaybePartialCommand::Full(Command::RcptTo {
2817 address: rcpt_path("user", Domain::DomainName("host".parse().unwrap())),
2818 parameters: vec![
2819 EsmtpParameter {
2820 name: "foo".into(),
2821 value: None,
2822 },
2823 EsmtpParameter {
2824 name: "bar".into(),
2825 value: Some("baz".into()),
2826 },
2827 ],
2828 })
2829 );
2830 }
2831
2832 #[test]
2833 fn test_rcpt_to_bare_address() {
2834 k9::assert_equal!(
2836 unwrapper(Command::parse("RCPT TO:user@host")),
2837 MaybePartialCommand::Full(Command::RcptTo {
2838 address: rcpt_path("user", Domain::DomainName("host".parse().unwrap())),
2839 parameters: vec![],
2840 })
2841 );
2842 }
2843
2844 #[test]
2845 fn test_rcpt_to_at_domain_list() {
2846 k9::assert_equal!(
2847 unwrapper(Command::parse(
2848 "RCPT TO:<@hosta.int,@jkl.org:userc@d.bar.org>"
2849 )),
2850 MaybePartialCommand::Full(Command::RcptTo {
2851 address: ForwardPath::Path(MailPath {
2852 at_domain_list: vec!["hosta.int".into(), "jkl.org".into()],
2853 mailbox: Mailbox {
2854 local_part: "userc".into(),
2855 domain: Domain::DomainName("d.bar.org".parse().unwrap()),
2856 },
2857 }),
2858 parameters: vec![],
2859 })
2860 );
2861 }
2862
2863 #[test]
2864 fn test_rcpt_to_invalid_ipv4_is_partial() {
2865 k9::assert_equal!(
2867 unwrapper(Command::parse("RCPT TO:<user@[999.999.999.999]>")),
2868 MaybePartialCommand::Partial {
2869 verb: CommandVerb::Rcpt,
2870 remainder: "<user@[999.999.999.999]>".into(),
2871 reason: PartialReason::InvalidRecipientAddress,
2872 }
2873 );
2874 }
2875
2876 #[test]
2877 fn test_rcpt_to_invalid_ipv6_is_partial() {
2878 k9::assert_equal!(
2880 unwrapper(Command::parse("RCPT TO:<user@[IPv6:not-an-ipv6]>")),
2881 MaybePartialCommand::Partial {
2882 verb: CommandVerb::Rcpt,
2883 remainder: "<user@[IPv6:not-an-ipv6]>".into(),
2884 reason: PartialReason::InvalidRecipientAddress,
2885 }
2886 );
2887 }
2888
2889 #[test]
2890 fn test_rcpt_alone_is_partial() {
2891 k9::assert_equal!(
2892 unwrapper(Command::parse("RCPT")),
2893 MaybePartialCommand::Partial {
2894 verb: CommandVerb::Rcpt,
2895 remainder: "".into(),
2896 reason: PartialReason::Syntax,
2897 }
2898 );
2899 }
2900
2901 #[test]
2902 fn test_rcpt_with_garbage_is_partial() {
2903 k9::assert_equal!(
2904 unwrapper(Command::parse("RCPT garbage")),
2905 MaybePartialCommand::Partial {
2906 verb: CommandVerb::Rcpt,
2907 remainder: "garbage".into(),
2908 reason: PartialReason::Syntax,
2909 }
2910 );
2911 }
2912
2913 #[test]
2914 fn test_rcpt_to_invalid_address_syntax() {
2915 k9::assert_equal!(
2917 unwrapper(Command::parse("RCPT TO:<not an address>")),
2918 MaybePartialCommand::Partial {
2919 verb: CommandVerb::Rcpt,
2920 remainder: "<not an address>".into(),
2921 reason: PartialReason::InvalidRecipientAddress,
2922 }
2923 );
2924 }
2925
2926 #[test]
2927 fn test_rcpt_to_valid_address_bad_params() {
2928 k9::assert_equal!(
2930 unwrapper(Command::parse("RCPT TO:<valid@example.com> !!!garbage")),
2931 MaybePartialCommand::Partial {
2932 verb: CommandVerb::Rcpt,
2933 remainder: "!!!garbage".into(),
2934 reason: PartialReason::Syntax,
2935 }
2936 );
2937 }
2938
2939 #[test]
2940 fn test_rcpt_to_bad_address_with_params_is_invalid_address() {
2941 k9::assert_equal!(
2943 unwrapper(Command::parse("RCPT TO:<bad address> NOTIFY=SUCCESS")),
2944 MaybePartialCommand::Partial {
2945 verb: CommandVerb::Rcpt,
2946 remainder: "<bad address> NOTIFY=SUCCESS".into(),
2947 reason: PartialReason::InvalidRecipientAddress,
2948 }
2949 );
2950 }
2951
2952 #[test]
2953 fn test_mail_from_invalid_address_syntax() {
2954 k9::assert_equal!(
2956 unwrapper(Command::parse("MAIL FROM:<not an address>")),
2957 MaybePartialCommand::Partial {
2958 verb: CommandVerb::Mail,
2959 remainder: "<not an address>".into(),
2960 reason: PartialReason::InvalidSenderAddress,
2961 }
2962 );
2963 }
2964
2965 #[test]
2966 fn test_mail_from_valid_address_bad_params() {
2967 k9::assert_equal!(
2969 unwrapper(Command::parse("MAIL FROM:<valid@example.com> !!!garbage")),
2970 MaybePartialCommand::Partial {
2971 verb: CommandVerb::Mail,
2972 remainder: "!!!garbage".into(),
2973 reason: PartialReason::Syntax,
2974 }
2975 );
2976 }
2977
2978 #[test]
2979 fn test_mail_from_null_sender_bad_params() {
2980 k9::assert_equal!(
2982 unwrapper(Command::parse("MAIL FROM:<> !!!garbage")),
2983 MaybePartialCommand::Partial {
2984 verb: CommandVerb::Mail,
2985 remainder: "!!!garbage".into(),
2986 reason: PartialReason::Syntax,
2987 }
2988 );
2989 }
2990
2991 #[test]
2992 fn test_mail_from_bad_address_with_params_is_invalid_address() {
2993 k9::assert_equal!(
2995 unwrapper(Command::parse("MAIL FROM:<bad address> SIZE=1000")),
2996 MaybePartialCommand::Partial {
2997 verb: CommandVerb::Mail,
2998 remainder: "<bad address> SIZE=1000".into(),
2999 reason: PartialReason::InvalidSenderAddress,
3000 }
3001 );
3002 }
3003
3004 fn assert_encode(input: &str, expected: &str) {
3012 let cmd = match unwrapper(Command::parse(input)) {
3013 MaybePartialCommand::Full(c) => c,
3014 other => panic!("expected Full, got {other:?}"),
3015 };
3016 let encoded = cmd.encode();
3017 k9::assert_equal!(encoded, BString::from(expected));
3018 k9::assert_equal!(
3020 unwrapper(Command::parse(encoded.clone())),
3021 MaybePartialCommand::Full(cmd)
3022 );
3023 }
3024
3025 #[test]
3026 fn test_encode_ehlo_domain() {
3027 assert_encode("EHLO example.com", "EHLO example.com\r\n");
3028 }
3029
3030 #[test]
3031 fn test_encode_ehlo_ipv4() {
3032 assert_encode("EHLO [10.0.0.1]", "EHLO [10.0.0.1]\r\n");
3033 }
3034
3035 #[test]
3036 fn test_encode_ehlo_ipv6() {
3037 assert_encode("EHLO [IPv6:::1]", "EHLO [IPv6:::1]\r\n");
3038 }
3039
3040 #[test]
3041 fn test_encode_ehlo_tagged() {
3042 assert_encode("EHLO [future:something]", "EHLO [future:something]\r\n");
3043 }
3044
3045 #[test]
3046 fn test_encode_helo() {
3047 assert_encode("HELO mail.example.com", "HELO mail.example.com\r\n");
3048 }
3049
3050 #[test]
3051 fn test_encode_lhlo() {
3052 assert_encode("LHLO mail.example.com", "LHLO mail.example.com\r\n");
3053 }
3054
3055 #[test]
3056 fn test_encode_noop_none() {
3057 assert_encode("NOOP", "NOOP\r\n");
3058 }
3059
3060 #[test]
3061 fn test_encode_noop_some() {
3062 assert_encode("NOOP something", "NOOP something\r\n");
3063 }
3064
3065 #[test]
3066 fn test_encode_help_none() {
3067 assert_encode("HELP", "HELP\r\n");
3068 }
3069
3070 #[test]
3071 fn test_encode_help_some() {
3072 assert_encode("HELP MAIL", "HELP MAIL\r\n");
3073 }
3074
3075 #[test]
3076 fn test_encode_vrfy_none() {
3077 assert_encode("VRFY", "VRFY\r\n");
3078 }
3079
3080 #[test]
3081 fn test_encode_vrfy_some() {
3082 assert_encode("VRFY user", "VRFY user\r\n");
3083 }
3084
3085 #[test]
3086 fn test_encode_expn_none() {
3087 assert_encode("EXPN", "EXPN\r\n");
3088 }
3089
3090 #[test]
3091 fn test_encode_expn_some() {
3092 assert_encode("EXPN list", "EXPN list\r\n");
3093 }
3094
3095 #[test]
3096 fn test_encode_data() {
3097 assert_encode("DATA", "DATA\r\n");
3098 }
3099
3100 #[test]
3101 fn test_encode_data_dot() {
3102 k9::assert_equal!(Command::DataDot.encode(), BString::from(".\r\n"));
3105 }
3106
3107 #[test]
3108 fn test_encode_rset() {
3109 assert_encode("RSET", "RSET\r\n");
3110 }
3111
3112 #[test]
3113 fn test_encode_quit() {
3114 assert_encode("QUIT", "QUIT\r\n");
3115 }
3116
3117 #[test]
3118 fn test_encode_starttls() {
3119 assert_encode("STARTTLS", "STARTTLS\r\n");
3120 }
3121
3122 #[test]
3123 fn test_encode_mail_from_path() {
3124 assert_encode(
3125 "MAIL FROM:<user@example.com>",
3126 "MAIL FROM:<user@example.com>\r\n",
3127 );
3128 }
3129
3130 #[test]
3131 fn test_encode_mail_from_null_sender() {
3132 assert_encode("MAIL FROM:<>", "MAIL FROM:<>\r\n");
3133 }
3134
3135 #[test]
3136 fn test_encode_mail_from_params() {
3137 assert_encode(
3138 "MAIL FROM:<user@example.com> SIZE=1000 BODY=8BITMIME",
3139 "MAIL FROM:<user@example.com> SIZE=1000 BODY=8BITMIME\r\n",
3140 );
3141 }
3142
3143 #[test]
3144 fn test_encode_mail_from_ipv4() {
3145 assert_encode(
3146 "MAIL FROM:<user@[10.0.0.1]>",
3147 "MAIL FROM:<user@[10.0.0.1]>\r\n",
3148 );
3149 }
3150
3151 #[test]
3152 fn test_encode_mail_from_ipv6() {
3153 assert_encode(
3154 "MAIL FROM:<user@[IPv6:::1]>",
3155 "MAIL FROM:<user@[IPv6:::1]>\r\n",
3156 );
3157 }
3158
3159 #[test]
3160 fn test_encode_rcpt_to_path() {
3161 assert_encode(
3162 "RCPT TO:<user@example.com>",
3163 "RCPT TO:<user@example.com>\r\n",
3164 );
3165 }
3166
3167 #[test]
3168 fn test_encode_rcpt_to_postmaster() {
3169 let cmd = Command::RcptTo {
3171 address: ForwardPath::Postmaster,
3172 parameters: vec![],
3173 };
3174 k9::assert_equal!(cmd.encode(), BString::from("RCPT TO:<Postmaster>\r\n"));
3175 k9::assert_equal!(
3176 unwrapper(Command::parse(cmd.encode())),
3177 MaybePartialCommand::Full(cmd)
3178 );
3179 }
3180
3181 #[test]
3182 fn test_encode_rcpt_to_params() {
3183 assert_encode(
3184 "RCPT TO:<user@example.com> NOTIFY=SUCCESS",
3185 "RCPT TO:<user@example.com> NOTIFY=SUCCESS\r\n",
3186 );
3187 }
3188
3189 #[test]
3190 fn test_encode_auth_no_response() {
3191 assert_encode("AUTH PLAIN", "AUTH PLAIN\r\n");
3192 }
3193
3194 #[test]
3195 fn test_encode_auth_with_response() {
3196 assert_encode("AUTH PLAIN dXNlcjpwYXNz", "AUTH PLAIN dXNlcjpwYXNz\r\n");
3197 }
3198
3199 #[test]
3200 fn test_encode_xclient_single() {
3201 assert_encode(
3202 "XCLIENT NAME=foo.example.com",
3203 "XCLIENT NAME=foo.example.com\r\n",
3204 );
3205 }
3206
3207 #[test]
3208 fn test_encode_xclient_multiple() {
3209 assert_encode(
3210 "XCLIENT NAME=foo.example.com ADDR=10.0.0.1",
3211 "XCLIENT NAME=foo.example.com ADDR=10.0.0.1\r\n",
3212 );
3213 }
3214
3215 #[test]
3216 fn test_encode_xclient_xtext_roundtrip() {
3217 assert_encode(
3221 "XCLIENT NAME=user+40example.com",
3222 "XCLIENT NAME=user@example.com\r\n",
3223 );
3224 }
3225
3226 #[test]
3227 fn test_encode_unknown() {
3228 assert_encode("FOOBAR some args", "FOOBAR some args\r\n");
3229 }
3230
3231 #[test]
3232 fn test_encode_unknown_bare_verb() {
3233 assert_encode("FOOBAR", "FOOBAR\r\n");
3234 }
3235
3236 #[test]
3237 fn test_encode_mail_from_source_route_dropped() {
3238 let parsed = unwrapper(Command::parse("MAIL FROM:<@route.example.com:user@host>"));
3242 let cmd = match parsed {
3243 MaybePartialCommand::Full(c) => c,
3244 other => panic!("expected Full, got {other:?}"),
3245 };
3246 let encoded = cmd.encode();
3247 k9::assert_equal!(encoded, BString::from("MAIL FROM:<user@host>\r\n"));
3248 let expected = Mailbox {
3250 local_part: "user".into(),
3251 domain: Domain::DomainName("host".parse().unwrap()),
3252 };
3253 k9::assert_equal!(
3254 unwrapper(Command::parse(encoded)),
3255 MaybePartialCommand::Full(Command::MailFrom {
3256 address: expected.into(),
3257 parameters: vec![],
3258 })
3259 );
3260 }
3261
3262 fn parse_full(input: &str) -> Command {
3268 match unwrapper(Command::parse(input)) {
3269 MaybePartialCommand::Full(c) => c,
3270 other => panic!("expected Full, got {other:?}"),
3271 }
3272 }
3273
3274 #[test]
3277 fn test_mail_from_utf8_local_part() {
3278 let cmd = parse_full("MAIL FROM:<ü@example.com>");
3280 let mailbox = match cmd {
3281 Command::MailFrom {
3282 address: ReversePath::Path(path),
3283 ..
3284 } => path.mailbox,
3285 other => panic!("unexpected {other:?}"),
3286 };
3287 k9::assert_equal!(mailbox.local_part, String::from("ü"));
3288 k9::assert_equal!(
3289 mailbox.domain,
3290 Domain::DomainName("example.com".parse().unwrap())
3291 );
3292 }
3293
3294 #[test]
3295 fn test_mail_from_utf8_local_part_roundtrip() {
3296 let input = "MAIL FROM:<ü@example.com>";
3299 let cmd = parse_full(input);
3300 k9::assert_equal!(
3301 unwrapper(Command::parse(cmd.encode())),
3302 MaybePartialCommand::Full(cmd)
3303 );
3304 }
3305
3306 #[test]
3307 fn test_mail_from_quoted_utf8_local_part() {
3308 let cmd = parse_full("MAIL FROM:<\"ü\"@example.com>");
3310 let mailbox = match cmd {
3311 Command::MailFrom {
3312 address: ReversePath::Path(path),
3313 ..
3314 } => path.mailbox,
3315 other => panic!("unexpected {other:?}"),
3316 };
3317 k9::assert_equal!(mailbox.local_part, String::from("\"ü\""));
3319 }
3320
3321 #[test]
3322 fn test_rcpt_to_utf8_local_part() {
3323 let cmd = parse_full("RCPT TO:<用户@example.com>");
3325 let mailbox = match cmd {
3326 Command::RcptTo {
3327 address: ForwardPath::Path(path),
3328 ..
3329 } => path.mailbox,
3330 other => panic!("unexpected {other:?}"),
3331 };
3332 k9::assert_equal!(mailbox.local_part, String::from("用户"));
3333 }
3334
3335 #[test]
3338 fn test_mail_from_u_label_domain() {
3339 let cmd = parse_full("MAIL FROM:<user@münchen.de>");
3341 let domain = match cmd {
3342 Command::MailFrom {
3343 address: ReversePath::Path(path),
3344 ..
3345 } => path.mailbox.domain,
3346 other => panic!("unexpected {other:?}"),
3347 };
3348 match domain {
3349 Domain::DomainName(ref s) => {
3350 k9::assert_equal!(s.as_str(), "xn--mnchen-3ya.de");
3351 }
3352 other => panic!("unexpected domain variant {other:?}"),
3353 }
3354 }
3355
3356 #[test]
3357 fn test_mail_from_u_label_domain_encode() {
3358 let cmd = parse_full("MAIL FROM:<user@münchen.de>");
3361 k9::assert_equal!(
3362 cmd.encode(),
3363 BString::from("MAIL FROM:<user@xn--mnchen-3ya.de>\r\n")
3364 );
3365 }
3366
3367 #[test]
3368 fn test_ehlo_u_label_domain() {
3369 let cmd = parse_full("EHLO münchen.de");
3371 match cmd {
3372 Command::Ehlo(Domain::DomainName(ref s)) => {
3373 k9::assert_equal!(s.as_str(), "xn--mnchen-3ya.de");
3374 }
3375 other => panic!("unexpected {other:?}"),
3376 }
3377 }
3378
3379 #[test]
3380 fn test_ehlo_u_label_domain_encode() {
3381 let cmd = parse_full("EHLO münchen.de");
3383 k9::assert_equal!(cmd.encode(), BString::from("EHLO xn--mnchen-3ya.de\r\n"));
3384 }
3385
3386 #[test]
3389 fn test_esmtp_value_utf8() {
3390 let cmd = parse_full("MAIL FROM:<u@h> PARAM=valüe");
3392 let params = match cmd {
3393 Command::MailFrom { parameters, .. } => parameters,
3394 other => panic!("unexpected {other:?}"),
3395 };
3396 k9::assert_equal!(params.len(), 1);
3397 k9::assert_equal!(params[0].name, "PARAM");
3398 k9::assert_equal!(params[0].value, Some("valüe".to_string()));
3399 }
3400
3401 #[test]
3402 fn test_esmtp_value_utf8_roundtrip() {
3403 let input = "MAIL FROM:<u@h> PARAM=valüe";
3405 let cmd = parse_full(input);
3406 k9::assert_equal!(
3407 cmd.encode(),
3408 BString::from("MAIL FROM:<u@h> PARAM=valüe\r\n")
3409 );
3410 k9::assert_equal!(
3411 unwrapper(Command::parse(cmd.encode())),
3412 MaybePartialCommand::Full(parse_full(input))
3413 );
3414 }
3415
3416 #[test]
3417 fn test_esmtp_value_utf8_only() {
3418 let cmd = parse_full("MAIL FROM:<u@h> X=ünïcödé");
3420 let params = match cmd {
3421 Command::MailFrom { parameters, .. } => parameters,
3422 other => panic!("unexpected {other:?}"),
3423 };
3424 k9::assert_equal!(params[0].name, "X");
3425 k9::assert_equal!(params[0].value, Some("ünïcödé".to_string()));
3426 }
3427
3428 #[test]
3431 fn test_reverse_path_null_sender_try_into_mailpath_err() {
3432 use core::convert::TryInto;
3433 let null_sender = ReversePath::NullSender;
3434 let err: &'static str =
3435 <ReversePath as TryInto<MailPath>>::try_into(null_sender).unwrap_err();
3436 k9::assert_equal!(err, "Cannot convert NullSender to MailPath");
3437 }
3438
3439 #[test]
3440 fn test_forward_path_postmaster_try_into_mailpath_err() {
3441 use core::convert::TryInto;
3442 let postmaster = ForwardPath::Postmaster;
3443 let err: &'static str =
3444 <ForwardPath as TryInto<MailPath>>::try_into(postmaster).unwrap_err();
3445 k9::assert_equal!(err, "Cannot convert Postmaster to MailPath");
3446 }
3447
3448 #[test]
3449 fn test_reverse_path_try_from_null_sender_err() {
3450 let null_sender = ReversePath::NullSender;
3451 let err = MailPath::try_from(null_sender).unwrap_err();
3452 k9::assert_equal!(err, "Cannot convert NullSender to MailPath");
3453 }
3454
3455 #[test]
3456 fn test_forward_path_try_from_postmaster_err() {
3457 let postmaster = ForwardPath::Postmaster;
3458 let err = MailPath::try_from(postmaster).unwrap_err();
3459 k9::assert_equal!(err, "Cannot convert Postmaster to MailPath");
3460 }
3461
3462 #[test]
3465 fn test_envelope_address_try_from_mailbox() {
3466 let mailbox = Mailbox {
3467 local_part: String::from("user"),
3468 domain: Domain::DomainName("example.com".parse().unwrap()),
3469 };
3470 let addr = EnvelopeAddress::from(mailbox);
3471 match addr {
3472 EnvelopeAddress::Path(path) => {
3473 k9::assert_equal!(path.mailbox.local_part(), "user");
3474 k9::assert_equal!(
3475 path.mailbox.domain,
3476 Domain::DomainName("example.com".parse().unwrap())
3477 );
3478 }
3479 _ => panic!("Expected Path variant"),
3480 }
3481 }
3482
3483 #[test]
3484 fn test_envelope_address_try_from_null_err() {
3485 let addr = EnvelopeAddress::Null;
3486 let err = Mailbox::try_from(addr).unwrap_err();
3487 k9::assert_equal!(err, "Cannot convert Null to Mailbox");
3488 }
3489
3490 #[test]
3491 fn test_envelope_address_try_from_postmaster_err() {
3492 let addr = EnvelopeAddress::Postmaster;
3493 let err = Mailbox::try_from(addr).unwrap_err();
3494 k9::assert_equal!(err, "Cannot convert Postmaster to Mailbox");
3495 }
3496
3497 #[test]
3498 fn test_envelope_address_try_from_null_to_reverse_path() {
3499 let addr = EnvelopeAddress::Null;
3500 let result = ReversePath::try_from(addr).unwrap();
3501 k9::assert_equal!(result, ReversePath::NullSender);
3502 }
3503
3504 #[test]
3505 fn test_envelope_address_try_from_postmaster_to_forward_path() {
3506 let addr = EnvelopeAddress::Postmaster;
3507 let result = ForwardPath::try_from(addr).unwrap();
3508 k9::assert_equal!(result, ForwardPath::Postmaster);
3509 }
3510
3511 #[test]
3514 fn test_envelope_address_try_into_mailpath_null_err() {
3515 let addr = EnvelopeAddress::Null;
3516 let err = MailPath::try_from(addr).unwrap_err();
3517 k9::assert_equal!(err, "Cannot convert Null to MailPath");
3518 }
3519
3520 #[test]
3521 fn test_envelope_address_try_into_mailpath_postmaster_err() {
3522 let addr = EnvelopeAddress::Postmaster;
3523 let err = MailPath::try_from(addr).unwrap_err();
3524 k9::assert_equal!(err, "Cannot convert Postmaster to MailPath");
3525 }
3526
3527 #[test]
3528 fn test_reverse_path_try_into_forward_path_null_sender_err() {
3529 let rp = ReversePath::NullSender;
3530 let err = ForwardPath::try_from(rp).unwrap_err();
3531 k9::assert_equal!(err, "Cannot convert NullSender to ForwardPath");
3532 }
3533
3534 #[test]
3535 fn test_forward_path_try_into_reverse_path_postmaster_err() {
3536 let fp = ForwardPath::Postmaster;
3537 let err = ReversePath::try_from(fp).unwrap_err();
3538 k9::assert_equal!(err, "Cannot convert Postmaster to ReversePath");
3539 }
3540
3541 #[test]
3544 fn test_mailbox_into_mailpath() {
3545 let mailbox = Mailbox {
3546 local_part: String::from("user"),
3547 domain: Domain::DomainName("example.com".parse().unwrap()),
3548 };
3549 let path: MailPath = mailbox.into();
3550 assert!(path.at_domain_list.is_empty());
3551 k9::assert_equal!(path.mailbox.local_part(), "user");
3552 }
3553
3554 #[test]
3555 fn test_mailbox_into_envelope_address() {
3556 let mailbox = Mailbox {
3557 local_part: String::from("user"),
3558 domain: Domain::DomainName("example.com".parse().unwrap()),
3559 };
3560 let addr: EnvelopeAddress = mailbox.into();
3561 match addr {
3562 EnvelopeAddress::Path(path) => {
3563 k9::assert_equal!(path.mailbox.local_part(), "user");
3564 }
3565 _ => panic!("Expected Path variant"),
3566 }
3567 }
3568
3569 #[test]
3570 fn test_mailbox_into_reverse_path() {
3571 let mailbox = Mailbox {
3572 local_part: String::from("user"),
3573 domain: Domain::DomainName("example.com".parse().unwrap()),
3574 };
3575 let path: ReversePath = mailbox.into();
3576 match path {
3577 ReversePath::Path(p) => {
3578 k9::assert_equal!(p.mailbox.local_part(), "user");
3579 }
3580 _ => panic!("Expected Path variant"),
3581 }
3582 }
3583
3584 #[test]
3585 fn test_mailbox_into_forward_path() {
3586 let mailbox = Mailbox {
3587 local_part: String::from("user"),
3588 domain: Domain::DomainName("example.com".parse().unwrap()),
3589 };
3590 let path: ForwardPath = mailbox.into();
3591 match path {
3592 ForwardPath::Path(p) => {
3593 k9::assert_equal!(p.mailbox.local_part(), "user");
3594 }
3595 _ => panic!("Expected Path variant"),
3596 }
3597 }
3598
3599 #[test]
3602 fn test_mailbox_roundtrip() {
3603 let original = Mailbox {
3604 local_part: String::from("user"),
3605 domain: Domain::DomainName("example.com".parse().unwrap()),
3606 };
3607 let path: MailPath = original.clone().into();
3608 let mailbox: Mailbox = path.mailbox.into();
3609 k9::assert_equal!(original.local_part(), mailbox.local_part());
3610 k9::assert_equal!(original.domain, mailbox.domain);
3611 }
3612
3613 #[test]
3614 fn test_reverse_path_to_forward_path() {
3615 let path = MailPath {
3616 at_domain_list: vec!["route.com".into()],
3617 mailbox: Mailbox {
3618 local_part: String::from("user"),
3619 domain: Domain::DomainName("example.com".parse().unwrap()),
3620 },
3621 };
3622 let rp = ReversePath::Path(path.clone());
3623 let fp: ForwardPath = rp.try_into().unwrap();
3624 match fp {
3625 ForwardPath::Path(p) => {
3626 k9::assert_equal!(p.mailbox.local_part(), "user");
3627 }
3628 _ => panic!("Expected Path variant"),
3629 }
3630 }
3631
3632 #[test]
3635 fn test_mailpath_debug_simple() {
3636 let mailbox = Mailbox {
3637 local_part: "someone".into(),
3638 domain: Domain::DomainName("example.com".parse().unwrap()),
3639 };
3640 let path: MailPath = mailbox.into();
3641 let debug_str = format!("{:?}", path);
3642 k9::assert_equal!(debug_str, r#"MailPath("someone@example.com")"#);
3643 }
3644
3645 #[test]
3646 fn test_mailpath_debug_with_at_domain_list() {
3647 let path = MailPath {
3648 at_domain_list: vec!["route.example.com".into()],
3649 mailbox: Mailbox {
3650 local_part: "user".into(),
3651 domain: Domain::DomainName("host".parse().unwrap()),
3652 },
3653 };
3654 let debug_str = format!("{:?}", path);
3655 k9::assert_equal!(debug_str, r#"MailPath("@route.example.com:user@host")"#);
3656 }
3657
3658 #[test]
3659 fn test_mailpath_debug_multiple_at_domains() {
3660 let path = MailPath {
3661 at_domain_list: vec!["hosta.int".into(), "jkl.org".into()],
3662 mailbox: Mailbox {
3663 local_part: "userc".into(),
3664 domain: Domain::DomainName("d.bar.org".parse().unwrap()),
3665 },
3666 };
3667 let debug_str = format!("{:?}", path);
3668 k9::assert_equal!(
3669 debug_str,
3670 r#"MailPath("@hosta.int,@jkl.org:userc@d.bar.org")"#
3671 );
3672 }
3673
3674 #[test]
3675 fn test_mailpath_debug_non_ascii_utf8() {
3676 let mailbox = Mailbox {
3679 local_part: "üser".into(),
3680 domain: Domain::DomainName("example.com".parse().unwrap()),
3681 };
3682 let path: MailPath = mailbox.into();
3683 let debug_str = format!("{:?}", path);
3684 k9::assert_equal!(debug_str, r#"MailPath("üser@example.com")"#);
3686 }
3687
3688 #[test]
3689 fn test_mailpath_debug_with_ipv4() {
3690 let mailbox = Mailbox {
3691 local_part: "user".into(),
3692 domain: Domain::V4("10.0.0.1".parse().unwrap()),
3693 };
3694 let path: MailPath = mailbox.into();
3695 let debug_str = format!("{:?}", path);
3696 k9::assert_equal!(debug_str, r#"MailPath("user@[10.0.0.1]")"#);
3698 }
3699
3700 #[test]
3701 fn test_mailpath_debug_with_ipv6() {
3702 let mailbox = Mailbox {
3703 local_part: "user".into(),
3704 domain: Domain::V6("::1".parse().unwrap()),
3705 };
3706 let path: MailPath = mailbox.into();
3707 let debug_str = format!("{:?}", path);
3708 k9::assert_equal!(debug_str, r#"MailPath("user@[IPv6:::1]")"#);
3710 }
3711
3712 #[test]
3713 fn test_mailpath_debug_with_tagged_literal() {
3714 let mailbox = Mailbox {
3715 local_part: "user".into(),
3716 domain: Domain::Tagged("future:something".into()),
3717 };
3718 let path: MailPath = mailbox.into();
3719 let debug_str = format!("{:?}", path);
3720 k9::assert_equal!(debug_str, r#"MailPath("user@[future:something]")"#);
3722 }
3723 #[test]
3724 fn test_envelope_address_debug_null() {
3725 let addr = EnvelopeAddress::Null;
3726 let debug_str = format!("{:?}", addr);
3727 k9::assert_equal!(debug_str, "<>");
3728 }
3729
3730 #[test]
3731 fn test_envelope_address_debug_postmaster() {
3732 let addr = EnvelopeAddress::Postmaster;
3733 let debug_str = format!("{:?}", addr);
3734 k9::assert_equal!(debug_str, "<Postmaster>");
3735 }
3736
3737 #[test]
3738 fn test_envelope_address_postmaster_full_address() {
3739 let postmaster = EnvelopeAddress::parse(r#"postmaster@example.com"#).unwrap();
3740 k9::assert_equal!(postmaster.to_string(), r#"postmaster@example.com"#);
3741 }
3742
3743 #[test]
3744 fn test_envelope_address_debug_path_with_non_ascii_utf8() {
3745 let path = MailPath {
3748 at_domain_list: vec!["example.com".into()],
3749 mailbox: Mailbox {
3750 local_part: String::from("üser"),
3751 domain: Domain::DomainName("example.com".parse().unwrap()),
3752 },
3753 };
3754 let addr = EnvelopeAddress::Path(path);
3755 let debug_str = format!("{:?}", addr);
3756 k9::assert_equal!(debug_str, r#"<üser@example.com>"#);
3760 }
3761
3762 #[test]
3763 fn test_envelope_address_display_drops_source_route() {
3764 let path = MailPath {
3765 at_domain_list: vec!["hosta.int".into(), "jkl.org".into()],
3766 mailbox: Mailbox {
3767 local_part: String::from("userc"),
3768 domain: Domain::DomainName("d.bar.org".parse().unwrap()),
3769 },
3770 };
3771 let addr = EnvelopeAddress::Path(path);
3772 k9::assert_equal!(addr.to_string(), "userc@d.bar.org");
3774 if let EnvelopeAddress::Path(ref p) = addr {
3776 k9::assert_equal!(p.to_string(), "@hosta.int,@jkl.org:userc@d.bar.org");
3777 }
3778
3779 let path2 = MailPath {
3781 at_domain_list: vec!["relay.example".into()],
3782 mailbox: Mailbox {
3783 local_part: String::from(r#""info@""#),
3784 domain: Domain::DomainName("example.com".parse().unwrap()),
3785 },
3786 };
3787 let addr2 = EnvelopeAddress::Path(path2);
3788 k9::assert_equal!(addr2.to_string(), r#""info@"@example.com"#);
3789 if let EnvelopeAddress::Path(ref p) = addr2 {
3790 k9::assert_equal!(p.to_string(), r#"@relay.example:"info@"@example.com"#);
3791 }
3792 }
3793
3794 #[test]
3795 fn test_envelope_address_serde_roundtrip_with_source_route() {
3796 let cmd = unwrapper(Command::parse(
3798 "MAIL FROM:<@hosta.int,@jkl.org:userc@d.bar.org>",
3799 ));
3800 if let MaybePartialCommand::Full(Command::MailFrom { address, .. }) = cmd {
3801 if let ReversePath::Path(mail_path) = address {
3802 let addr = EnvelopeAddress::Path(mail_path);
3803 let serialized = serde_json::to_string(&addr).unwrap();
3805 k9::assert_equal!(serialized, r#""userc@d.bar.org""#);
3806 let deserialized: EnvelopeAddress = serde_json::from_str(&serialized).unwrap();
3808 k9::assert_equal!(deserialized.to_string(), "userc@d.bar.org");
3810 } else {
3811 panic!("Expected Path variant");
3812 }
3813 } else {
3814 panic!("Expected Full MailFrom");
3815 }
3816
3817 let cmd2 = unwrapper(Command::parse(
3819 r#"MAIL FROM:<@relay.example:"info@"@example.com>"#,
3820 ));
3821 if let MaybePartialCommand::Full(Command::MailFrom { address, .. }) = cmd2 {
3822 if let ReversePath::Path(mail_path) = address {
3823 let addr = EnvelopeAddress::Path(mail_path);
3824 let serialized = serde_json::to_string(&addr).unwrap();
3825 k9::assert_equal!(serialized, r#""\"info@\"@example.com""#);
3826 let deserialized: EnvelopeAddress = serde_json::from_str(&serialized).unwrap();
3827 k9::assert_equal!(deserialized.to_string(), r#""info@"@example.com"#);
3828 } else {
3829 panic!("Expected Path variant");
3830 }
3831 } else {
3832 panic!("Expected Full MailFrom");
3833 }
3834 }
3835
3836 #[test]
3837 fn test_envelope_address_rejects_obs_local_part() {
3838 EnvelopeAddress::parse(r#""first".last@example.com"#).unwrap_err();
3841 EnvelopeAddress::parse(r#"first."last"@example.com"#).unwrap_err();
3842 EnvelopeAddress::parse(r#""first"."last"@example.com"#).unwrap_err();
3843 }
3844
3845 #[test]
3846 fn test_envelope_address_null_sender_roundtrip() {
3847 let null = EnvelopeAddress::Null;
3851
3852 k9::assert_equal!(null.to_string(), "");
3854
3855 let parsed = EnvelopeAddress::parse("").unwrap();
3857 k9::assert_equal!(parsed, EnvelopeAddress::Null);
3858
3859 let json = serde_json::to_string(&null).unwrap();
3861 k9::assert_equal!(json, "\"\"");
3862 let deserialized: EnvelopeAddress = serde_json::from_str(&json).unwrap();
3863 k9::assert_equal!(deserialized, EnvelopeAddress::Null);
3864 }
3865}