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> {
1817 context(
1818 "mail-from",
1819 alt((
1820 map(
1822 all_consuming((
1823 tag_no_case("MAIL"),
1824 wsp,
1825 tag_no_case("FROM:"),
1826 opt(wsp),
1830 reverse_path,
1831 opt(map((wsp, mail_parameters), |(_, p)| p)),
1832 )),
1833 |(_, _, _, _, address, parameters)| {
1834 MaybePartialCommand::Full(Command::MailFrom {
1835 address,
1836 parameters: parameters.unwrap_or_default(),
1837 })
1838 },
1839 ),
1840 map(
1842 all_consuming((
1843 tag_no_case("MAIL"),
1844 wsp,
1845 tag_no_case("FROM:"),
1846 opt(wsp),
1847 reverse_path,
1848 wsp,
1849 anything,
1850 )),
1851 |(_, _, _, _, _, _, remainder)| MaybePartialCommand::Partial {
1852 verb: CommandVerb::Mail,
1853 remainder: (*remainder).into(),
1854 reason: PartialReason::Syntax,
1855 },
1856 ),
1857 map(
1859 all_consuming((tag_no_case("MAIL"), wsp, tag_no_case("FROM:"), anything)),
1860 |(_, _, _, remainder)| MaybePartialCommand::Partial {
1861 verb: CommandVerb::Mail,
1862 remainder: (*remainder).into(),
1863 reason: PartialReason::InvalidSenderAddress,
1864 },
1865 ),
1866 map(
1868 all_consuming((tag_no_case("MAIL"), wsp, anything)),
1869 |(_, _, remainder)| MaybePartialCommand::Partial {
1870 verb: CommandVerb::Mail,
1871 remainder: (*remainder).into(),
1872 reason: PartialReason::Syntax,
1873 },
1874 ),
1875 map(all_consuming(tag_no_case("MAIL")), |_| {
1877 MaybePartialCommand::Partial {
1878 verb: CommandVerb::Mail,
1879 remainder: BString::default(),
1880 reason: PartialReason::Syntax,
1881 }
1882 }),
1883 )),
1884 )
1885 .parse(input)
1886}
1887
1888fn parse_rcpt_to(input: Span) -> IResult<Span, MaybePartialCommand> {
1901 context(
1902 "rcpt-to",
1903 alt((
1904 map(
1906 all_consuming((
1907 tag_no_case("RCPT"),
1908 wsp,
1909 tag_no_case("TO:"),
1910 opt(wsp),
1913 forward_path,
1914 opt(map((wsp, mail_parameters), |(_, p)| p)),
1915 )),
1916 |(_, _, _, _, address, parameters)| {
1917 MaybePartialCommand::Full(Command::RcptTo {
1918 address,
1919 parameters: parameters.unwrap_or_default(),
1920 })
1921 },
1922 ),
1923 map(
1925 all_consuming((
1926 tag_no_case("RCPT"),
1927 wsp,
1928 tag_no_case("TO:"),
1929 opt(wsp),
1930 forward_path,
1931 wsp,
1932 anything,
1933 )),
1934 |(_, _, _, _, _, _, remainder)| MaybePartialCommand::Partial {
1935 verb: CommandVerb::Rcpt,
1936 remainder: (*remainder).into(),
1937 reason: PartialReason::Syntax,
1938 },
1939 ),
1940 map(
1942 all_consuming((tag_no_case("RCPT"), wsp, tag_no_case("TO:"), anything)),
1943 |(_, _, _, remainder)| MaybePartialCommand::Partial {
1944 verb: CommandVerb::Rcpt,
1945 remainder: (*remainder).into(),
1946 reason: PartialReason::InvalidRecipientAddress,
1947 },
1948 ),
1949 map(
1951 all_consuming((tag_no_case("RCPT"), wsp, anything)),
1952 |(_, _, remainder)| MaybePartialCommand::Partial {
1953 verb: CommandVerb::Rcpt,
1954 remainder: (*remainder).into(),
1955 reason: PartialReason::Syntax,
1956 },
1957 ),
1958 map(all_consuming(tag_no_case("RCPT")), |_| {
1960 MaybePartialCommand::Partial {
1961 verb: CommandVerb::Rcpt,
1962 remainder: BString::default(),
1963 reason: PartialReason::Syntax,
1964 }
1965 }),
1966 )),
1967 )
1968 .parse(input)
1969}
1970
1971fn hex_nibble_lower(n: u8) -> u8 {
1977 if n < 10 {
1978 b'0' + n
1979 } else {
1980 b'a' + n - 10
1981 }
1982}
1983
1984fn xtext_encode_bytes(value: &[u8]) -> Vec<u8> {
1990 let mut result = Vec::with_capacity(value.len());
1991 for &b in value {
1992 if b >= 33 && b <= 126 && b != b'+' && b != b'=' {
1993 result.push(b);
1994 } else {
1995 result.push(b'+');
1996 result.push(hex_nibble_lower(b >> 4));
1997 result.push(hex_nibble_lower(b & 0x0f));
1998 }
1999 }
2000 result
2001}
2002
2003fn encode_domain(domain: &Domain) -> BString {
2010 match domain {
2011 Domain::DomainName(s) => BString::from(s.to_string()),
2012 Domain::V4(ip) => BString::from(format!("[{}]", ip)),
2013 Domain::V6(ip) => BString::from(format!("[IPv6:{}]", ip)),
2014 Domain::Tagged(s) => BString::from(format!("[{}]", s)),
2015 }
2016}
2017
2018fn encode_mail_path(path: &MailPath) -> Vec<u8> {
2024 let mut buf = path.mailbox.local_part.as_bytes().to_vec();
2025 buf.push(b'@');
2026 buf.extend_from_slice(encode_domain(&path.mailbox.domain).as_ref());
2027 buf
2028}
2029
2030fn encode_reverse_path(rp: &ReversePath) -> Vec<u8> {
2036 match rp {
2037 ReversePath::NullSender => vec![],
2038 ReversePath::Path(path) => encode_mail_path(path),
2039 }
2040}
2041
2042fn encode_forward_path(fp: &ForwardPath) -> Vec<u8> {
2048 match fp {
2049 ForwardPath::Postmaster => b"Postmaster".to_vec(),
2050 ForwardPath::Path(path) => encode_mail_path(path),
2051 }
2052}
2053
2054fn encode_esmtp_params(params: &[EsmtpParameter]) -> Vec<u8> {
2059 let mut buf = Vec::new();
2060 for p in params {
2061 buf.push(b' ');
2062 buf.extend_from_slice(p.name.as_bytes());
2063 if let Some(v) = &p.value {
2064 buf.push(b'=');
2065 buf.extend_from_slice(v.as_bytes());
2066 }
2067 }
2068 buf
2069}
2070
2071fn encode_xclient_params(params: &[XClientParameter]) -> Vec<u8> {
2075 let mut buf = Vec::new();
2076 for p in params {
2077 buf.push(b' ');
2078 buf.extend_from_slice(p.name.as_bytes());
2079 buf.push(b'=');
2080 buf.extend(xtext_encode_bytes(p.value.as_bytes()));
2081 }
2082 buf
2083}
2084
2085#[cfg(test)]
2086mod test {
2087 use super::*;
2088
2089 #[test]
2090 fn test_string() {
2091 k9::snapshot!(
2092 BStr::new(&parse_with("hello".as_bytes(), string).unwrap()),
2093 "hello"
2094 );
2095 k9::snapshot!(
2096 BStr::new(&parse_with("\"hello\"".as_bytes(), string).unwrap()),
2097 "\"hello\""
2098 );
2099 k9::snapshot!(
2100 BStr::new(&parse_with("\"hello world\"".as_bytes(), string).unwrap()),
2101 "\"hello world\""
2102 );
2103 k9::snapshot!(
2104 parse_with("hello world".as_bytes(), string),
2105 r#"
2106Err(
2107 "Error at line 1, in Eof:
2108hello world
2109 ^_____
2110
2111",
2112)
2113"#
2114 );
2115 }
2116
2117 #[test]
2118 fn test_bogus() {
2119 k9::snapshot!(
2120 Command::parse("bogus"),
2121 r#"
2122Ok(
2123 Full(Command("bogus\r
2124")),
2125)
2126"#
2127 );
2128 }
2129
2130 #[test]
2135 fn test_ehlo_domain_name() {
2136 k9::assert_equal!(
2137 unwrapper(Command::parse("EHLO example.com")),
2138 MaybePartialCommand::Full(Command::Ehlo(Domain::DomainName(
2139 "example.com".parse().unwrap()
2140 )))
2141 );
2142 }
2143
2144 #[test]
2145 fn test_ehlo_case_insensitive() {
2146 k9::assert_equal!(
2147 unwrapper(Command::parse("ehlo example.com")),
2148 MaybePartialCommand::Full(Command::Ehlo(Domain::DomainName(
2149 "example.com".parse().unwrap()
2150 )))
2151 );
2152 }
2153
2154 #[test]
2155 fn test_ehlo_ipv4_literal() {
2156 k9::assert_equal!(
2157 unwrapper(Command::parse("EHLO [10.0.0.1]")),
2158 MaybePartialCommand::Full(Command::Ehlo(Domain::V4("10.0.0.1".parse().unwrap())))
2159 );
2160 }
2161
2162 #[test]
2163 fn test_ehlo_ipv6_literal() {
2164 k9::assert_equal!(
2165 unwrapper(Command::parse("EHLO [IPv6:::1]")),
2166 MaybePartialCommand::Full(Command::Ehlo(Domain::V6("::1".parse().unwrap())))
2167 );
2168 }
2169
2170 #[test]
2171 fn test_ehlo_tagged_literal() {
2172 k9::assert_equal!(
2173 unwrapper(Command::parse("EHLO [future:something]")),
2174 MaybePartialCommand::Full(Command::Ehlo(Domain::Tagged("future:something".into(),)))
2175 );
2176 }
2177
2178 #[test]
2179 fn test_ehlo_invalid_ipv4_is_partial() {
2180 k9::assert_equal!(
2181 unwrapper(Command::parse("EHLO [999.999.999.999]")),
2182 MaybePartialCommand::Partial {
2183 verb: CommandVerb::Ehlo,
2184 remainder: "[999.999.999.999]".into(),
2185 reason: PartialReason::Syntax,
2186 }
2187 );
2188 }
2189
2190 #[test]
2191 fn test_ehlo_invalid_ipv6_is_partial() {
2192 k9::assert_equal!(
2193 unwrapper(Command::parse("EHLO [IPv6:not-an-ipv6]")),
2194 MaybePartialCommand::Partial {
2195 verb: CommandVerb::Ehlo,
2196 remainder: "[IPv6:not-an-ipv6]".into(),
2197 reason: PartialReason::Syntax,
2198 }
2199 );
2200 }
2201
2202 #[test]
2203 fn test_ehlo_alone_is_partial() {
2204 k9::assert_equal!(
2205 unwrapper(Command::parse("EHLO")),
2206 MaybePartialCommand::Partial {
2207 verb: CommandVerb::Ehlo,
2208 remainder: "".into(),
2209 reason: PartialReason::Syntax,
2210 }
2211 );
2212 }
2213
2214 #[test]
2215 fn test_ehlo_with_garbage_is_partial() {
2216 k9::assert_equal!(
2217 unwrapper(Command::parse("EHLO !!invalid!!")),
2218 MaybePartialCommand::Partial {
2219 verb: CommandVerb::Ehlo,
2220 remainder: "!!invalid!!".into(),
2221 reason: PartialReason::Syntax,
2222 }
2223 );
2224 }
2225
2226 #[test]
2231 fn test_helo_domain_name() {
2232 k9::assert_equal!(
2233 unwrapper(Command::parse("HELO example.com")),
2234 MaybePartialCommand::Full(Command::Helo(Domain::DomainName(
2235 "example.com".parse().unwrap()
2236 )))
2237 );
2238 }
2239
2240 #[test]
2241 fn test_helo_case_insensitive() {
2242 k9::assert_equal!(
2243 unwrapper(Command::parse("helo example.com")),
2244 MaybePartialCommand::Full(Command::Helo(Domain::DomainName(
2245 "example.com".parse().unwrap()
2246 )))
2247 );
2248 }
2249
2250 #[test]
2251 fn test_helo_ipv4_literal() {
2252 k9::assert_equal!(
2254 unwrapper(Command::parse("HELO [10.0.0.1]")),
2255 MaybePartialCommand::Full(Command::Helo(Domain::V4("10.0.0.1".parse().unwrap())))
2256 );
2257 }
2258
2259 #[test]
2260 fn test_helo_alone_is_partial() {
2261 k9::assert_equal!(
2262 unwrapper(Command::parse("HELO")),
2263 MaybePartialCommand::Partial {
2264 verb: CommandVerb::Helo,
2265 remainder: "".into(),
2266 reason: PartialReason::Syntax,
2267 }
2268 );
2269 }
2270
2271 #[test]
2272 fn test_helo_with_garbage_is_partial() {
2273 k9::assert_equal!(
2274 unwrapper(Command::parse("HELO !!invalid!!")),
2275 MaybePartialCommand::Partial {
2276 verb: CommandVerb::Helo,
2277 remainder: "!!invalid!!".into(),
2278 reason: PartialReason::Syntax,
2279 }
2280 );
2281 }
2282
2283 #[test]
2288 fn test_lhlo_domain_name() {
2289 k9::assert_equal!(
2290 unwrapper(Command::parse("LHLO example.com")),
2291 MaybePartialCommand::Full(Command::Lhlo(Domain::DomainName(
2292 "example.com".parse().unwrap()
2293 )))
2294 );
2295 }
2296
2297 #[test]
2298 fn test_lhlo_case_insensitive() {
2299 k9::assert_equal!(
2300 unwrapper(Command::parse("lhlo example.com")),
2301 MaybePartialCommand::Full(Command::Lhlo(Domain::DomainName(
2302 "example.com".parse().unwrap()
2303 )))
2304 );
2305 }
2306
2307 #[test]
2308 fn test_lhlo_ipv4_literal() {
2309 k9::assert_equal!(
2310 unwrapper(Command::parse("LHLO [10.0.0.1]")),
2311 MaybePartialCommand::Full(Command::Lhlo(Domain::V4("10.0.0.1".parse().unwrap())))
2312 );
2313 }
2314
2315 #[test]
2316 fn test_lhlo_ipv6_literal() {
2317 k9::assert_equal!(
2318 unwrapper(Command::parse("LHLO [IPv6:::1]")),
2319 MaybePartialCommand::Full(Command::Lhlo(Domain::V6("::1".parse().unwrap())))
2320 );
2321 }
2322
2323 #[test]
2324 fn test_lhlo_invalid_ipv4_is_partial() {
2325 k9::assert_equal!(
2326 unwrapper(Command::parse("LHLO [999.999.999.999]")),
2327 MaybePartialCommand::Partial {
2328 verb: CommandVerb::Lhlo,
2329 remainder: "[999.999.999.999]".into(),
2330 reason: PartialReason::Syntax,
2331 }
2332 );
2333 }
2334
2335 #[test]
2336 fn test_lhlo_alone_is_partial() {
2337 k9::assert_equal!(
2338 unwrapper(Command::parse("LHLO")),
2339 MaybePartialCommand::Partial {
2340 verb: CommandVerb::Lhlo,
2341 remainder: "".into(),
2342 reason: PartialReason::Syntax,
2343 }
2344 );
2345 }
2346
2347 #[test]
2352 fn test_auth_mechanism_only() {
2353 k9::assert_equal!(
2354 unwrapper(Command::parse("AUTH PLAIN")),
2355 MaybePartialCommand::Full(Command::Auth {
2356 sasl_mech: "PLAIN".into(),
2357 initial_response: None,
2358 })
2359 );
2360 }
2361
2362 #[test]
2363 fn test_auth_with_initial_response() {
2364 k9::assert_equal!(
2365 unwrapper(Command::parse("AUTH PLAIN dXNlcjpwYXNz")),
2366 MaybePartialCommand::Full(Command::Auth {
2367 sasl_mech: "PLAIN".into(),
2368 initial_response: Some("dXNlcjpwYXNz".into()),
2369 })
2370 );
2371 }
2372
2373 #[test]
2374 fn test_auth_empty_initial_response() {
2375 k9::assert_equal!(
2377 unwrapper(Command::parse("AUTH PLAIN =")),
2378 MaybePartialCommand::Full(Command::Auth {
2379 sasl_mech: "PLAIN".into(),
2380 initial_response: Some("=".into()),
2381 })
2382 );
2383 }
2384
2385 #[test]
2386 fn test_auth_hyphenated_mechanism() {
2387 k9::assert_equal!(
2388 unwrapper(Command::parse("AUTH CRAM-MD5")),
2389 MaybePartialCommand::Full(Command::Auth {
2390 sasl_mech: "CRAM-MD5".into(),
2391 initial_response: None,
2392 })
2393 );
2394 }
2395
2396 #[test]
2397 fn test_auth_case_insensitive() {
2398 k9::assert_equal!(
2399 unwrapper(Command::parse("auth plain")),
2400 MaybePartialCommand::Full(Command::Auth {
2401 sasl_mech: "plain".into(),
2402 initial_response: None,
2403 })
2404 );
2405 }
2406
2407 #[test]
2408 fn test_auth_alone_is_partial() {
2409 k9::assert_equal!(
2410 unwrapper(Command::parse("AUTH")),
2411 MaybePartialCommand::Partial {
2412 verb: CommandVerb::Auth,
2413 remainder: "".into(),
2414 reason: PartialReason::Syntax,
2415 }
2416 );
2417 }
2418
2419 #[test]
2420 fn test_auth_with_garbage_is_partial() {
2421 k9::assert_equal!(
2423 unwrapper(Command::parse("AUTH !!bad!!")),
2424 MaybePartialCommand::Partial {
2425 verb: CommandVerb::Auth,
2426 remainder: "!!bad!!".into(),
2427 reason: PartialReason::Syntax,
2428 }
2429 );
2430 }
2431
2432 #[test]
2437 fn test_xclient_single_param() {
2438 k9::assert_equal!(
2439 unwrapper(Command::parse("XCLIENT NAME=foo.example.com")),
2440 MaybePartialCommand::Full(Command::XClient(vec![XClientParameter {
2441 name: "NAME".into(),
2442 value: "foo.example.com".into(),
2443 }]))
2444 );
2445 }
2446
2447 #[test]
2448 fn test_xclient_multiple_params() {
2449 k9::assert_equal!(
2450 unwrapper(Command::parse("XCLIENT NAME=foo.example.com ADDR=10.0.0.1")),
2451 MaybePartialCommand::Full(Command::XClient(vec![
2452 XClientParameter {
2453 name: "NAME".into(),
2454 value: "foo.example.com".into(),
2455 },
2456 XClientParameter {
2457 name: "ADDR".into(),
2458 value: "10.0.0.1".into(),
2459 },
2460 ]))
2461 );
2462 }
2463
2464 #[test]
2465 fn test_xclient_xtext_hex_escape() {
2466 k9::assert_equal!(
2468 unwrapper(Command::parse("XCLIENT NAME=user+40example.com")),
2469 MaybePartialCommand::Full(Command::XClient(vec![XClientParameter {
2470 name: "NAME".into(),
2471 value: "user@example.com".into(),
2472 }]))
2473 );
2474 }
2475
2476 #[test]
2477 fn test_xclient_case_insensitive() {
2478 k9::assert_equal!(
2479 unwrapper(Command::parse("xclient NAME=host")),
2480 MaybePartialCommand::Full(Command::XClient(vec![XClientParameter {
2481 name: "NAME".into(),
2482 value: "host".into(),
2483 }]))
2484 );
2485 }
2486
2487 #[test]
2488 fn test_xclient_alone_is_partial() {
2489 k9::assert_equal!(
2490 unwrapper(Command::parse("XCLIENT")),
2491 MaybePartialCommand::Partial {
2492 verb: CommandVerb::XClient,
2493 remainder: "".into(),
2494 reason: PartialReason::Syntax,
2495 }
2496 );
2497 }
2498
2499 #[test]
2500 fn test_xclient_invalid_xtext_is_partial() {
2501 k9::assert_equal!(
2503 unwrapper(Command::parse("XCLIENT NAME=bad+ZZ")),
2504 MaybePartialCommand::Partial {
2505 verb: CommandVerb::XClient,
2506 remainder: "NAME=bad+ZZ".into(),
2507 reason: PartialReason::Syntax,
2508 }
2509 );
2510 }
2511
2512 #[test]
2513 fn test_xclient_garbage_is_partial() {
2514 k9::assert_equal!(
2516 unwrapper(Command::parse("XCLIENT noequals")),
2517 MaybePartialCommand::Partial {
2518 verb: CommandVerb::XClient,
2519 remainder: "noequals".into(),
2520 reason: PartialReason::Syntax,
2521 }
2522 );
2523 }
2524
2525 #[test]
2526 fn test_xclient_parameter_methods() {
2527 let cmd = unwrapper(Command::parse("XCLIENT ADDR=192.168.1.1"));
2529 let params = match cmd {
2530 MaybePartialCommand::Full(Command::XClient(params)) => params,
2531 _ => panic!("Expected XCLIENT command"),
2532 };
2533
2534 k9::assert_equal!(params[0].is_name("ADDR"), true);
2536 k9::assert_equal!(params[0].is_name("addr"), true);
2537 k9::assert_equal!(params[0].is_name("NAME"), false);
2538
2539 let ip: std::net::IpAddr = params[0].parse().expect("Failed to parse IP address");
2541 k9::assert_equal!(
2542 ip,
2543 std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 1, 1))
2544 );
2545 }
2546
2547 #[test]
2548 fn test_xclient_parameter_parse_invalid_ip() {
2549 let cmd = unwrapper(Command::parse("XCLIENT ADDR=not-an-ip"));
2551 let params = match cmd {
2552 MaybePartialCommand::Full(Command::XClient(params)) => params,
2553 _ => panic!("Expected XCLIENT command"),
2554 };
2555
2556 let result: Result<std::net::IpAddr, _> = params[0].parse();
2557 k9::assert_equal!(result.unwrap_err(), "invalid IP address syntax");
2558 }
2559
2560 fn mail_path(local: &str, domain: Domain) -> ReversePath {
2565 Mailbox {
2566 local_part: local.into(),
2567 domain,
2568 }
2569 .into()
2570 }
2571
2572 #[test]
2573 fn test_mail_from_domain_name() {
2574 k9::assert_equal!(
2575 unwrapper(Command::parse("MAIL FROM:<user@host>")),
2576 MaybePartialCommand::Full(Command::MailFrom {
2577 address: mail_path("user", Domain::DomainName("host".parse().unwrap())),
2578 parameters: vec![],
2579 })
2580 );
2581 k9::assert_equal!(
2583 unwrapper(Command::parse("mail from:<user@host>")),
2584 MaybePartialCommand::Full(Command::MailFrom {
2585 address: mail_path("user", Domain::DomainName("host".parse().unwrap())),
2586 parameters: vec![],
2587 })
2588 );
2589 }
2590
2591 #[test]
2592 fn test_mail_from_null_sender() {
2593 k9::assert_equal!(
2594 unwrapper(Command::parse("MAIL FROM:<>")),
2595 MaybePartialCommand::Full(Command::MailFrom {
2596 address: ReversePath::NullSender,
2597 parameters: vec![],
2598 })
2599 );
2600 }
2601
2602 #[test]
2603 fn test_mail_from_ipv4() {
2604 k9::assert_equal!(
2605 unwrapper(Command::parse("MAIL FROM:<user@[10.0.0.1]>")),
2606 MaybePartialCommand::Full(Command::MailFrom {
2607 address: mail_path("user", Domain::V4("10.0.0.1".parse().unwrap())),
2608 parameters: vec![],
2609 })
2610 );
2611 }
2612
2613 #[test]
2614 fn test_mail_from_ipv6() {
2615 k9::assert_equal!(
2616 unwrapper(Command::parse("MAIL FROM:<user@[IPv6:::1]>")),
2617 MaybePartialCommand::Full(Command::MailFrom {
2618 address: mail_path("user", Domain::V6("::1".parse().unwrap())),
2619 parameters: vec![],
2620 })
2621 );
2622 }
2623
2624 #[test]
2625 fn test_mail_from_tagged_literal() {
2626 k9::assert_equal!(
2627 unwrapper(Command::parse("MAIL FROM:<user@[future:something]>")),
2628 MaybePartialCommand::Full(Command::MailFrom {
2629 address: mail_path("user", Domain::Tagged("future:something".into())),
2630 parameters: vec![],
2631 })
2632 );
2633 }
2634
2635 #[test]
2636 fn test_mail_from_esmtp_params() {
2637 k9::assert_equal!(
2638 unwrapper(Command::parse("MAIL FROM:<user@host> foo bar=baz")),
2639 MaybePartialCommand::Full(Command::MailFrom {
2640 address: mail_path("user", Domain::DomainName("host".parse().unwrap())),
2641 parameters: vec![
2642 EsmtpParameter {
2643 name: "foo".into(),
2644 value: None,
2645 },
2646 EsmtpParameter {
2647 name: "bar".into(),
2648 value: Some("baz".into()),
2649 },
2650 ],
2651 })
2652 );
2653 }
2654
2655 #[test]
2656 fn test_mail_from_bare_address() {
2657 k9::assert_equal!(
2659 unwrapper(Command::parse("MAIL FROM:user@host")),
2660 MaybePartialCommand::Full(Command::MailFrom {
2661 address: mail_path("user", Domain::DomainName("host".parse().unwrap())),
2662 parameters: vec![],
2663 })
2664 );
2665 }
2666
2667 #[test]
2668 fn test_mail_from_space_before_path() {
2669 for cmd in [
2673 "MAIL FROM: <user@host>",
2674 "MAIL FROM:\t<user@host>",
2675 "MAIL FROM: <user@host>",
2676 ] {
2677 k9::assert_equal!(
2678 unwrapper(Command::parse(cmd)),
2679 MaybePartialCommand::Full(Command::MailFrom {
2680 address: mail_path("user", Domain::DomainName("host".parse().unwrap())),
2681 parameters: vec![],
2682 })
2683 );
2684 }
2685
2686 k9::assert_equal!(
2688 unwrapper(Command::parse("MAIL FROM: <>")),
2689 MaybePartialCommand::Full(Command::MailFrom {
2690 address: ReversePath::NullSender,
2691 parameters: vec![],
2692 })
2693 );
2694
2695 k9::assert_equal!(
2697 unwrapper(Command::parse("MAIL FROM: <user@host> BODY=8BITMIME")),
2698 MaybePartialCommand::Full(Command::MailFrom {
2699 address: mail_path("user", Domain::DomainName("host".parse().unwrap())),
2700 parameters: vec![EsmtpParameter {
2701 name: "BODY".into(),
2702 value: Some("8BITMIME".into()),
2703 }],
2704 })
2705 );
2706 }
2707
2708 #[test]
2709 fn test_mail_from_at_domain_list() {
2710 k9::assert_equal!(
2711 unwrapper(Command::parse(
2712 "MAIL FROM:<@hosta.int,@jkl.org:userc@d.bar.org>"
2713 )),
2714 MaybePartialCommand::Full(Command::MailFrom {
2715 address: ReversePath::Path(MailPath {
2716 at_domain_list: vec!["hosta.int".into(), "jkl.org".into()],
2717 mailbox: Mailbox {
2718 local_part: "userc".into(),
2719 domain: Domain::DomainName("d.bar.org".parse().unwrap()),
2720 },
2721 }),
2722 parameters: vec![],
2723 })
2724 );
2725 }
2726
2727 #[test]
2728 fn test_mail_from_invalid_ipv4_is_partial() {
2729 k9::assert_equal!(
2731 unwrapper(Command::parse("MAIL FROM:<user@[999.999.999.999]>")),
2732 MaybePartialCommand::Partial {
2733 verb: CommandVerb::Mail,
2734 remainder: "<user@[999.999.999.999]>".into(),
2735 reason: PartialReason::InvalidSenderAddress,
2736 }
2737 );
2738 }
2739
2740 #[test]
2741 fn test_mail_from_invalid_ipv6_is_partial() {
2742 k9::assert_equal!(
2744 unwrapper(Command::parse("MAIL FROM:<user@[IPv6:not-an-ipv6]>")),
2745 MaybePartialCommand::Partial {
2746 verb: CommandVerb::Mail,
2747 remainder: "<user@[IPv6:not-an-ipv6]>".into(),
2748 reason: PartialReason::InvalidSenderAddress,
2749 }
2750 );
2751 }
2752
2753 #[test]
2754 fn test_mail_alone_is_partial() {
2755 k9::assert_equal!(
2756 unwrapper(Command::parse("MAIL")),
2757 MaybePartialCommand::Partial {
2758 verb: CommandVerb::Mail,
2759 remainder: "".into(),
2760 reason: PartialReason::Syntax,
2761 }
2762 );
2763 }
2764
2765 #[test]
2766 fn test_mail_with_garbage_is_partial() {
2767 k9::assert_equal!(
2768 unwrapper(Command::parse("MAIL garbage")),
2769 MaybePartialCommand::Partial {
2770 verb: CommandVerb::Mail,
2771 remainder: "garbage".into(),
2772 reason: PartialReason::Syntax,
2773 }
2774 );
2775 }
2776
2777 fn rcpt_path(local: &str, domain: Domain) -> ForwardPath {
2782 Mailbox {
2783 local_part: local.into(),
2784 domain,
2785 }
2786 .into()
2787 }
2788
2789 #[test]
2790 fn test_rcpt_to_domain_name() {
2791 k9::assert_equal!(
2792 unwrapper(Command::parse("RCPT TO:<user@host>")),
2793 MaybePartialCommand::Full(Command::RcptTo {
2794 address: rcpt_path("user", Domain::DomainName("host".parse().unwrap())),
2795 parameters: vec![],
2796 })
2797 );
2798 }
2799
2800 #[test]
2801 fn test_rcpt_to_case_insensitive() {
2802 k9::assert_equal!(
2804 unwrapper(Command::parse("rcpt to:<user@host>")),
2805 MaybePartialCommand::Full(Command::RcptTo {
2806 address: rcpt_path("user", Domain::DomainName("host".parse().unwrap())),
2807 parameters: vec![],
2808 })
2809 );
2810 }
2811
2812 #[test]
2813 fn test_rcpt_to_postmaster() {
2814 k9::assert_equal!(
2815 unwrapper(Command::parse("RCPT TO:<Postmaster>")),
2816 MaybePartialCommand::Full(Command::RcptTo {
2817 address: ForwardPath::Postmaster,
2818 parameters: vec![],
2819 })
2820 );
2821 }
2822
2823 #[test]
2824 fn test_rcpt_to_postmaster_lowercase() {
2825 k9::assert_equal!(
2827 unwrapper(Command::parse("RCPT TO:<postmaster>")),
2828 MaybePartialCommand::Full(Command::RcptTo {
2829 address: ForwardPath::Postmaster,
2830 parameters: vec![],
2831 })
2832 );
2833 }
2834
2835 #[test]
2836 fn test_rcpt_to_ipv4() {
2837 k9::assert_equal!(
2838 unwrapper(Command::parse("RCPT TO:<user@[10.0.0.1]>")),
2839 MaybePartialCommand::Full(Command::RcptTo {
2840 address: rcpt_path("user", Domain::V4("10.0.0.1".parse().unwrap())),
2841 parameters: vec![],
2842 })
2843 );
2844 }
2845
2846 #[test]
2847 fn test_rcpt_to_ipv6() {
2848 k9::assert_equal!(
2849 unwrapper(Command::parse("RCPT TO:<user@[IPv6:::1]>")),
2850 MaybePartialCommand::Full(Command::RcptTo {
2851 address: rcpt_path("user", Domain::V6("::1".parse().unwrap())),
2852 parameters: vec![],
2853 })
2854 );
2855 }
2856
2857 #[test]
2858 fn test_rcpt_to_tagged_literal() {
2859 k9::assert_equal!(
2860 unwrapper(Command::parse("RCPT TO:<user@[future:something]>")),
2861 MaybePartialCommand::Full(Command::RcptTo {
2862 address: rcpt_path("user", Domain::Tagged("future:something".into())),
2863 parameters: vec![],
2864 })
2865 );
2866 }
2867
2868 #[test]
2869 fn test_rcpt_to_esmtp_params() {
2870 k9::assert_equal!(
2871 unwrapper(Command::parse("RCPT TO:<user@host> foo bar=baz")),
2872 MaybePartialCommand::Full(Command::RcptTo {
2873 address: rcpt_path("user", Domain::DomainName("host".parse().unwrap())),
2874 parameters: vec![
2875 EsmtpParameter {
2876 name: "foo".into(),
2877 value: None,
2878 },
2879 EsmtpParameter {
2880 name: "bar".into(),
2881 value: Some("baz".into()),
2882 },
2883 ],
2884 })
2885 );
2886 }
2887
2888 #[test]
2889 fn test_rcpt_to_bare_address() {
2890 k9::assert_equal!(
2892 unwrapper(Command::parse("RCPT TO:user@host")),
2893 MaybePartialCommand::Full(Command::RcptTo {
2894 address: rcpt_path("user", Domain::DomainName("host".parse().unwrap())),
2895 parameters: vec![],
2896 })
2897 );
2898 }
2899
2900 #[test]
2901 fn test_rcpt_to_space_before_path() {
2902 for cmd in [
2905 "RCPT TO: <user@host>",
2906 "RCPT TO:\t<user@host>",
2907 "RCPT TO: <user@host>",
2908 ] {
2909 k9::assert_equal!(
2910 unwrapper(Command::parse(cmd)),
2911 MaybePartialCommand::Full(Command::RcptTo {
2912 address: rcpt_path("user", Domain::DomainName("host".parse().unwrap())),
2913 parameters: vec![],
2914 })
2915 );
2916 }
2917
2918 k9::assert_equal!(
2920 unwrapper(Command::parse("RCPT TO: <Postmaster>")),
2921 MaybePartialCommand::Full(Command::RcptTo {
2922 address: ForwardPath::Postmaster,
2923 parameters: vec![],
2924 })
2925 );
2926
2927 k9::assert_equal!(
2929 unwrapper(Command::parse("RCPT TO: <user@host> NOTIFY=SUCCESS")),
2930 MaybePartialCommand::Full(Command::RcptTo {
2931 address: rcpt_path("user", Domain::DomainName("host".parse().unwrap())),
2932 parameters: vec![EsmtpParameter {
2933 name: "NOTIFY".into(),
2934 value: Some("SUCCESS".into()),
2935 }],
2936 })
2937 );
2938 }
2939
2940 #[test]
2941 fn test_rcpt_to_at_domain_list() {
2942 k9::assert_equal!(
2943 unwrapper(Command::parse(
2944 "RCPT TO:<@hosta.int,@jkl.org:userc@d.bar.org>"
2945 )),
2946 MaybePartialCommand::Full(Command::RcptTo {
2947 address: ForwardPath::Path(MailPath {
2948 at_domain_list: vec!["hosta.int".into(), "jkl.org".into()],
2949 mailbox: Mailbox {
2950 local_part: "userc".into(),
2951 domain: Domain::DomainName("d.bar.org".parse().unwrap()),
2952 },
2953 }),
2954 parameters: vec![],
2955 })
2956 );
2957 }
2958
2959 #[test]
2960 fn test_rcpt_to_invalid_ipv4_is_partial() {
2961 k9::assert_equal!(
2963 unwrapper(Command::parse("RCPT TO:<user@[999.999.999.999]>")),
2964 MaybePartialCommand::Partial {
2965 verb: CommandVerb::Rcpt,
2966 remainder: "<user@[999.999.999.999]>".into(),
2967 reason: PartialReason::InvalidRecipientAddress,
2968 }
2969 );
2970 }
2971
2972 #[test]
2973 fn test_rcpt_to_invalid_ipv6_is_partial() {
2974 k9::assert_equal!(
2976 unwrapper(Command::parse("RCPT TO:<user@[IPv6:not-an-ipv6]>")),
2977 MaybePartialCommand::Partial {
2978 verb: CommandVerb::Rcpt,
2979 remainder: "<user@[IPv6:not-an-ipv6]>".into(),
2980 reason: PartialReason::InvalidRecipientAddress,
2981 }
2982 );
2983 }
2984
2985 #[test]
2986 fn test_rcpt_alone_is_partial() {
2987 k9::assert_equal!(
2988 unwrapper(Command::parse("RCPT")),
2989 MaybePartialCommand::Partial {
2990 verb: CommandVerb::Rcpt,
2991 remainder: "".into(),
2992 reason: PartialReason::Syntax,
2993 }
2994 );
2995 }
2996
2997 #[test]
2998 fn test_rcpt_with_garbage_is_partial() {
2999 k9::assert_equal!(
3000 unwrapper(Command::parse("RCPT garbage")),
3001 MaybePartialCommand::Partial {
3002 verb: CommandVerb::Rcpt,
3003 remainder: "garbage".into(),
3004 reason: PartialReason::Syntax,
3005 }
3006 );
3007 }
3008
3009 #[test]
3010 fn test_rcpt_to_invalid_address_syntax() {
3011 k9::assert_equal!(
3013 unwrapper(Command::parse("RCPT TO:<not an address>")),
3014 MaybePartialCommand::Partial {
3015 verb: CommandVerb::Rcpt,
3016 remainder: "<not an address>".into(),
3017 reason: PartialReason::InvalidRecipientAddress,
3018 }
3019 );
3020 }
3021
3022 #[test]
3023 fn test_rcpt_to_valid_address_bad_params() {
3024 k9::assert_equal!(
3026 unwrapper(Command::parse("RCPT TO:<valid@example.com> !!!garbage")),
3027 MaybePartialCommand::Partial {
3028 verb: CommandVerb::Rcpt,
3029 remainder: "!!!garbage".into(),
3030 reason: PartialReason::Syntax,
3031 }
3032 );
3033 }
3034
3035 #[test]
3036 fn test_rcpt_to_bad_address_with_params_is_invalid_address() {
3037 k9::assert_equal!(
3039 unwrapper(Command::parse("RCPT TO:<bad address> NOTIFY=SUCCESS")),
3040 MaybePartialCommand::Partial {
3041 verb: CommandVerb::Rcpt,
3042 remainder: "<bad address> NOTIFY=SUCCESS".into(),
3043 reason: PartialReason::InvalidRecipientAddress,
3044 }
3045 );
3046 }
3047
3048 #[test]
3049 fn test_mail_from_invalid_address_syntax() {
3050 k9::assert_equal!(
3052 unwrapper(Command::parse("MAIL FROM:<not an address>")),
3053 MaybePartialCommand::Partial {
3054 verb: CommandVerb::Mail,
3055 remainder: "<not an address>".into(),
3056 reason: PartialReason::InvalidSenderAddress,
3057 }
3058 );
3059 }
3060
3061 #[test]
3062 fn test_mail_from_valid_address_bad_params() {
3063 k9::assert_equal!(
3065 unwrapper(Command::parse("MAIL FROM:<valid@example.com> !!!garbage")),
3066 MaybePartialCommand::Partial {
3067 verb: CommandVerb::Mail,
3068 remainder: "!!!garbage".into(),
3069 reason: PartialReason::Syntax,
3070 }
3071 );
3072 }
3073
3074 #[test]
3075 fn test_mail_from_null_sender_bad_params() {
3076 k9::assert_equal!(
3078 unwrapper(Command::parse("MAIL FROM:<> !!!garbage")),
3079 MaybePartialCommand::Partial {
3080 verb: CommandVerb::Mail,
3081 remainder: "!!!garbage".into(),
3082 reason: PartialReason::Syntax,
3083 }
3084 );
3085 }
3086
3087 #[test]
3088 fn test_mail_from_bad_address_with_params_is_invalid_address() {
3089 k9::assert_equal!(
3091 unwrapper(Command::parse("MAIL FROM:<bad address> SIZE=1000")),
3092 MaybePartialCommand::Partial {
3093 verb: CommandVerb::Mail,
3094 remainder: "<bad address> SIZE=1000".into(),
3095 reason: PartialReason::InvalidSenderAddress,
3096 }
3097 );
3098 }
3099
3100 fn assert_encode(input: &str, expected: &str) {
3108 let cmd = match unwrapper(Command::parse(input)) {
3109 MaybePartialCommand::Full(c) => c,
3110 other => panic!("expected Full, got {other:?}"),
3111 };
3112 let encoded = cmd.encode();
3113 k9::assert_equal!(encoded, BString::from(expected));
3114 k9::assert_equal!(
3116 unwrapper(Command::parse(encoded.clone())),
3117 MaybePartialCommand::Full(cmd)
3118 );
3119 }
3120
3121 #[test]
3122 fn test_encode_ehlo_domain() {
3123 assert_encode("EHLO example.com", "EHLO example.com\r\n");
3124 }
3125
3126 #[test]
3127 fn test_encode_ehlo_ipv4() {
3128 assert_encode("EHLO [10.0.0.1]", "EHLO [10.0.0.1]\r\n");
3129 }
3130
3131 #[test]
3132 fn test_encode_ehlo_ipv6() {
3133 assert_encode("EHLO [IPv6:::1]", "EHLO [IPv6:::1]\r\n");
3134 }
3135
3136 #[test]
3137 fn test_encode_ehlo_tagged() {
3138 assert_encode("EHLO [future:something]", "EHLO [future:something]\r\n");
3139 }
3140
3141 #[test]
3142 fn test_encode_helo() {
3143 assert_encode("HELO mail.example.com", "HELO mail.example.com\r\n");
3144 }
3145
3146 #[test]
3147 fn test_encode_lhlo() {
3148 assert_encode("LHLO mail.example.com", "LHLO mail.example.com\r\n");
3149 }
3150
3151 #[test]
3152 fn test_encode_noop_none() {
3153 assert_encode("NOOP", "NOOP\r\n");
3154 }
3155
3156 #[test]
3157 fn test_encode_noop_some() {
3158 assert_encode("NOOP something", "NOOP something\r\n");
3159 }
3160
3161 #[test]
3162 fn test_encode_help_none() {
3163 assert_encode("HELP", "HELP\r\n");
3164 }
3165
3166 #[test]
3167 fn test_encode_help_some() {
3168 assert_encode("HELP MAIL", "HELP MAIL\r\n");
3169 }
3170
3171 #[test]
3172 fn test_encode_vrfy_none() {
3173 assert_encode("VRFY", "VRFY\r\n");
3174 }
3175
3176 #[test]
3177 fn test_encode_vrfy_some() {
3178 assert_encode("VRFY user", "VRFY user\r\n");
3179 }
3180
3181 #[test]
3182 fn test_encode_expn_none() {
3183 assert_encode("EXPN", "EXPN\r\n");
3184 }
3185
3186 #[test]
3187 fn test_encode_expn_some() {
3188 assert_encode("EXPN list", "EXPN list\r\n");
3189 }
3190
3191 #[test]
3192 fn test_encode_data() {
3193 assert_encode("DATA", "DATA\r\n");
3194 }
3195
3196 #[test]
3197 fn test_encode_data_dot() {
3198 k9::assert_equal!(Command::DataDot.encode(), BString::from(".\r\n"));
3201 }
3202
3203 #[test]
3204 fn test_encode_rset() {
3205 assert_encode("RSET", "RSET\r\n");
3206 }
3207
3208 #[test]
3209 fn test_encode_quit() {
3210 assert_encode("QUIT", "QUIT\r\n");
3211 }
3212
3213 #[test]
3214 fn test_encode_starttls() {
3215 assert_encode("STARTTLS", "STARTTLS\r\n");
3216 }
3217
3218 #[test]
3219 fn test_encode_mail_from_path() {
3220 assert_encode(
3221 "MAIL FROM:<user@example.com>",
3222 "MAIL FROM:<user@example.com>\r\n",
3223 );
3224 }
3225
3226 #[test]
3227 fn test_encode_mail_from_null_sender() {
3228 assert_encode("MAIL FROM:<>", "MAIL FROM:<>\r\n");
3229 }
3230
3231 #[test]
3232 fn test_encode_mail_from_params() {
3233 assert_encode(
3234 "MAIL FROM:<user@example.com> SIZE=1000 BODY=8BITMIME",
3235 "MAIL FROM:<user@example.com> SIZE=1000 BODY=8BITMIME\r\n",
3236 );
3237 }
3238
3239 #[test]
3240 fn test_encode_mail_from_ipv4() {
3241 assert_encode(
3242 "MAIL FROM:<user@[10.0.0.1]>",
3243 "MAIL FROM:<user@[10.0.0.1]>\r\n",
3244 );
3245 }
3246
3247 #[test]
3248 fn test_encode_mail_from_ipv6() {
3249 assert_encode(
3250 "MAIL FROM:<user@[IPv6:::1]>",
3251 "MAIL FROM:<user@[IPv6:::1]>\r\n",
3252 );
3253 }
3254
3255 #[test]
3256 fn test_encode_rcpt_to_path() {
3257 assert_encode(
3258 "RCPT TO:<user@example.com>",
3259 "RCPT TO:<user@example.com>\r\n",
3260 );
3261 }
3262
3263 #[test]
3264 fn test_encode_rcpt_to_postmaster() {
3265 let cmd = Command::RcptTo {
3267 address: ForwardPath::Postmaster,
3268 parameters: vec![],
3269 };
3270 k9::assert_equal!(cmd.encode(), BString::from("RCPT TO:<Postmaster>\r\n"));
3271 k9::assert_equal!(
3272 unwrapper(Command::parse(cmd.encode())),
3273 MaybePartialCommand::Full(cmd)
3274 );
3275 }
3276
3277 #[test]
3278 fn test_encode_rcpt_to_params() {
3279 assert_encode(
3280 "RCPT TO:<user@example.com> NOTIFY=SUCCESS",
3281 "RCPT TO:<user@example.com> NOTIFY=SUCCESS\r\n",
3282 );
3283 }
3284
3285 #[test]
3286 fn test_encode_auth_no_response() {
3287 assert_encode("AUTH PLAIN", "AUTH PLAIN\r\n");
3288 }
3289
3290 #[test]
3291 fn test_encode_auth_with_response() {
3292 assert_encode("AUTH PLAIN dXNlcjpwYXNz", "AUTH PLAIN dXNlcjpwYXNz\r\n");
3293 }
3294
3295 #[test]
3296 fn test_encode_xclient_single() {
3297 assert_encode(
3298 "XCLIENT NAME=foo.example.com",
3299 "XCLIENT NAME=foo.example.com\r\n",
3300 );
3301 }
3302
3303 #[test]
3304 fn test_encode_xclient_multiple() {
3305 assert_encode(
3306 "XCLIENT NAME=foo.example.com ADDR=10.0.0.1",
3307 "XCLIENT NAME=foo.example.com ADDR=10.0.0.1\r\n",
3308 );
3309 }
3310
3311 #[test]
3312 fn test_encode_xclient_xtext_roundtrip() {
3313 assert_encode(
3317 "XCLIENT NAME=user+40example.com",
3318 "XCLIENT NAME=user@example.com\r\n",
3319 );
3320 }
3321
3322 #[test]
3323 fn test_encode_unknown() {
3324 assert_encode("FOOBAR some args", "FOOBAR some args\r\n");
3325 }
3326
3327 #[test]
3328 fn test_encode_unknown_bare_verb() {
3329 assert_encode("FOOBAR", "FOOBAR\r\n");
3330 }
3331
3332 #[test]
3333 fn test_encode_mail_from_source_route_dropped() {
3334 let parsed = unwrapper(Command::parse("MAIL FROM:<@route.example.com:user@host>"));
3338 let cmd = match parsed {
3339 MaybePartialCommand::Full(c) => c,
3340 other => panic!("expected Full, got {other:?}"),
3341 };
3342 let encoded = cmd.encode();
3343 k9::assert_equal!(encoded, BString::from("MAIL FROM:<user@host>\r\n"));
3344 let expected = Mailbox {
3346 local_part: "user".into(),
3347 domain: Domain::DomainName("host".parse().unwrap()),
3348 };
3349 k9::assert_equal!(
3350 unwrapper(Command::parse(encoded)),
3351 MaybePartialCommand::Full(Command::MailFrom {
3352 address: expected.into(),
3353 parameters: vec![],
3354 })
3355 );
3356 }
3357
3358 fn parse_full(input: &str) -> Command {
3364 match unwrapper(Command::parse(input)) {
3365 MaybePartialCommand::Full(c) => c,
3366 other => panic!("expected Full, got {other:?}"),
3367 }
3368 }
3369
3370 #[test]
3373 fn test_mail_from_utf8_local_part() {
3374 let cmd = parse_full("MAIL FROM:<ü@example.com>");
3376 let mailbox = match cmd {
3377 Command::MailFrom {
3378 address: ReversePath::Path(path),
3379 ..
3380 } => path.mailbox,
3381 other => panic!("unexpected {other:?}"),
3382 };
3383 k9::assert_equal!(mailbox.local_part, String::from("ü"));
3384 k9::assert_equal!(
3385 mailbox.domain,
3386 Domain::DomainName("example.com".parse().unwrap())
3387 );
3388 }
3389
3390 #[test]
3391 fn test_mail_from_utf8_local_part_roundtrip() {
3392 let input = "MAIL FROM:<ü@example.com>";
3395 let cmd = parse_full(input);
3396 k9::assert_equal!(
3397 unwrapper(Command::parse(cmd.encode())),
3398 MaybePartialCommand::Full(cmd)
3399 );
3400 }
3401
3402 #[test]
3403 fn test_mail_from_quoted_utf8_local_part() {
3404 let cmd = parse_full("MAIL FROM:<\"ü\"@example.com>");
3406 let mailbox = match cmd {
3407 Command::MailFrom {
3408 address: ReversePath::Path(path),
3409 ..
3410 } => path.mailbox,
3411 other => panic!("unexpected {other:?}"),
3412 };
3413 k9::assert_equal!(mailbox.local_part, String::from("\"ü\""));
3415 }
3416
3417 #[test]
3418 fn test_rcpt_to_utf8_local_part() {
3419 let cmd = parse_full("RCPT TO:<用户@example.com>");
3421 let mailbox = match cmd {
3422 Command::RcptTo {
3423 address: ForwardPath::Path(path),
3424 ..
3425 } => path.mailbox,
3426 other => panic!("unexpected {other:?}"),
3427 };
3428 k9::assert_equal!(mailbox.local_part, String::from("用户"));
3429 }
3430
3431 #[test]
3434 fn test_mail_from_u_label_domain() {
3435 let cmd = parse_full("MAIL FROM:<user@münchen.de>");
3437 let domain = match cmd {
3438 Command::MailFrom {
3439 address: ReversePath::Path(path),
3440 ..
3441 } => path.mailbox.domain,
3442 other => panic!("unexpected {other:?}"),
3443 };
3444 match domain {
3445 Domain::DomainName(ref s) => {
3446 k9::assert_equal!(s.as_str(), "xn--mnchen-3ya.de");
3447 }
3448 other => panic!("unexpected domain variant {other:?}"),
3449 }
3450 }
3451
3452 #[test]
3453 fn test_mail_from_u_label_domain_encode() {
3454 let cmd = parse_full("MAIL FROM:<user@münchen.de>");
3457 k9::assert_equal!(
3458 cmd.encode(),
3459 BString::from("MAIL FROM:<user@xn--mnchen-3ya.de>\r\n")
3460 );
3461 }
3462
3463 #[test]
3464 fn test_ehlo_u_label_domain() {
3465 let cmd = parse_full("EHLO münchen.de");
3467 match cmd {
3468 Command::Ehlo(Domain::DomainName(ref s)) => {
3469 k9::assert_equal!(s.as_str(), "xn--mnchen-3ya.de");
3470 }
3471 other => panic!("unexpected {other:?}"),
3472 }
3473 }
3474
3475 #[test]
3476 fn test_ehlo_u_label_domain_encode() {
3477 let cmd = parse_full("EHLO münchen.de");
3479 k9::assert_equal!(cmd.encode(), BString::from("EHLO xn--mnchen-3ya.de\r\n"));
3480 }
3481
3482 #[test]
3485 fn test_esmtp_value_utf8() {
3486 let cmd = parse_full("MAIL FROM:<u@h> PARAM=valüe");
3488 let params = match cmd {
3489 Command::MailFrom { parameters, .. } => parameters,
3490 other => panic!("unexpected {other:?}"),
3491 };
3492 k9::assert_equal!(params.len(), 1);
3493 k9::assert_equal!(params[0].name, "PARAM");
3494 k9::assert_equal!(params[0].value, Some("valüe".to_string()));
3495 }
3496
3497 #[test]
3498 fn test_esmtp_value_utf8_roundtrip() {
3499 let input = "MAIL FROM:<u@h> PARAM=valüe";
3501 let cmd = parse_full(input);
3502 k9::assert_equal!(
3503 cmd.encode(),
3504 BString::from("MAIL FROM:<u@h> PARAM=valüe\r\n")
3505 );
3506 k9::assert_equal!(
3507 unwrapper(Command::parse(cmd.encode())),
3508 MaybePartialCommand::Full(parse_full(input))
3509 );
3510 }
3511
3512 #[test]
3513 fn test_esmtp_value_utf8_only() {
3514 let cmd = parse_full("MAIL FROM:<u@h> X=ünïcödé");
3516 let params = match cmd {
3517 Command::MailFrom { parameters, .. } => parameters,
3518 other => panic!("unexpected {other:?}"),
3519 };
3520 k9::assert_equal!(params[0].name, "X");
3521 k9::assert_equal!(params[0].value, Some("ünïcödé".to_string()));
3522 }
3523
3524 #[test]
3527 fn test_reverse_path_null_sender_try_into_mailpath_err() {
3528 use core::convert::TryInto;
3529 let null_sender = ReversePath::NullSender;
3530 let err: &'static str =
3531 <ReversePath as TryInto<MailPath>>::try_into(null_sender).unwrap_err();
3532 k9::assert_equal!(err, "Cannot convert NullSender to MailPath");
3533 }
3534
3535 #[test]
3536 fn test_forward_path_postmaster_try_into_mailpath_err() {
3537 use core::convert::TryInto;
3538 let postmaster = ForwardPath::Postmaster;
3539 let err: &'static str =
3540 <ForwardPath as TryInto<MailPath>>::try_into(postmaster).unwrap_err();
3541 k9::assert_equal!(err, "Cannot convert Postmaster to MailPath");
3542 }
3543
3544 #[test]
3545 fn test_reverse_path_try_from_null_sender_err() {
3546 let null_sender = ReversePath::NullSender;
3547 let err = MailPath::try_from(null_sender).unwrap_err();
3548 k9::assert_equal!(err, "Cannot convert NullSender to MailPath");
3549 }
3550
3551 #[test]
3552 fn test_forward_path_try_from_postmaster_err() {
3553 let postmaster = ForwardPath::Postmaster;
3554 let err = MailPath::try_from(postmaster).unwrap_err();
3555 k9::assert_equal!(err, "Cannot convert Postmaster to MailPath");
3556 }
3557
3558 #[test]
3561 fn test_envelope_address_try_from_mailbox() {
3562 let mailbox = Mailbox {
3563 local_part: String::from("user"),
3564 domain: Domain::DomainName("example.com".parse().unwrap()),
3565 };
3566 let addr = EnvelopeAddress::from(mailbox);
3567 match addr {
3568 EnvelopeAddress::Path(path) => {
3569 k9::assert_equal!(path.mailbox.local_part(), "user");
3570 k9::assert_equal!(
3571 path.mailbox.domain,
3572 Domain::DomainName("example.com".parse().unwrap())
3573 );
3574 }
3575 _ => panic!("Expected Path variant"),
3576 }
3577 }
3578
3579 #[test]
3580 fn test_envelope_address_try_from_null_err() {
3581 let addr = EnvelopeAddress::Null;
3582 let err = Mailbox::try_from(addr).unwrap_err();
3583 k9::assert_equal!(err, "Cannot convert Null to Mailbox");
3584 }
3585
3586 #[test]
3587 fn test_envelope_address_try_from_postmaster_err() {
3588 let addr = EnvelopeAddress::Postmaster;
3589 let err = Mailbox::try_from(addr).unwrap_err();
3590 k9::assert_equal!(err, "Cannot convert Postmaster to Mailbox");
3591 }
3592
3593 #[test]
3594 fn test_envelope_address_try_from_null_to_reverse_path() {
3595 let addr = EnvelopeAddress::Null;
3596 let result = ReversePath::try_from(addr).unwrap();
3597 k9::assert_equal!(result, ReversePath::NullSender);
3598 }
3599
3600 #[test]
3601 fn test_envelope_address_try_from_postmaster_to_forward_path() {
3602 let addr = EnvelopeAddress::Postmaster;
3603 let result = ForwardPath::try_from(addr).unwrap();
3604 k9::assert_equal!(result, ForwardPath::Postmaster);
3605 }
3606
3607 #[test]
3610 fn test_envelope_address_try_into_mailpath_null_err() {
3611 let addr = EnvelopeAddress::Null;
3612 let err = MailPath::try_from(addr).unwrap_err();
3613 k9::assert_equal!(err, "Cannot convert Null to MailPath");
3614 }
3615
3616 #[test]
3617 fn test_envelope_address_try_into_mailpath_postmaster_err() {
3618 let addr = EnvelopeAddress::Postmaster;
3619 let err = MailPath::try_from(addr).unwrap_err();
3620 k9::assert_equal!(err, "Cannot convert Postmaster to MailPath");
3621 }
3622
3623 #[test]
3624 fn test_reverse_path_try_into_forward_path_null_sender_err() {
3625 let rp = ReversePath::NullSender;
3626 let err = ForwardPath::try_from(rp).unwrap_err();
3627 k9::assert_equal!(err, "Cannot convert NullSender to ForwardPath");
3628 }
3629
3630 #[test]
3631 fn test_forward_path_try_into_reverse_path_postmaster_err() {
3632 let fp = ForwardPath::Postmaster;
3633 let err = ReversePath::try_from(fp).unwrap_err();
3634 k9::assert_equal!(err, "Cannot convert Postmaster to ReversePath");
3635 }
3636
3637 #[test]
3640 fn test_mailbox_into_mailpath() {
3641 let mailbox = Mailbox {
3642 local_part: String::from("user"),
3643 domain: Domain::DomainName("example.com".parse().unwrap()),
3644 };
3645 let path: MailPath = mailbox.into();
3646 assert!(path.at_domain_list.is_empty());
3647 k9::assert_equal!(path.mailbox.local_part(), "user");
3648 }
3649
3650 #[test]
3651 fn test_mailbox_into_envelope_address() {
3652 let mailbox = Mailbox {
3653 local_part: String::from("user"),
3654 domain: Domain::DomainName("example.com".parse().unwrap()),
3655 };
3656 let addr: EnvelopeAddress = mailbox.into();
3657 match addr {
3658 EnvelopeAddress::Path(path) => {
3659 k9::assert_equal!(path.mailbox.local_part(), "user");
3660 }
3661 _ => panic!("Expected Path variant"),
3662 }
3663 }
3664
3665 #[test]
3666 fn test_mailbox_into_reverse_path() {
3667 let mailbox = Mailbox {
3668 local_part: String::from("user"),
3669 domain: Domain::DomainName("example.com".parse().unwrap()),
3670 };
3671 let path: ReversePath = mailbox.into();
3672 match path {
3673 ReversePath::Path(p) => {
3674 k9::assert_equal!(p.mailbox.local_part(), "user");
3675 }
3676 _ => panic!("Expected Path variant"),
3677 }
3678 }
3679
3680 #[test]
3681 fn test_mailbox_into_forward_path() {
3682 let mailbox = Mailbox {
3683 local_part: String::from("user"),
3684 domain: Domain::DomainName("example.com".parse().unwrap()),
3685 };
3686 let path: ForwardPath = mailbox.into();
3687 match path {
3688 ForwardPath::Path(p) => {
3689 k9::assert_equal!(p.mailbox.local_part(), "user");
3690 }
3691 _ => panic!("Expected Path variant"),
3692 }
3693 }
3694
3695 #[test]
3698 fn test_mailbox_roundtrip() {
3699 let original = Mailbox {
3700 local_part: String::from("user"),
3701 domain: Domain::DomainName("example.com".parse().unwrap()),
3702 };
3703 let path: MailPath = original.clone().into();
3704 let mailbox: Mailbox = path.mailbox.into();
3705 k9::assert_equal!(original.local_part(), mailbox.local_part());
3706 k9::assert_equal!(original.domain, mailbox.domain);
3707 }
3708
3709 #[test]
3710 fn test_reverse_path_to_forward_path() {
3711 let path = MailPath {
3712 at_domain_list: vec!["route.com".into()],
3713 mailbox: Mailbox {
3714 local_part: String::from("user"),
3715 domain: Domain::DomainName("example.com".parse().unwrap()),
3716 },
3717 };
3718 let rp = ReversePath::Path(path.clone());
3719 let fp: ForwardPath = rp.try_into().unwrap();
3720 match fp {
3721 ForwardPath::Path(p) => {
3722 k9::assert_equal!(p.mailbox.local_part(), "user");
3723 }
3724 _ => panic!("Expected Path variant"),
3725 }
3726 }
3727
3728 #[test]
3731 fn test_mailpath_debug_simple() {
3732 let mailbox = Mailbox {
3733 local_part: "someone".into(),
3734 domain: Domain::DomainName("example.com".parse().unwrap()),
3735 };
3736 let path: MailPath = mailbox.into();
3737 let debug_str = format!("{:?}", path);
3738 k9::assert_equal!(debug_str, r#"MailPath("someone@example.com")"#);
3739 }
3740
3741 #[test]
3742 fn test_mailpath_debug_with_at_domain_list() {
3743 let path = MailPath {
3744 at_domain_list: vec!["route.example.com".into()],
3745 mailbox: Mailbox {
3746 local_part: "user".into(),
3747 domain: Domain::DomainName("host".parse().unwrap()),
3748 },
3749 };
3750 let debug_str = format!("{:?}", path);
3751 k9::assert_equal!(debug_str, r#"MailPath("@route.example.com:user@host")"#);
3752 }
3753
3754 #[test]
3755 fn test_mailpath_debug_multiple_at_domains() {
3756 let path = MailPath {
3757 at_domain_list: vec!["hosta.int".into(), "jkl.org".into()],
3758 mailbox: Mailbox {
3759 local_part: "userc".into(),
3760 domain: Domain::DomainName("d.bar.org".parse().unwrap()),
3761 },
3762 };
3763 let debug_str = format!("{:?}", path);
3764 k9::assert_equal!(
3765 debug_str,
3766 r#"MailPath("@hosta.int,@jkl.org:userc@d.bar.org")"#
3767 );
3768 }
3769
3770 #[test]
3771 fn test_mailpath_debug_non_ascii_utf8() {
3772 let mailbox = Mailbox {
3775 local_part: "üser".into(),
3776 domain: Domain::DomainName("example.com".parse().unwrap()),
3777 };
3778 let path: MailPath = mailbox.into();
3779 let debug_str = format!("{:?}", path);
3780 k9::assert_equal!(debug_str, r#"MailPath("üser@example.com")"#);
3782 }
3783
3784 #[test]
3785 fn test_mailpath_debug_with_ipv4() {
3786 let mailbox = Mailbox {
3787 local_part: "user".into(),
3788 domain: Domain::V4("10.0.0.1".parse().unwrap()),
3789 };
3790 let path: MailPath = mailbox.into();
3791 let debug_str = format!("{:?}", path);
3792 k9::assert_equal!(debug_str, r#"MailPath("user@[10.0.0.1]")"#);
3794 }
3795
3796 #[test]
3797 fn test_mailpath_debug_with_ipv6() {
3798 let mailbox = Mailbox {
3799 local_part: "user".into(),
3800 domain: Domain::V6("::1".parse().unwrap()),
3801 };
3802 let path: MailPath = mailbox.into();
3803 let debug_str = format!("{:?}", path);
3804 k9::assert_equal!(debug_str, r#"MailPath("user@[IPv6:::1]")"#);
3806 }
3807
3808 #[test]
3809 fn test_mailpath_debug_with_tagged_literal() {
3810 let mailbox = Mailbox {
3811 local_part: "user".into(),
3812 domain: Domain::Tagged("future:something".into()),
3813 };
3814 let path: MailPath = mailbox.into();
3815 let debug_str = format!("{:?}", path);
3816 k9::assert_equal!(debug_str, r#"MailPath("user@[future:something]")"#);
3818 }
3819 #[test]
3820 fn test_envelope_address_debug_null() {
3821 let addr = EnvelopeAddress::Null;
3822 let debug_str = format!("{:?}", addr);
3823 k9::assert_equal!(debug_str, "<>");
3824 }
3825
3826 #[test]
3827 fn test_envelope_address_debug_postmaster() {
3828 let addr = EnvelopeAddress::Postmaster;
3829 let debug_str = format!("{:?}", addr);
3830 k9::assert_equal!(debug_str, "<Postmaster>");
3831 }
3832
3833 #[test]
3834 fn test_envelope_address_postmaster_full_address() {
3835 let postmaster = EnvelopeAddress::parse(r#"postmaster@example.com"#).unwrap();
3836 k9::assert_equal!(postmaster.to_string(), r#"postmaster@example.com"#);
3837 }
3838
3839 #[test]
3840 fn test_envelope_address_debug_path_with_non_ascii_utf8() {
3841 let path = MailPath {
3844 at_domain_list: vec!["example.com".into()],
3845 mailbox: Mailbox {
3846 local_part: String::from("üser"),
3847 domain: Domain::DomainName("example.com".parse().unwrap()),
3848 },
3849 };
3850 let addr = EnvelopeAddress::Path(path);
3851 let debug_str = format!("{:?}", addr);
3852 k9::assert_equal!(debug_str, r#"<üser@example.com>"#);
3856 }
3857
3858 #[test]
3859 fn test_envelope_address_display_drops_source_route() {
3860 let path = MailPath {
3861 at_domain_list: vec!["hosta.int".into(), "jkl.org".into()],
3862 mailbox: Mailbox {
3863 local_part: String::from("userc"),
3864 domain: Domain::DomainName("d.bar.org".parse().unwrap()),
3865 },
3866 };
3867 let addr = EnvelopeAddress::Path(path);
3868 k9::assert_equal!(addr.to_string(), "userc@d.bar.org");
3870 if let EnvelopeAddress::Path(ref p) = addr {
3872 k9::assert_equal!(p.to_string(), "@hosta.int,@jkl.org:userc@d.bar.org");
3873 }
3874
3875 let path2 = MailPath {
3877 at_domain_list: vec!["relay.example".into()],
3878 mailbox: Mailbox {
3879 local_part: String::from(r#""info@""#),
3880 domain: Domain::DomainName("example.com".parse().unwrap()),
3881 },
3882 };
3883 let addr2 = EnvelopeAddress::Path(path2);
3884 k9::assert_equal!(addr2.to_string(), r#""info@"@example.com"#);
3885 if let EnvelopeAddress::Path(ref p) = addr2 {
3886 k9::assert_equal!(p.to_string(), r#"@relay.example:"info@"@example.com"#);
3887 }
3888 }
3889
3890 #[test]
3891 fn test_envelope_address_serde_roundtrip_with_source_route() {
3892 let cmd = unwrapper(Command::parse(
3894 "MAIL FROM:<@hosta.int,@jkl.org:userc@d.bar.org>",
3895 ));
3896 if let MaybePartialCommand::Full(Command::MailFrom { address, .. }) = cmd {
3897 if let ReversePath::Path(mail_path) = address {
3898 let addr = EnvelopeAddress::Path(mail_path);
3899 let serialized = serde_json::to_string(&addr).unwrap();
3901 k9::assert_equal!(serialized, r#""userc@d.bar.org""#);
3902 let deserialized: EnvelopeAddress = serde_json::from_str(&serialized).unwrap();
3904 k9::assert_equal!(deserialized.to_string(), "userc@d.bar.org");
3906 } else {
3907 panic!("Expected Path variant");
3908 }
3909 } else {
3910 panic!("Expected Full MailFrom");
3911 }
3912
3913 let cmd2 = unwrapper(Command::parse(
3915 r#"MAIL FROM:<@relay.example:"info@"@example.com>"#,
3916 ));
3917 if let MaybePartialCommand::Full(Command::MailFrom { address, .. }) = cmd2 {
3918 if let ReversePath::Path(mail_path) = address {
3919 let addr = EnvelopeAddress::Path(mail_path);
3920 let serialized = serde_json::to_string(&addr).unwrap();
3921 k9::assert_equal!(serialized, r#""\"info@\"@example.com""#);
3922 let deserialized: EnvelopeAddress = serde_json::from_str(&serialized).unwrap();
3923 k9::assert_equal!(deserialized.to_string(), r#""info@"@example.com"#);
3924 } else {
3925 panic!("Expected Path variant");
3926 }
3927 } else {
3928 panic!("Expected Full MailFrom");
3929 }
3930 }
3931
3932 #[test]
3933 fn test_envelope_address_rejects_obs_local_part() {
3934 EnvelopeAddress::parse(r#""first".last@example.com"#).unwrap_err();
3937 EnvelopeAddress::parse(r#"first."last"@example.com"#).unwrap_err();
3938 EnvelopeAddress::parse(r#""first"."last"@example.com"#).unwrap_err();
3939 }
3940
3941 #[test]
3942 fn test_envelope_address_null_sender_roundtrip() {
3943 let null = EnvelopeAddress::Null;
3947
3948 k9::assert_equal!(null.to_string(), "");
3950
3951 let parsed = EnvelopeAddress::parse("").unwrap();
3953 k9::assert_equal!(parsed, EnvelopeAddress::Null);
3954
3955 let json = serde_json::to_string(&null).unwrap();
3957 k9::assert_equal!(json, "\"\"");
3958 let deserialized: EnvelopeAddress = serde_json::from_str(&json).unwrap();
3959 k9::assert_equal!(deserialized, EnvelopeAddress::Null);
3960 }
3961}