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 let input = make_span(input.as_bytes());
468 let (_, result) = all_consuming(alt((
469 map(tag_no_case("<>"), |_| EnvelopeAddress::Null),
470 map(tag_no_case("<Postmaster>"), |_| EnvelopeAddress::Postmaster),
471 map(tag_no_case("Postmaster"), |_| EnvelopeAddress::Postmaster),
472 map(path, EnvelopeAddress::Path),
473 map(mailbox, EnvelopeAddress::from),
474 )))
475 .parse(input)
476 .map_err(|e| explain_nom(input, e))?;
477 Ok(result)
478 }
479
480 pub fn parse(input: &str) -> anyhow::Result<Self> {
485 EnvelopeAddress::parse_impl(input).map_err(|e| anyhow::anyhow!(e))
486 }
487
488 pub fn user(&self) -> String {
491 match self {
492 EnvelopeAddress::Postmaster => "postmaster".to_string(),
493 EnvelopeAddress::Null => "".to_string(),
494 EnvelopeAddress::Path(path) => path.mailbox.local_part().into(),
495 }
496 }
497
498 pub fn domain(&self) -> String {
501 match self {
502 EnvelopeAddress::Postmaster | EnvelopeAddress::Null => "".to_string(),
503 EnvelopeAddress::Path(path) => path.mailbox.domain.to_string(),
504 }
505 }
506
507 pub fn null_sender() -> Self {
509 EnvelopeAddress::Null
510 }
511}
512
513#[cfg(feature = "lua")]
514impl FromLua for EnvelopeAddress {
515 fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> {
516 match value {
517 mlua::Value::String(s) => s
518 .to_str()?
519 .parse::<EnvelopeAddress>()
520 .map_err(|e: String| mlua::Error::RuntimeError(e)),
521 _ => {
522 let ud = mlua::UserDataRef::<EnvelopeAddress>::from_lua(value, lua)?;
523 Ok(ud.clone())
524 }
525 }
526 }
527}
528
529#[cfg(feature = "lua")]
530impl UserData for EnvelopeAddress {
531 fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
532 fields.add_field_method_get("user", |_, this| Ok(this.user()));
533 fields.add_field_method_get("domain", |_, this| Ok(this.domain()));
534 fields.add_field_method_get("email", |_, this| Ok(this.to_string()));
535 }
536
537 fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
538 methods.add_meta_method(MetaMethod::ToString, |_, this, _: ()| Ok(this.to_string()));
539 }
540}
541
542impl From<MailPath> for ReversePath {
543 fn from(path: MailPath) -> Self {
544 ReversePath::Path(path)
545 }
546}
547
548impl From<MailPath> for ForwardPath {
549 fn from(path: MailPath) -> Self {
550 ForwardPath::Path(path)
551 }
552}
553
554impl TryFrom<ReversePath> for MailPath {
555 type Error = &'static str;
556
557 fn try_from(path: ReversePath) -> Result<Self, Self::Error> {
558 match path {
559 ReversePath::Path(mailpath) => Ok(mailpath),
560 ReversePath::NullSender => Err("Cannot convert NullSender to MailPath"),
561 }
562 }
563}
564
565impl TryFrom<ForwardPath> for MailPath {
566 type Error = &'static str;
567
568 fn try_from(path: ForwardPath) -> Result<Self, Self::Error> {
569 match path {
570 ForwardPath::Path(mailpath) => Ok(mailpath),
571 ForwardPath::Postmaster => Err("Cannot convert Postmaster to MailPath"),
572 }
573 }
574}
575
576impl From<Mailbox> for MailPath {
582 fn from(mailbox: Mailbox) -> Self {
583 MailPath {
584 at_domain_list: vec![],
585 mailbox,
586 }
587 }
588}
589
590impl From<Mailbox> for EnvelopeAddress {
592 fn from(mailbox: Mailbox) -> Self {
593 EnvelopeAddress::Path(MailPath {
594 at_domain_list: vec![],
595 mailbox,
596 })
597 }
598}
599
600impl From<Mailbox> for ReversePath {
602 fn from(mailbox: Mailbox) -> Self {
603 ReversePath::Path(MailPath {
604 at_domain_list: vec![],
605 mailbox,
606 })
607 }
608}
609
610impl From<Mailbox> for ForwardPath {
612 fn from(mailbox: Mailbox) -> Self {
613 ForwardPath::Path(MailPath {
614 at_domain_list: vec![],
615 mailbox,
616 })
617 }
618}
619
620impl TryFrom<EnvelopeAddress> for Mailbox {
625 type Error = &'static str;
626
627 fn try_from(addr: EnvelopeAddress) -> Result<Self, Self::Error> {
628 match addr {
629 EnvelopeAddress::Path(path) => Ok(path.mailbox),
630 EnvelopeAddress::Null => Err("Cannot convert Null to Mailbox"),
631 EnvelopeAddress::Postmaster => Err("Cannot convert Postmaster to Mailbox"),
632 }
633 }
634}
635
636impl TryFrom<ReversePath> for Mailbox {
637 type Error = &'static str;
638
639 fn try_from(path: ReversePath) -> Result<Self, Self::Error> {
640 match path {
641 ReversePath::Path(path) => Ok(path.mailbox),
642 ReversePath::NullSender => Err("Cannot convert NullSender to Mailbox"),
643 }
644 }
645}
646
647impl TryFrom<ForwardPath> for Mailbox {
648 type Error = &'static str;
649
650 fn try_from(path: ForwardPath) -> Result<Self, Self::Error> {
651 match path {
652 ForwardPath::Path(path) => Ok(path.mailbox),
653 ForwardPath::Postmaster => Err("Cannot convert Postmaster to Mailbox"),
654 }
655 }
656}
657
658impl TryFrom<EnvelopeAddress> for MailPath {
663 type Error = &'static str;
664
665 fn try_from(addr: EnvelopeAddress) -> Result<Self, Self::Error> {
666 match addr {
667 EnvelopeAddress::Path(path) => Ok(path),
668 EnvelopeAddress::Null => Err("Cannot convert Null to MailPath"),
669 EnvelopeAddress::Postmaster => Err("Cannot convert Postmaster to MailPath"),
670 }
671 }
672}
673
674impl TryFrom<ReversePath> for ForwardPath {
675 type Error = &'static str;
676
677 fn try_from(path: ReversePath) -> Result<Self, Self::Error> {
678 match path {
679 ReversePath::Path(mailpath) => Ok(ForwardPath::Path(mailpath)),
680 ReversePath::NullSender => Err("Cannot convert NullSender to ForwardPath"),
681 }
682 }
683}
684
685impl TryFrom<ForwardPath> for ReversePath {
686 type Error = &'static str;
687
688 fn try_from(path: ForwardPath) -> Result<Self, Self::Error> {
689 match path {
690 ForwardPath::Path(mailpath) => Ok(ReversePath::Path(mailpath)),
691 ForwardPath::Postmaster => Err("Cannot convert Postmaster to ReversePath"),
692 }
693 }
694}
695
696impl TryFrom<EnvelopeAddress> for ReversePath {
697 type Error = &'static str;
698
699 fn try_from(addr: EnvelopeAddress) -> Result<Self, Self::Error> {
700 match addr {
701 EnvelopeAddress::Path(path) => Ok(ReversePath::Path(path)),
702 EnvelopeAddress::Null => Ok(ReversePath::NullSender),
703 EnvelopeAddress::Postmaster => Err("Cannot convert Postmaster to ReversePath"),
704 }
705 }
706}
707
708impl TryFrom<EnvelopeAddress> for ForwardPath {
709 type Error = &'static str;
710
711 fn try_from(addr: EnvelopeAddress) -> Result<Self, Self::Error> {
712 match addr {
713 EnvelopeAddress::Path(path) => Ok(ForwardPath::Path(path)),
714 EnvelopeAddress::Null => Err("Cannot convert Null to ForwardPath"),
715 EnvelopeAddress::Postmaster => Ok(ForwardPath::Postmaster),
716 }
717 }
718}
719
720#[derive(Clone, Debug, PartialEq, Eq)]
722pub struct EsmtpParameter {
723 pub name: String,
724 pub value: Option<String>,
725}
726
727#[derive(Debug, Clone, PartialEq, Eq)]
731pub struct XClientParameter {
732 pub name: String,
733 pub value: String,
734}
735
736impl XClientParameter {
737 pub fn is_name(&self, name: impl AsRef<str>) -> bool {
739 self.name.eq_ignore_ascii_case(name.as_ref())
740 }
741
742 pub fn parse<T>(&self) -> Result<T, String>
746 where
747 T: std::str::FromStr,
748 T::Err: std::fmt::Display,
749 {
750 let parsed: Result<T, T::Err> = self.value.parse();
751 parsed.map_err(|e| e.to_string())
752 }
753}
754
755#[derive(Clone, PartialEq, Eq)]
756pub enum Command {
757 Ehlo(Domain),
758 Helo(Domain),
759 Lhlo(Domain),
760 Noop(Option<String>),
761 Help(Option<String>),
762 Vrfy(Option<String>),
763 Expn(Option<String>),
764 Data,
765 DataDot,
772 Rset,
773 Quit,
774 StartTls,
775 MailFrom {
776 address: ReversePath,
777 parameters: Vec<EsmtpParameter>,
778 },
779 RcptTo {
780 address: ForwardPath,
781 parameters: Vec<EsmtpParameter>,
782 },
783 Auth {
784 sasl_mech: String,
785 initial_response: Option<String>,
786 },
787 XClient(Vec<XClientParameter>),
788 Unknown(BString),
789}
790
791impl std::fmt::Debug for Command {
792 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
793 let encoded = self.encode();
795 write!(f, "Command(\"{}\")", encoded.escape_bytes())
796 }
797}
798
799#[derive(Clone, Debug, PartialEq, Eq)]
801pub enum PartialReason {
802 Syntax,
804 InvalidSenderAddress,
807 InvalidRecipientAddress,
810}
811
812#[derive(Clone, PartialEq, Eq)]
813pub enum MaybePartialCommand {
814 Full(Command),
815 Partial {
816 verb: CommandVerb,
817 remainder: BString,
818 reason: PartialReason,
819 },
820}
821
822impl std::fmt::Debug for MaybePartialCommand {
823 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
824 match self {
825 MaybePartialCommand::Full(cmd) => write!(f, "Full({cmd:?})"),
826 MaybePartialCommand::Partial {
827 verb,
828 remainder,
829 reason,
830 } => {
831 write!(
832 f,
833 "Partial {{ verb: {verb:?}, remainder: {:?}, reason: {reason:?} }}",
834 remainder.escape_bytes()
835 )
836 }
837 }
838 }
839}
840
841macro_rules! parse_single {
842 ($func_name:ident, $token:literal, $verb:ident) => {
843 fn $func_name(input: Span) -> IResult<Span, MaybePartialCommand> {
844 context(
845 $token,
846 alt((
847 map(
848 all_consuming((tag_no_case($token), wsp, anything)),
849 |(_cmd, _space, remainder)| MaybePartialCommand::Partial {
850 verb: CommandVerb::$verb,
851 remainder: (*remainder).into(),
852 reason: PartialReason::Syntax,
853 },
854 ),
855 map(all_consuming(tag_no_case($token)), |_| {
856 MaybePartialCommand::Full(Command::$verb)
857 }),
858 )),
859 )
860 .parse(input)
861 }
862
863 paste! {
864 #[cfg(test)]
865 #[test]
866 fn [<test_ $func_name>]() {
867 k9::assert_equal!(
868 unwrapper(Command::parse($token)),
869 MaybePartialCommand::Full(Command::$verb)
870 );
871 k9::assert_equal!(
872 unwrapper(Command::parse($token.to_lowercase())),
873 MaybePartialCommand::Full(Command::$verb)
874 );
875 k9::assert_equal!(
876 unwrapper(Command::parse(format!("{} trailing garbage", $token))),
877 MaybePartialCommand::Partial {
878 verb: CommandVerb::$verb,
879 remainder:"trailing garbage".into(),
880 reason: PartialReason::Syntax,
881 }
882 );
883 }
884 }
885 };
886}
887
888macro_rules! parse_opt_arg {
889 ($func_name:ident, $token:literal, $verb:ident) => {
890 fn $func_name(input: Span) -> IResult<Span, MaybePartialCommand> {
891 context(
892 $token,
893 alt((
894 map(
895 all_consuming((tag_no_case($token), wsp, string)),
896 |(_cmd, _space, param)| match String::from_utf8(param.fragment().to_vec()) {
897 Ok(s) => MaybePartialCommand::Full(Command::$verb(Some(s))),
898 Err(_) => MaybePartialCommand::Partial {
899 verb: CommandVerb::$verb,
900 remainder: BString::default(),
901 reason: PartialReason::Syntax,
902 },
903 },
904 ),
905 map(
906 all_consuming((tag_no_case($token), wsp, anything)),
907 |(_cmd, _space, remainder)| MaybePartialCommand::Partial {
908 verb: CommandVerb::$verb,
909 remainder: (*remainder).into(),
910 reason: PartialReason::Syntax,
911 },
912 ),
913 map(all_consuming(tag_no_case($token)), |_| {
914 MaybePartialCommand::Full(Command::$verb(None))
915 }),
916 )),
917 )
918 .parse(input)
919 }
920
921 paste! {
922 #[cfg(test)]
923 #[test]
924 fn [<test_ $func_name>]() {
925 k9::assert_equal!(
926 unwrapper(Command::parse($token)),
927 MaybePartialCommand::Full(Command::$verb(None)),
928 "full no param"
929 );
930 k9::assert_equal!(
931 unwrapper(Command::parse($token.to_lowercase())),
932 MaybePartialCommand::Full(Command::$verb(None)),
933 "full no param, different case"
934 );
935 k9::assert_equal!(
936 unwrapper(Command::parse(format!("{} parameter", $token))),
937 MaybePartialCommand::Full(Command::$verb(Some("parameter".into()))),
938 "full with param"
939 );
940 k9::assert_equal!(
941 unwrapper(Command::parse(format!("{} trailing garbage", $token))),
942 MaybePartialCommand::Partial {
943 verb: CommandVerb::$verb,
944 remainder:"trailing garbage".into(),
945 reason: PartialReason::Syntax,
946 },
947 "should have partial"
948 );
949 }
950 }
951 };
952}
953
954#[cfg(test)]
960fn unwrapper<T, E: std::fmt::Display>(result: Result<T, E>) -> T {
961 match result {
962 Ok(r) => r,
963 Err(err) => panic!("{err}"),
964 }
965}
966
967parse_opt_arg!(parse_noop, "NOOP", Noop);
968parse_opt_arg!(parse_help, "HELP", Help);
969parse_opt_arg!(parse_vrfy, "VRFY", Vrfy);
970parse_opt_arg!(parse_expn, "EXPN", Expn);
971parse_single!(parse_data, "DATA", Data);
972parse_single!(parse_rset, "RSET", Rset);
973parse_single!(parse_quit, "QUIT", Quit);
974parse_single!(parse_starttls, "STARTTLS", StartTls);
975
976fn parse_with<'a, R, F>(text: &'a [u8], parser: F) -> Result<R, String>
977where
978 F: Fn(Span<'a>) -> IResult<'a, Span<'a>, R>,
979{
980 let input = make_span(text);
981 let (_, result) = all_consuming(parser)
982 .parse(input)
983 .map_err(|err| explain_nom(input, err))?;
984 Ok(result)
985}
986
987impl Command {
988 pub fn parse(input: impl AsRef<[u8]>) -> Result<MaybePartialCommand, String> {
989 let bytes = input.as_ref();
993 let bytes = bytes
994 .strip_suffix(b"\r\n")
995 .or_else(|| bytes.strip_suffix(b"\n"))
996 .unwrap_or(bytes);
997 parse_with(bytes, Self::parse_span)
998 }
999
1000 fn parse_span(input: Span) -> IResult<Span, MaybePartialCommand> {
1001 context(
1002 "command-verb",
1003 alt((
1004 parse_ehlo,
1005 parse_helo,
1006 parse_lhlo,
1007 parse_help,
1008 parse_noop,
1009 parse_vrfy,
1010 parse_expn,
1011 parse_data,
1012 parse_rset,
1013 parse_quit,
1014 parse_starttls,
1015 parse_mail_from,
1016 parse_rcpt_to,
1017 parse_auth,
1018 parse_xclient,
1019 Self::parse_unknown,
1020 )),
1021 )
1022 .parse(input)
1023 }
1024
1025 pub fn encode(&self) -> BString {
1033 let mut buf: Vec<u8> = Vec::new();
1034 match self {
1035 Self::Ehlo(domain) => {
1036 buf.extend_from_slice(b"EHLO ");
1037 buf.extend_from_slice(encode_domain(domain).as_ref());
1038 }
1039 Self::Helo(domain) => {
1040 buf.extend_from_slice(b"HELO ");
1041 buf.extend_from_slice(encode_domain(domain).as_ref());
1042 }
1043 Self::Lhlo(domain) => {
1044 buf.extend_from_slice(b"LHLO ");
1045 buf.extend_from_slice(encode_domain(domain).as_ref());
1046 }
1047 Self::Noop(None) => buf.extend_from_slice(b"NOOP"),
1048 Self::Noop(Some(s)) => {
1049 buf.extend_from_slice(b"NOOP ");
1050 buf.extend_from_slice(s.as_bytes());
1051 }
1052 Self::Help(None) => buf.extend_from_slice(b"HELP"),
1053 Self::Help(Some(s)) => {
1054 buf.extend_from_slice(b"HELP ");
1055 buf.extend_from_slice(s.as_bytes());
1056 }
1057 Self::Vrfy(None) => buf.extend_from_slice(b"VRFY"),
1058 Self::Vrfy(Some(s)) => {
1059 buf.extend_from_slice(b"VRFY ");
1060 buf.extend_from_slice(s.as_bytes());
1061 }
1062 Self::Expn(None) => buf.extend_from_slice(b"EXPN"),
1063 Self::Expn(Some(s)) => {
1064 buf.extend_from_slice(b"EXPN ");
1065 buf.extend_from_slice(s.as_bytes());
1066 }
1067 Self::Data => buf.extend_from_slice(b"DATA"),
1068 Self::DataDot => return BString::from(".\r\n"),
1071 Self::Rset => buf.extend_from_slice(b"RSET"),
1072 Self::Quit => buf.extend_from_slice(b"QUIT"),
1073 Self::StartTls => buf.extend_from_slice(b"STARTTLS"),
1074 Self::MailFrom {
1075 address,
1076 parameters,
1077 } => {
1078 buf.extend_from_slice(b"MAIL FROM:<");
1079 buf.extend(encode_reverse_path(address));
1080 buf.push(b'>');
1081 buf.extend(encode_esmtp_params(parameters));
1082 }
1083 Self::RcptTo {
1084 address,
1085 parameters,
1086 } => {
1087 buf.extend_from_slice(b"RCPT TO:<");
1088 buf.extend(encode_forward_path(address));
1089 buf.push(b'>');
1090 buf.extend(encode_esmtp_params(parameters));
1091 }
1092 Self::Auth {
1093 sasl_mech,
1094 initial_response: None,
1095 } => {
1096 buf.extend_from_slice(b"AUTH ");
1097 buf.extend_from_slice(sasl_mech.as_bytes());
1098 }
1099 Self::Auth {
1100 sasl_mech,
1101 initial_response: Some(resp),
1102 } => {
1103 buf.extend_from_slice(b"AUTH ");
1104 buf.extend_from_slice(sasl_mech.as_bytes());
1105 buf.push(b' ');
1106 buf.extend_from_slice(resp.as_bytes());
1107 }
1108 Self::XClient(params) => {
1109 buf.extend_from_slice(b"XCLIENT");
1110 buf.extend(encode_xclient_params(params));
1111 }
1112 Self::Unknown(s) => {
1113 buf.extend_from_slice(s);
1114 }
1115 }
1116 buf.extend_from_slice(b"\r\n");
1117 BString::from(buf)
1118 }
1119
1120 pub fn client_timeout(&self, timeouts: &SmtpClientTimeouts) -> Duration {
1122 match self {
1123 Self::Helo(_) | Self::Ehlo(_) | Self::Lhlo(_) => timeouts.ehlo_timeout,
1124 Self::MailFrom { .. } => timeouts.mail_from_timeout,
1125 Self::RcptTo { .. } => timeouts.rcpt_to_timeout,
1126 Self::Data => timeouts.data_timeout,
1127 Self::DataDot => timeouts.data_dot_timeout,
1128 Self::Rset => timeouts.rset_timeout,
1129 Self::StartTls => timeouts.starttls_timeout,
1130 Self::Quit | Self::Vrfy(_) | Self::Expn(_) | Self::Help(_) | Self::Noop(_) => {
1131 timeouts.idle_timeout
1132 }
1133 Self::Auth { .. } => timeouts.auth_timeout,
1134 Self::XClient(_) => timeouts.auth_timeout, Self::Unknown(_) => timeouts.mail_from_timeout, }
1137 }
1138
1139 pub fn client_timeout_request(&self, timeouts: &SmtpClientTimeouts) -> Duration {
1141 self.client_timeout(timeouts).min(Duration::from_secs(60))
1142 }
1143
1144 fn parse_unknown(input: Span) -> IResult<Span, MaybePartialCommand> {
1145 context(
1146 "unknown-command",
1147 alt((
1148 map(
1149 all_consuming(recognize((command_word, wsp, anything))),
1150 |command| MaybePartialCommand::Full(Command::Unknown((*command).into())),
1151 ),
1152 map(all_consuming(command_word), |command| {
1153 MaybePartialCommand::Full(Command::Unknown((*command).into()))
1154 }),
1155 )),
1156 )
1157 .parse(input)
1158 }
1159}
1160
1161fn command_word(input: Span) -> IResult<Span, Span> {
1162 context(
1163 "command-word",
1164 take_while1(|c: u8| c.is_ascii_alphanumeric()),
1165 )
1166 .parse(input)
1167}
1168
1169fn wsp(input: Span) -> IResult<Span, Span> {
1170 context("wsp", take_while1(|c| c == b' ' || c == b'\t')).parse(input)
1171}
1172
1173fn anything(input: Span) -> IResult<Span, Span> {
1174 context("anything", take_while1(|_| true)).parse(input)
1175}
1176
1177fn atext(input: Span) -> IResult<Span, Span> {
1178 recognize(alt((
1179 take_while_m_n(1, 1, |c: u8| {
1180 matches!(
1181 c,
1182 b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'/' | b'=' | b'?'
1183 | b'^' | b'_' | b'`' | b'{' | b'|' | b'}' | b'~'
1184 | b'A'..=b'Z'
1185 | b'a'..=b'z'
1186 | b'0'..=b'9'
1187 )
1188 }),
1189 utf8_non_ascii,
1190 )))
1191 .parse(input)
1192}
1193
1194fn atom(input: Span) -> IResult<Span, Span> {
1195 context("atom", recognize(many1(atext))).parse(input)
1196}
1197
1198fn quoted_string(input: Span) -> IResult<Span, Span> {
1199 context(
1200 "quoted-string",
1201 recognize((
1202 tag("\""),
1203 many0(alt((
1204 recognize(pair(
1205 tag("\\"),
1206 take_while_m_n(1, 1, |c: u8| c >= 0x20 && c <= 0x7e),
1207 )),
1208 take_while_m_n(1, 1, |c: u8| {
1209 (c >= 0x20 && c <= 0x21) || (c >= 0x23 && c <= 0x5b) || (c >= 0x5d && c <= 0x7e)
1210 }),
1211 utf8_non_ascii,
1212 ))),
1213 tag("\""),
1214 )),
1215 )
1216 .parse(input)
1217}
1218
1219fn string(input: Span) -> IResult<Span, Span> {
1220 context("string", alt((atom, quoted_string))).parse(input)
1221}
1222
1223fn dot_string(input: Span) -> IResult<Span, Span> {
1229 context("dot-string", recognize((atom, many0(pair(tag("."), atom))))).parse(input)
1230}
1231
1232fn local_part(input: Span) -> IResult<Span, Span> {
1234 context("local-part", alt((dot_string, quoted_string))).parse(input)
1235}
1236
1237fn dcontent(input: Span) -> IResult<Span, Span> {
1239 take_while1(|c: u8| (c >= 33 && c <= 90) || (c >= 94 && c <= 126)).parse(input)
1240}
1241
1242fn address_literal_content(input: Span) -> IResult<Span, Domain> {
1248 let is_ipv6 = input
1249 .fragment()
1250 .get(..5)
1251 .map(|b| b.eq_ignore_ascii_case(b"IPv6:"))
1252 .unwrap_or(false);
1253
1254 if is_ipv6 {
1255 context(
1257 "ipv6-address-literal",
1258 map((tag_no_case("IPv6:"), ipv6_address), |(_, ip)| {
1259 Domain::V6(ip)
1260 }),
1261 )
1262 .parse(input)
1263 } else {
1264 alt((
1265 map(ipv4_address, Domain::V4),
1266 map_res(
1267 (
1268 recognize(take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'-')),
1269 tag(":"),
1270 recognize(many1(dcontent)),
1271 ),
1272 |(tag_s, _colon, lit): (Span, _, Span)| -> Result<Domain, String> {
1273 let mut s = String::from_utf8(tag_s.fragment().to_vec())
1275 .map_err(|_| "address_literal: invalid UTF-8 in tag".to_string())?;
1276 s.push(':');
1277 let lit_str = std::str::from_utf8(lit.fragment())
1278 .map_err(|_| "address_literal: invalid UTF-8 in literal".to_string())?;
1279 s.push_str(lit_str);
1280 Ok(Domain::Tagged(s))
1281 },
1282 ),
1283 ))
1284 .parse(input)
1285 }
1286}
1287
1288fn address_literal(input: Span) -> IResult<Span, Domain> {
1290 context(
1291 "address-literal",
1292 map((tag("["), address_literal_content, tag("]")), |(_, d, _)| d),
1293 )
1294 .parse(input)
1295}
1296
1297fn mailbox_domain(input: Span) -> IResult<Span, Domain> {
1299 context(
1300 "mailbox-domain",
1301 alt((address_literal, map(domain_name, Domain::DomainName))),
1302 )
1303 .parse(input)
1304}
1305
1306fn mailbox(input: Span) -> IResult<Span, Mailbox> {
1308 context(
1309 "mailbox",
1310 map_res(
1311 (local_part, tag("@"), mailbox_domain),
1312 |(lp, _, dom): (Span, _, Domain)| -> Result<Mailbox, String> {
1313 let local_part = String::from_utf8(lp.fragment().to_vec())
1316 .map_err(|_| "invalid UTF-8 in local-part".to_string())?;
1317 Ok(Mailbox {
1318 local_part,
1319 domain: dom,
1320 })
1321 },
1322 ),
1323 )
1324 .parse(input)
1325}
1326
1327fn at_domain(input: Span) -> IResult<Span, String> {
1329 map_res(
1330 (tag("@"), recognize(domain_name)),
1331 |(_, d): (Span, Span)| -> Result<String, String> {
1332 String::from_utf8(d.fragment().to_vec())
1333 .map_err(|_| "at_domain: invalid UTF-8 in domain".to_string())
1334 },
1335 )
1336 .parse(input)
1337}
1338
1339fn at_domain_list(input: Span) -> IResult<Span, Vec<String>> {
1341 context(
1342 "at-domain-list",
1343 map(
1344 (at_domain, many0((tag(","), at_domain)), tag(":")),
1345 |(first, rest, _)| {
1346 let mut v = vec![first];
1347 v.extend(rest.into_iter().map(|(_, d)| d));
1348 v
1349 },
1350 ),
1351 )
1352 .parse(input)
1353}
1354
1355fn null_sender(input: Span) -> IResult<Span, ReversePath> {
1357 context("null-sender", map(tag("<>"), |_| ReversePath::NullSender)).parse(input)
1358}
1359
1360fn path(input: Span) -> IResult<Span, MailPath> {
1366 context(
1367 "path",
1368 map(
1369 (tag("<"), opt(at_domain_list), mailbox, tag(">")),
1370 |(_, domains, mb, _)| MailPath {
1371 at_domain_list: domains.unwrap_or_default(),
1372 mailbox: mb,
1373 },
1374 ),
1375 )
1376 .parse(input)
1377}
1378
1379fn reverse_path(input: Span) -> IResult<Span, ReversePath> {
1385 context(
1386 "reverse-path",
1387 alt((
1388 null_sender,
1389 map(path, ReversePath::Path),
1390 map(mailbox, ReversePath::from),
1391 )),
1392 )
1393 .parse(input)
1394}
1395
1396fn postmaster_path(input: Span) -> IResult<Span, ForwardPath> {
1402 context(
1403 "postmaster",
1404 map(tag_no_case("<Postmaster>"), |_| ForwardPath::Postmaster),
1405 )
1406 .parse(input)
1407}
1408
1409fn forward_path(input: Span) -> IResult<Span, ForwardPath> {
1415 context(
1416 "forward-path",
1417 alt((
1418 postmaster_path,
1419 map(path, ForwardPath::Path),
1420 map(mailbox, ForwardPath::from),
1421 )),
1422 )
1423 .parse(input)
1424}
1425
1426fn esmtp_keyword(input: Span) -> IResult<Span, Span> {
1428 context(
1429 "esmtp-keyword",
1430 recognize((
1431 take_while_m_n(1, 1, |c: u8| c.is_ascii_alphanumeric()),
1432 many0(take_while_m_n(1, 1, |c: u8| {
1433 c.is_ascii_alphanumeric() || c == b'-'
1434 })),
1435 )),
1436 )
1437 .parse(input)
1438}
1439
1440fn esmtp_value(input: Span) -> IResult<Span, Span> {
1450 context(
1451 "esmtp-value",
1452 recognize(many1(alt((
1453 take_while1(|c: u8| (c >= 33 && c <= 60) || (c >= 62 && c <= 126)),
1454 utf8_non_ascii,
1455 )))),
1456 )
1457 .parse(input)
1458}
1459
1460fn esmtp_param(input: Span) -> IResult<Span, EsmtpParameter> {
1462 context(
1463 "esmtp-param",
1464 map_res(
1465 (esmtp_keyword, opt((tag("="), esmtp_value))),
1466 |(name, value): (Span, Option<(Span, Span)>)| -> Result<EsmtpParameter, String> {
1467 let name = String::from_utf8(name.fragment().to_vec())
1468 .map_err(|_| "esmtp_param: invalid UTF-8 in name".to_string())?;
1469 let value = value
1470 .map(|(_, v)| {
1471 String::from_utf8(v.fragment().to_vec())
1472 .map_err(|_| "esmtp_param: invalid UTF-8 in value".to_string())
1473 })
1474 .transpose()?;
1475 Ok(EsmtpParameter { name, value })
1476 },
1477 ),
1478 )
1479 .parse(input)
1480}
1481
1482fn mail_parameters(input: Span) -> IResult<Span, Vec<EsmtpParameter>> {
1484 context(
1485 "mail-parameters",
1486 map((esmtp_param, many0((wsp, esmtp_param))), |(first, rest)| {
1487 let mut params = vec![first];
1488 params.extend(rest.into_iter().map(|(_, p)| p));
1489 params
1490 }),
1491 )
1492 .parse(input)
1493}
1494
1495fn parse_ehlo(input: Span) -> IResult<Span, MaybePartialCommand> {
1505 context(
1506 "ehlo",
1507 alt((
1508 map(
1509 all_consuming((tag_no_case("EHLO"), wsp, mailbox_domain)),
1510 |(_, _, domain)| MaybePartialCommand::Full(Command::Ehlo(domain)),
1511 ),
1512 map(
1513 all_consuming((tag_no_case("EHLO"), wsp, anything)),
1514 |(_, _, remainder)| MaybePartialCommand::Partial {
1515 verb: CommandVerb::Ehlo,
1516 remainder: (*remainder).into(),
1517 reason: PartialReason::Syntax,
1518 },
1519 ),
1520 map(all_consuming(tag_no_case("EHLO")), |_| {
1521 MaybePartialCommand::Partial {
1522 verb: CommandVerb::Ehlo,
1523 remainder: BString::default(),
1524 reason: PartialReason::Syntax,
1525 }
1526 }),
1527 )),
1528 )
1529 .parse(input)
1530}
1531
1532fn parse_helo(input: Span) -> IResult<Span, MaybePartialCommand> {
1536 context(
1537 "helo",
1538 alt((
1539 map(
1540 all_consuming((tag_no_case("HELO"), wsp, mailbox_domain)),
1541 |(_, _, domain)| MaybePartialCommand::Full(Command::Helo(domain)),
1542 ),
1543 map(
1544 all_consuming((tag_no_case("HELO"), wsp, anything)),
1545 |(_, _, remainder)| MaybePartialCommand::Partial {
1546 verb: CommandVerb::Helo,
1547 remainder: (*remainder).into(),
1548 reason: PartialReason::Syntax,
1549 },
1550 ),
1551 map(all_consuming(tag_no_case("HELO")), |_| {
1552 MaybePartialCommand::Partial {
1553 verb: CommandVerb::Helo,
1554 remainder: BString::default(),
1555 reason: PartialReason::Syntax,
1556 }
1557 }),
1558 )),
1559 )
1560 .parse(input)
1561}
1562
1563fn parse_lhlo(input: Span) -> IResult<Span, MaybePartialCommand> {
1565 context(
1566 "lhlo",
1567 alt((
1568 map(
1569 all_consuming((tag_no_case("LHLO"), wsp, mailbox_domain)),
1570 |(_, _, domain)| MaybePartialCommand::Full(Command::Lhlo(domain)),
1571 ),
1572 map(
1573 all_consuming((tag_no_case("LHLO"), wsp, anything)),
1574 |(_, _, remainder)| MaybePartialCommand::Partial {
1575 verb: CommandVerb::Lhlo,
1576 remainder: (*remainder).into(),
1577 reason: PartialReason::Syntax,
1578 },
1579 ),
1580 map(all_consuming(tag_no_case("LHLO")), |_| {
1581 MaybePartialCommand::Partial {
1582 verb: CommandVerb::Lhlo,
1583 remainder: BString::default(),
1584 reason: PartialReason::Syntax,
1585 }
1586 }),
1587 )),
1588 )
1589 .parse(input)
1590}
1591
1592fn sasl_mechanism(input: Span) -> IResult<Span, String> {
1598 context(
1599 "sasl-mechanism",
1600 map(
1601 take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'-'),
1602 |s: Span| {
1603 String::from_utf8(s.fragment().to_vec()).expect("sasl_mechanism guaranteed ASCII")
1605 },
1606 ),
1607 )
1608 .parse(input)
1609}
1610
1611fn auth_initial_response(input: Span) -> IResult<Span, String> {
1617 context(
1618 "auth-initial-response",
1619 map(
1620 take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'+' || c == b'/' || c == b'='),
1621 |s: Span| {
1622 String::from_utf8(s.fragment().to_vec())
1624 .expect("auth_initial_response guaranteed ASCII")
1625 },
1626 ),
1627 )
1628 .parse(input)
1629}
1630
1631fn parse_auth(input: Span) -> IResult<Span, MaybePartialCommand> {
1633 context(
1634 "auth",
1635 alt((
1636 map(
1638 all_consuming((
1639 tag_no_case("AUTH"),
1640 wsp,
1641 sasl_mechanism,
1642 opt((wsp, auth_initial_response)),
1643 )),
1644 |(_, _, sasl_mech, resp)| match resp {
1645 Some((_, r)) => MaybePartialCommand::Full(Command::Auth {
1646 sasl_mech,
1647 initial_response: Some(r),
1648 }),
1649 None => MaybePartialCommand::Full(Command::Auth {
1650 sasl_mech,
1651 initial_response: None,
1652 }),
1653 },
1654 ),
1655 map(
1657 all_consuming((tag_no_case("AUTH"), wsp, anything)),
1658 |(_, _, remainder)| MaybePartialCommand::Partial {
1659 verb: CommandVerb::Auth,
1660 remainder: (*remainder).into(),
1661 reason: PartialReason::Syntax,
1662 },
1663 ),
1664 map(all_consuming(tag_no_case("AUTH")), |_| {
1666 MaybePartialCommand::Partial {
1667 verb: CommandVerb::Auth,
1668 remainder: BString::default(),
1669 reason: PartialReason::Syntax,
1670 }
1671 }),
1672 )),
1673 )
1674 .parse(input)
1675}
1676
1677fn xtext_decode(encoded: &[u8]) -> Result<String, String> {
1687 let mut result: Vec<u8> = Vec::with_capacity(encoded.len());
1688 let mut i = 0;
1689 while i < encoded.len() {
1690 if encoded[i] == b'+' {
1691 if i + 2 >= encoded.len() {
1692 return Err(format!("xtext_decode: truncated hex escape at byte {i}"));
1693 }
1694 let hi = hex_nibble(encoded[i + 1]).map_err(|e| format!("xtext_decode: {e}"))?;
1695 let lo = hex_nibble(encoded[i + 2]).map_err(|e| format!("xtext_decode: {e}"))?;
1696 result.push((hi << 4) | lo);
1697 i += 3;
1698 } else {
1699 result.push(encoded[i]);
1700 i += 1;
1701 }
1702 }
1703 String::from_utf8(result)
1704 .map_err(|_| "xtext_decode: invalid UTF-8 in decoded value".to_string())
1705}
1706
1707fn hex_nibble(b: u8) -> Result<u8, String> {
1708 match b {
1709 b'0'..=b'9' => Ok(b - b'0'),
1710 b'a'..=b'f' => Ok(b - b'a' + 10),
1711 b'A'..=b'F' => Ok(b - b'A' + 10),
1712 _ => Err(format!("invalid hex digit '{}'", b as char)),
1713 }
1714}
1715
1716fn xclient_xtext_value(input: Span) -> IResult<Span, Span> {
1720 context(
1721 "xclient-xtext-value",
1722 take_while1(|c: u8| c >= 33 && c <= 126),
1723 )
1724 .parse(input)
1725}
1726
1727fn xclient_param(input: Span) -> IResult<Span, XClientParameter> {
1732 context(
1733 "xclient-param",
1734 map_res(
1735 (esmtp_keyword, tag("="), xclient_xtext_value),
1736 |(name, _, value): (Span, _, Span)| -> Result<XClientParameter, String> {
1737 let name = String::from_utf8(name.fragment().to_vec())
1738 .map_err(|_| "xclient_param: invalid UTF-8 in name".to_string())?;
1739 let value = xtext_decode(value.fragment())?;
1740 Ok(XClientParameter { name, value })
1741 },
1742 ),
1743 )
1744 .parse(input)
1745}
1746
1747fn xclient_params(input: Span) -> IResult<Span, Vec<XClientParameter>> {
1749 context(
1750 "xclient-params",
1751 map(
1752 (xclient_param, many0((wsp, xclient_param))),
1753 |(first, rest)| {
1754 let mut params = vec![first];
1755 params.extend(rest.into_iter().map(|(_, p)| p));
1756 params
1757 },
1758 ),
1759 )
1760 .parse(input)
1761}
1762
1763fn parse_xclient(input: Span) -> IResult<Span, MaybePartialCommand> {
1765 context(
1766 "xclient",
1767 alt((
1768 map(
1770 all_consuming((tag_no_case("XCLIENT"), wsp, xclient_params)),
1771 |(_, _, params)| MaybePartialCommand::Full(Command::XClient(params)),
1772 ),
1773 map(
1775 all_consuming((tag_no_case("XCLIENT"), wsp, anything)),
1776 |(_, _, remainder)| MaybePartialCommand::Partial {
1777 verb: CommandVerb::XClient,
1778 remainder: (*remainder).into(),
1779 reason: PartialReason::Syntax,
1780 },
1781 ),
1782 map(all_consuming(tag_no_case("XCLIENT")), |_| {
1784 MaybePartialCommand::Partial {
1785 verb: CommandVerb::XClient,
1786 remainder: BString::default(),
1787 reason: PartialReason::Syntax,
1788 }
1789 }),
1790 )),
1791 )
1792 .parse(input)
1793}
1794
1795fn parse_mail_from(input: Span) -> IResult<Span, MaybePartialCommand> {
1805 context(
1806 "mail-from",
1807 alt((
1808 map(
1810 all_consuming((
1811 tag_no_case("MAIL"),
1812 wsp,
1813 tag_no_case("FROM:"),
1814 reverse_path,
1815 opt(map((wsp, mail_parameters), |(_, p)| p)),
1816 )),
1817 |(_, _, _, address, parameters)| {
1818 MaybePartialCommand::Full(Command::MailFrom {
1819 address,
1820 parameters: parameters.unwrap_or_default(),
1821 })
1822 },
1823 ),
1824 map(
1826 all_consuming((
1827 tag_no_case("MAIL"),
1828 wsp,
1829 tag_no_case("FROM:"),
1830 reverse_path,
1831 wsp,
1832 anything,
1833 )),
1834 |(_, _, _, _, _, remainder)| MaybePartialCommand::Partial {
1835 verb: CommandVerb::Mail,
1836 remainder: (*remainder).into(),
1837 reason: PartialReason::Syntax,
1838 },
1839 ),
1840 map(
1842 all_consuming((tag_no_case("MAIL"), wsp, tag_no_case("FROM:"), anything)),
1843 |(_, _, _, remainder)| MaybePartialCommand::Partial {
1844 verb: CommandVerb::Mail,
1845 remainder: (*remainder).into(),
1846 reason: PartialReason::InvalidSenderAddress,
1847 },
1848 ),
1849 map(
1851 all_consuming((tag_no_case("MAIL"), wsp, anything)),
1852 |(_, _, remainder)| MaybePartialCommand::Partial {
1853 verb: CommandVerb::Mail,
1854 remainder: (*remainder).into(),
1855 reason: PartialReason::Syntax,
1856 },
1857 ),
1858 map(all_consuming(tag_no_case("MAIL")), |_| {
1860 MaybePartialCommand::Partial {
1861 verb: CommandVerb::Mail,
1862 remainder: BString::default(),
1863 reason: PartialReason::Syntax,
1864 }
1865 }),
1866 )),
1867 )
1868 .parse(input)
1869}
1870
1871fn parse_rcpt_to(input: Span) -> IResult<Span, MaybePartialCommand> {
1881 context(
1882 "rcpt-to",
1883 alt((
1884 map(
1886 all_consuming((
1887 tag_no_case("RCPT"),
1888 wsp,
1889 tag_no_case("TO:"),
1890 forward_path,
1891 opt(map((wsp, mail_parameters), |(_, p)| p)),
1892 )),
1893 |(_, _, _, address, parameters)| {
1894 MaybePartialCommand::Full(Command::RcptTo {
1895 address,
1896 parameters: parameters.unwrap_or_default(),
1897 })
1898 },
1899 ),
1900 map(
1902 all_consuming((
1903 tag_no_case("RCPT"),
1904 wsp,
1905 tag_no_case("TO:"),
1906 forward_path,
1907 wsp,
1908 anything,
1909 )),
1910 |(_, _, _, _, _, remainder)| MaybePartialCommand::Partial {
1911 verb: CommandVerb::Rcpt,
1912 remainder: (*remainder).into(),
1913 reason: PartialReason::Syntax,
1914 },
1915 ),
1916 map(
1918 all_consuming((tag_no_case("RCPT"), wsp, tag_no_case("TO:"), anything)),
1919 |(_, _, _, remainder)| MaybePartialCommand::Partial {
1920 verb: CommandVerb::Rcpt,
1921 remainder: (*remainder).into(),
1922 reason: PartialReason::InvalidRecipientAddress,
1923 },
1924 ),
1925 map(
1927 all_consuming((tag_no_case("RCPT"), wsp, anything)),
1928 |(_, _, remainder)| MaybePartialCommand::Partial {
1929 verb: CommandVerb::Rcpt,
1930 remainder: (*remainder).into(),
1931 reason: PartialReason::Syntax,
1932 },
1933 ),
1934 map(all_consuming(tag_no_case("RCPT")), |_| {
1936 MaybePartialCommand::Partial {
1937 verb: CommandVerb::Rcpt,
1938 remainder: BString::default(),
1939 reason: PartialReason::Syntax,
1940 }
1941 }),
1942 )),
1943 )
1944 .parse(input)
1945}
1946
1947fn hex_nibble_lower(n: u8) -> u8 {
1953 if n < 10 {
1954 b'0' + n
1955 } else {
1956 b'a' + n - 10
1957 }
1958}
1959
1960fn xtext_encode_bytes(value: &[u8]) -> Vec<u8> {
1966 let mut result = Vec::with_capacity(value.len());
1967 for &b in value {
1968 if b >= 33 && b <= 126 && b != b'+' && b != b'=' {
1969 result.push(b);
1970 } else {
1971 result.push(b'+');
1972 result.push(hex_nibble_lower(b >> 4));
1973 result.push(hex_nibble_lower(b & 0x0f));
1974 }
1975 }
1976 result
1977}
1978
1979fn encode_domain(domain: &Domain) -> BString {
1986 match domain {
1987 Domain::DomainName(s) => BString::from(s.to_string()),
1988 Domain::V4(ip) => BString::from(format!("[{}]", ip)),
1989 Domain::V6(ip) => BString::from(format!("[IPv6:{}]", ip)),
1990 Domain::Tagged(s) => BString::from(format!("[{}]", s)),
1991 }
1992}
1993
1994fn encode_mail_path(path: &MailPath) -> Vec<u8> {
2000 let mut buf = path.mailbox.local_part.as_bytes().to_vec();
2001 buf.push(b'@');
2002 buf.extend_from_slice(encode_domain(&path.mailbox.domain).as_ref());
2003 buf
2004}
2005
2006fn encode_reverse_path(rp: &ReversePath) -> Vec<u8> {
2012 match rp {
2013 ReversePath::NullSender => vec![],
2014 ReversePath::Path(path) => encode_mail_path(path),
2015 }
2016}
2017
2018fn encode_forward_path(fp: &ForwardPath) -> Vec<u8> {
2024 match fp {
2025 ForwardPath::Postmaster => b"Postmaster".to_vec(),
2026 ForwardPath::Path(path) => encode_mail_path(path),
2027 }
2028}
2029
2030fn encode_esmtp_params(params: &[EsmtpParameter]) -> Vec<u8> {
2035 let mut buf = Vec::new();
2036 for p in params {
2037 buf.push(b' ');
2038 buf.extend_from_slice(p.name.as_bytes());
2039 if let Some(v) = &p.value {
2040 buf.push(b'=');
2041 buf.extend_from_slice(v.as_bytes());
2042 }
2043 }
2044 buf
2045}
2046
2047fn encode_xclient_params(params: &[XClientParameter]) -> Vec<u8> {
2051 let mut buf = Vec::new();
2052 for p in params {
2053 buf.push(b' ');
2054 buf.extend_from_slice(p.name.as_bytes());
2055 buf.push(b'=');
2056 buf.extend(xtext_encode_bytes(p.value.as_bytes()));
2057 }
2058 buf
2059}
2060
2061#[cfg(test)]
2062mod test {
2063 use super::*;
2064
2065 #[test]
2066 fn test_string() {
2067 k9::snapshot!(
2068 BStr::new(&parse_with("hello".as_bytes(), string).unwrap()),
2069 "hello"
2070 );
2071 k9::snapshot!(
2072 BStr::new(&parse_with("\"hello\"".as_bytes(), string).unwrap()),
2073 "\"hello\""
2074 );
2075 k9::snapshot!(
2076 BStr::new(&parse_with("\"hello world\"".as_bytes(), string).unwrap()),
2077 "\"hello world\""
2078 );
2079 k9::snapshot!(
2080 parse_with("hello world".as_bytes(), string),
2081 r#"
2082Err(
2083 "Error at line 1, in Eof:
2084hello world
2085 ^_____
2086
2087",
2088)
2089"#
2090 );
2091 }
2092
2093 #[test]
2094 fn test_bogus() {
2095 k9::snapshot!(
2096 Command::parse("bogus"),
2097 r#"
2098Ok(
2099 Full(Command("bogus\r
2100")),
2101)
2102"#
2103 );
2104 }
2105
2106 #[test]
2111 fn test_ehlo_domain_name() {
2112 k9::assert_equal!(
2113 unwrapper(Command::parse("EHLO example.com")),
2114 MaybePartialCommand::Full(Command::Ehlo(Domain::DomainName(
2115 "example.com".parse().unwrap()
2116 )))
2117 );
2118 }
2119
2120 #[test]
2121 fn test_ehlo_case_insensitive() {
2122 k9::assert_equal!(
2123 unwrapper(Command::parse("ehlo example.com")),
2124 MaybePartialCommand::Full(Command::Ehlo(Domain::DomainName(
2125 "example.com".parse().unwrap()
2126 )))
2127 );
2128 }
2129
2130 #[test]
2131 fn test_ehlo_ipv4_literal() {
2132 k9::assert_equal!(
2133 unwrapper(Command::parse("EHLO [10.0.0.1]")),
2134 MaybePartialCommand::Full(Command::Ehlo(Domain::V4("10.0.0.1".parse().unwrap())))
2135 );
2136 }
2137
2138 #[test]
2139 fn test_ehlo_ipv6_literal() {
2140 k9::assert_equal!(
2141 unwrapper(Command::parse("EHLO [IPv6:::1]")),
2142 MaybePartialCommand::Full(Command::Ehlo(Domain::V6("::1".parse().unwrap())))
2143 );
2144 }
2145
2146 #[test]
2147 fn test_ehlo_tagged_literal() {
2148 k9::assert_equal!(
2149 unwrapper(Command::parse("EHLO [future:something]")),
2150 MaybePartialCommand::Full(Command::Ehlo(Domain::Tagged("future:something".into(),)))
2151 );
2152 }
2153
2154 #[test]
2155 fn test_ehlo_invalid_ipv4_is_partial() {
2156 k9::assert_equal!(
2157 unwrapper(Command::parse("EHLO [999.999.999.999]")),
2158 MaybePartialCommand::Partial {
2159 verb: CommandVerb::Ehlo,
2160 remainder: "[999.999.999.999]".into(),
2161 reason: PartialReason::Syntax,
2162 }
2163 );
2164 }
2165
2166 #[test]
2167 fn test_ehlo_invalid_ipv6_is_partial() {
2168 k9::assert_equal!(
2169 unwrapper(Command::parse("EHLO [IPv6:not-an-ipv6]")),
2170 MaybePartialCommand::Partial {
2171 verb: CommandVerb::Ehlo,
2172 remainder: "[IPv6:not-an-ipv6]".into(),
2173 reason: PartialReason::Syntax,
2174 }
2175 );
2176 }
2177
2178 #[test]
2179 fn test_ehlo_alone_is_partial() {
2180 k9::assert_equal!(
2181 unwrapper(Command::parse("EHLO")),
2182 MaybePartialCommand::Partial {
2183 verb: CommandVerb::Ehlo,
2184 remainder: "".into(),
2185 reason: PartialReason::Syntax,
2186 }
2187 );
2188 }
2189
2190 #[test]
2191 fn test_ehlo_with_garbage_is_partial() {
2192 k9::assert_equal!(
2193 unwrapper(Command::parse("EHLO !!invalid!!")),
2194 MaybePartialCommand::Partial {
2195 verb: CommandVerb::Ehlo,
2196 remainder: "!!invalid!!".into(),
2197 reason: PartialReason::Syntax,
2198 }
2199 );
2200 }
2201
2202 #[test]
2207 fn test_helo_domain_name() {
2208 k9::assert_equal!(
2209 unwrapper(Command::parse("HELO example.com")),
2210 MaybePartialCommand::Full(Command::Helo(Domain::DomainName(
2211 "example.com".parse().unwrap()
2212 )))
2213 );
2214 }
2215
2216 #[test]
2217 fn test_helo_case_insensitive() {
2218 k9::assert_equal!(
2219 unwrapper(Command::parse("helo example.com")),
2220 MaybePartialCommand::Full(Command::Helo(Domain::DomainName(
2221 "example.com".parse().unwrap()
2222 )))
2223 );
2224 }
2225
2226 #[test]
2227 fn test_helo_ipv4_literal() {
2228 k9::assert_equal!(
2230 unwrapper(Command::parse("HELO [10.0.0.1]")),
2231 MaybePartialCommand::Full(Command::Helo(Domain::V4("10.0.0.1".parse().unwrap())))
2232 );
2233 }
2234
2235 #[test]
2236 fn test_helo_alone_is_partial() {
2237 k9::assert_equal!(
2238 unwrapper(Command::parse("HELO")),
2239 MaybePartialCommand::Partial {
2240 verb: CommandVerb::Helo,
2241 remainder: "".into(),
2242 reason: PartialReason::Syntax,
2243 }
2244 );
2245 }
2246
2247 #[test]
2248 fn test_helo_with_garbage_is_partial() {
2249 k9::assert_equal!(
2250 unwrapper(Command::parse("HELO !!invalid!!")),
2251 MaybePartialCommand::Partial {
2252 verb: CommandVerb::Helo,
2253 remainder: "!!invalid!!".into(),
2254 reason: PartialReason::Syntax,
2255 }
2256 );
2257 }
2258
2259 #[test]
2264 fn test_lhlo_domain_name() {
2265 k9::assert_equal!(
2266 unwrapper(Command::parse("LHLO example.com")),
2267 MaybePartialCommand::Full(Command::Lhlo(Domain::DomainName(
2268 "example.com".parse().unwrap()
2269 )))
2270 );
2271 }
2272
2273 #[test]
2274 fn test_lhlo_case_insensitive() {
2275 k9::assert_equal!(
2276 unwrapper(Command::parse("lhlo example.com")),
2277 MaybePartialCommand::Full(Command::Lhlo(Domain::DomainName(
2278 "example.com".parse().unwrap()
2279 )))
2280 );
2281 }
2282
2283 #[test]
2284 fn test_lhlo_ipv4_literal() {
2285 k9::assert_equal!(
2286 unwrapper(Command::parse("LHLO [10.0.0.1]")),
2287 MaybePartialCommand::Full(Command::Lhlo(Domain::V4("10.0.0.1".parse().unwrap())))
2288 );
2289 }
2290
2291 #[test]
2292 fn test_lhlo_ipv6_literal() {
2293 k9::assert_equal!(
2294 unwrapper(Command::parse("LHLO [IPv6:::1]")),
2295 MaybePartialCommand::Full(Command::Lhlo(Domain::V6("::1".parse().unwrap())))
2296 );
2297 }
2298
2299 #[test]
2300 fn test_lhlo_invalid_ipv4_is_partial() {
2301 k9::assert_equal!(
2302 unwrapper(Command::parse("LHLO [999.999.999.999]")),
2303 MaybePartialCommand::Partial {
2304 verb: CommandVerb::Lhlo,
2305 remainder: "[999.999.999.999]".into(),
2306 reason: PartialReason::Syntax,
2307 }
2308 );
2309 }
2310
2311 #[test]
2312 fn test_lhlo_alone_is_partial() {
2313 k9::assert_equal!(
2314 unwrapper(Command::parse("LHLO")),
2315 MaybePartialCommand::Partial {
2316 verb: CommandVerb::Lhlo,
2317 remainder: "".into(),
2318 reason: PartialReason::Syntax,
2319 }
2320 );
2321 }
2322
2323 #[test]
2328 fn test_auth_mechanism_only() {
2329 k9::assert_equal!(
2330 unwrapper(Command::parse("AUTH PLAIN")),
2331 MaybePartialCommand::Full(Command::Auth {
2332 sasl_mech: "PLAIN".into(),
2333 initial_response: None,
2334 })
2335 );
2336 }
2337
2338 #[test]
2339 fn test_auth_with_initial_response() {
2340 k9::assert_equal!(
2341 unwrapper(Command::parse("AUTH PLAIN dXNlcjpwYXNz")),
2342 MaybePartialCommand::Full(Command::Auth {
2343 sasl_mech: "PLAIN".into(),
2344 initial_response: Some("dXNlcjpwYXNz".into()),
2345 })
2346 );
2347 }
2348
2349 #[test]
2350 fn test_auth_empty_initial_response() {
2351 k9::assert_equal!(
2353 unwrapper(Command::parse("AUTH PLAIN =")),
2354 MaybePartialCommand::Full(Command::Auth {
2355 sasl_mech: "PLAIN".into(),
2356 initial_response: Some("=".into()),
2357 })
2358 );
2359 }
2360
2361 #[test]
2362 fn test_auth_hyphenated_mechanism() {
2363 k9::assert_equal!(
2364 unwrapper(Command::parse("AUTH CRAM-MD5")),
2365 MaybePartialCommand::Full(Command::Auth {
2366 sasl_mech: "CRAM-MD5".into(),
2367 initial_response: None,
2368 })
2369 );
2370 }
2371
2372 #[test]
2373 fn test_auth_case_insensitive() {
2374 k9::assert_equal!(
2375 unwrapper(Command::parse("auth plain")),
2376 MaybePartialCommand::Full(Command::Auth {
2377 sasl_mech: "plain".into(),
2378 initial_response: None,
2379 })
2380 );
2381 }
2382
2383 #[test]
2384 fn test_auth_alone_is_partial() {
2385 k9::assert_equal!(
2386 unwrapper(Command::parse("AUTH")),
2387 MaybePartialCommand::Partial {
2388 verb: CommandVerb::Auth,
2389 remainder: "".into(),
2390 reason: PartialReason::Syntax,
2391 }
2392 );
2393 }
2394
2395 #[test]
2396 fn test_auth_with_garbage_is_partial() {
2397 k9::assert_equal!(
2399 unwrapper(Command::parse("AUTH !!bad!!")),
2400 MaybePartialCommand::Partial {
2401 verb: CommandVerb::Auth,
2402 remainder: "!!bad!!".into(),
2403 reason: PartialReason::Syntax,
2404 }
2405 );
2406 }
2407
2408 #[test]
2413 fn test_xclient_single_param() {
2414 k9::assert_equal!(
2415 unwrapper(Command::parse("XCLIENT NAME=foo.example.com")),
2416 MaybePartialCommand::Full(Command::XClient(vec![XClientParameter {
2417 name: "NAME".into(),
2418 value: "foo.example.com".into(),
2419 }]))
2420 );
2421 }
2422
2423 #[test]
2424 fn test_xclient_multiple_params() {
2425 k9::assert_equal!(
2426 unwrapper(Command::parse("XCLIENT NAME=foo.example.com ADDR=10.0.0.1")),
2427 MaybePartialCommand::Full(Command::XClient(vec![
2428 XClientParameter {
2429 name: "NAME".into(),
2430 value: "foo.example.com".into(),
2431 },
2432 XClientParameter {
2433 name: "ADDR".into(),
2434 value: "10.0.0.1".into(),
2435 },
2436 ]))
2437 );
2438 }
2439
2440 #[test]
2441 fn test_xclient_xtext_hex_escape() {
2442 k9::assert_equal!(
2444 unwrapper(Command::parse("XCLIENT NAME=user+40example.com")),
2445 MaybePartialCommand::Full(Command::XClient(vec![XClientParameter {
2446 name: "NAME".into(),
2447 value: "user@example.com".into(),
2448 }]))
2449 );
2450 }
2451
2452 #[test]
2453 fn test_xclient_case_insensitive() {
2454 k9::assert_equal!(
2455 unwrapper(Command::parse("xclient NAME=host")),
2456 MaybePartialCommand::Full(Command::XClient(vec![XClientParameter {
2457 name: "NAME".into(),
2458 value: "host".into(),
2459 }]))
2460 );
2461 }
2462
2463 #[test]
2464 fn test_xclient_alone_is_partial() {
2465 k9::assert_equal!(
2466 unwrapper(Command::parse("XCLIENT")),
2467 MaybePartialCommand::Partial {
2468 verb: CommandVerb::XClient,
2469 remainder: "".into(),
2470 reason: PartialReason::Syntax,
2471 }
2472 );
2473 }
2474
2475 #[test]
2476 fn test_xclient_invalid_xtext_is_partial() {
2477 k9::assert_equal!(
2479 unwrapper(Command::parse("XCLIENT NAME=bad+ZZ")),
2480 MaybePartialCommand::Partial {
2481 verb: CommandVerb::XClient,
2482 remainder: "NAME=bad+ZZ".into(),
2483 reason: PartialReason::Syntax,
2484 }
2485 );
2486 }
2487
2488 #[test]
2489 fn test_xclient_garbage_is_partial() {
2490 k9::assert_equal!(
2492 unwrapper(Command::parse("XCLIENT noequals")),
2493 MaybePartialCommand::Partial {
2494 verb: CommandVerb::XClient,
2495 remainder: "noequals".into(),
2496 reason: PartialReason::Syntax,
2497 }
2498 );
2499 }
2500
2501 #[test]
2502 fn test_xclient_parameter_methods() {
2503 let cmd = unwrapper(Command::parse("XCLIENT ADDR=192.168.1.1"));
2505 let params = match cmd {
2506 MaybePartialCommand::Full(Command::XClient(params)) => params,
2507 _ => panic!("Expected XCLIENT command"),
2508 };
2509
2510 k9::assert_equal!(params[0].is_name("ADDR"), true);
2512 k9::assert_equal!(params[0].is_name("addr"), true);
2513 k9::assert_equal!(params[0].is_name("NAME"), false);
2514
2515 let ip: std::net::IpAddr = params[0].parse().expect("Failed to parse IP address");
2517 k9::assert_equal!(
2518 ip,
2519 std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 1, 1))
2520 );
2521 }
2522
2523 #[test]
2524 fn test_xclient_parameter_parse_invalid_ip() {
2525 let cmd = unwrapper(Command::parse("XCLIENT ADDR=not-an-ip"));
2527 let params = match cmd {
2528 MaybePartialCommand::Full(Command::XClient(params)) => params,
2529 _ => panic!("Expected XCLIENT command"),
2530 };
2531
2532 let result: Result<std::net::IpAddr, _> = params[0].parse();
2533 k9::assert_equal!(result.unwrap_err(), "invalid IP address syntax");
2534 }
2535
2536 fn mail_path(local: &str, domain: Domain) -> ReversePath {
2541 Mailbox {
2542 local_part: local.into(),
2543 domain,
2544 }
2545 .into()
2546 }
2547
2548 #[test]
2549 fn test_mail_from_domain_name() {
2550 k9::assert_equal!(
2551 unwrapper(Command::parse("MAIL FROM:<user@host>")),
2552 MaybePartialCommand::Full(Command::MailFrom {
2553 address: mail_path("user", Domain::DomainName("host".parse().unwrap())),
2554 parameters: vec![],
2555 })
2556 );
2557 k9::assert_equal!(
2559 unwrapper(Command::parse("mail from:<user@host>")),
2560 MaybePartialCommand::Full(Command::MailFrom {
2561 address: mail_path("user", Domain::DomainName("host".parse().unwrap())),
2562 parameters: vec![],
2563 })
2564 );
2565 }
2566
2567 #[test]
2568 fn test_mail_from_null_sender() {
2569 k9::assert_equal!(
2570 unwrapper(Command::parse("MAIL FROM:<>")),
2571 MaybePartialCommand::Full(Command::MailFrom {
2572 address: ReversePath::NullSender,
2573 parameters: vec![],
2574 })
2575 );
2576 }
2577
2578 #[test]
2579 fn test_mail_from_ipv4() {
2580 k9::assert_equal!(
2581 unwrapper(Command::parse("MAIL FROM:<user@[10.0.0.1]>")),
2582 MaybePartialCommand::Full(Command::MailFrom {
2583 address: mail_path("user", Domain::V4("10.0.0.1".parse().unwrap())),
2584 parameters: vec![],
2585 })
2586 );
2587 }
2588
2589 #[test]
2590 fn test_mail_from_ipv6() {
2591 k9::assert_equal!(
2592 unwrapper(Command::parse("MAIL FROM:<user@[IPv6:::1]>")),
2593 MaybePartialCommand::Full(Command::MailFrom {
2594 address: mail_path("user", Domain::V6("::1".parse().unwrap())),
2595 parameters: vec![],
2596 })
2597 );
2598 }
2599
2600 #[test]
2601 fn test_mail_from_tagged_literal() {
2602 k9::assert_equal!(
2603 unwrapper(Command::parse("MAIL FROM:<user@[future:something]>")),
2604 MaybePartialCommand::Full(Command::MailFrom {
2605 address: mail_path("user", Domain::Tagged("future:something".into())),
2606 parameters: vec![],
2607 })
2608 );
2609 }
2610
2611 #[test]
2612 fn test_mail_from_esmtp_params() {
2613 k9::assert_equal!(
2614 unwrapper(Command::parse("MAIL FROM:<user@host> foo bar=baz")),
2615 MaybePartialCommand::Full(Command::MailFrom {
2616 address: mail_path("user", Domain::DomainName("host".parse().unwrap())),
2617 parameters: vec![
2618 EsmtpParameter {
2619 name: "foo".into(),
2620 value: None,
2621 },
2622 EsmtpParameter {
2623 name: "bar".into(),
2624 value: Some("baz".into()),
2625 },
2626 ],
2627 })
2628 );
2629 }
2630
2631 #[test]
2632 fn test_mail_from_bare_address() {
2633 k9::assert_equal!(
2635 unwrapper(Command::parse("MAIL FROM:user@host")),
2636 MaybePartialCommand::Full(Command::MailFrom {
2637 address: mail_path("user", Domain::DomainName("host".parse().unwrap())),
2638 parameters: vec![],
2639 })
2640 );
2641 }
2642
2643 #[test]
2644 fn test_mail_from_at_domain_list() {
2645 k9::assert_equal!(
2646 unwrapper(Command::parse(
2647 "MAIL FROM:<@hosta.int,@jkl.org:userc@d.bar.org>"
2648 )),
2649 MaybePartialCommand::Full(Command::MailFrom {
2650 address: ReversePath::Path(MailPath {
2651 at_domain_list: vec!["hosta.int".into(), "jkl.org".into()],
2652 mailbox: Mailbox {
2653 local_part: "userc".into(),
2654 domain: Domain::DomainName("d.bar.org".parse().unwrap()),
2655 },
2656 }),
2657 parameters: vec![],
2658 })
2659 );
2660 }
2661
2662 #[test]
2663 fn test_mail_from_invalid_ipv4_is_partial() {
2664 k9::assert_equal!(
2666 unwrapper(Command::parse("MAIL FROM:<user@[999.999.999.999]>")),
2667 MaybePartialCommand::Partial {
2668 verb: CommandVerb::Mail,
2669 remainder: "<user@[999.999.999.999]>".into(),
2670 reason: PartialReason::InvalidSenderAddress,
2671 }
2672 );
2673 }
2674
2675 #[test]
2676 fn test_mail_from_invalid_ipv6_is_partial() {
2677 k9::assert_equal!(
2679 unwrapper(Command::parse("MAIL FROM:<user@[IPv6:not-an-ipv6]>")),
2680 MaybePartialCommand::Partial {
2681 verb: CommandVerb::Mail,
2682 remainder: "<user@[IPv6:not-an-ipv6]>".into(),
2683 reason: PartialReason::InvalidSenderAddress,
2684 }
2685 );
2686 }
2687
2688 #[test]
2689 fn test_mail_alone_is_partial() {
2690 k9::assert_equal!(
2691 unwrapper(Command::parse("MAIL")),
2692 MaybePartialCommand::Partial {
2693 verb: CommandVerb::Mail,
2694 remainder: "".into(),
2695 reason: PartialReason::Syntax,
2696 }
2697 );
2698 }
2699
2700 #[test]
2701 fn test_mail_with_garbage_is_partial() {
2702 k9::assert_equal!(
2703 unwrapper(Command::parse("MAIL garbage")),
2704 MaybePartialCommand::Partial {
2705 verb: CommandVerb::Mail,
2706 remainder: "garbage".into(),
2707 reason: PartialReason::Syntax,
2708 }
2709 );
2710 }
2711
2712 fn rcpt_path(local: &str, domain: Domain) -> ForwardPath {
2717 Mailbox {
2718 local_part: local.into(),
2719 domain,
2720 }
2721 .into()
2722 }
2723
2724 #[test]
2725 fn test_rcpt_to_domain_name() {
2726 k9::assert_equal!(
2727 unwrapper(Command::parse("RCPT TO:<user@host>")),
2728 MaybePartialCommand::Full(Command::RcptTo {
2729 address: rcpt_path("user", Domain::DomainName("host".parse().unwrap())),
2730 parameters: vec![],
2731 })
2732 );
2733 }
2734
2735 #[test]
2736 fn test_rcpt_to_case_insensitive() {
2737 k9::assert_equal!(
2739 unwrapper(Command::parse("rcpt to:<user@host>")),
2740 MaybePartialCommand::Full(Command::RcptTo {
2741 address: rcpt_path("user", Domain::DomainName("host".parse().unwrap())),
2742 parameters: vec![],
2743 })
2744 );
2745 }
2746
2747 #[test]
2748 fn test_rcpt_to_postmaster() {
2749 k9::assert_equal!(
2750 unwrapper(Command::parse("RCPT TO:<Postmaster>")),
2751 MaybePartialCommand::Full(Command::RcptTo {
2752 address: ForwardPath::Postmaster,
2753 parameters: vec![],
2754 })
2755 );
2756 }
2757
2758 #[test]
2759 fn test_rcpt_to_postmaster_lowercase() {
2760 k9::assert_equal!(
2762 unwrapper(Command::parse("RCPT TO:<postmaster>")),
2763 MaybePartialCommand::Full(Command::RcptTo {
2764 address: ForwardPath::Postmaster,
2765 parameters: vec![],
2766 })
2767 );
2768 }
2769
2770 #[test]
2771 fn test_rcpt_to_ipv4() {
2772 k9::assert_equal!(
2773 unwrapper(Command::parse("RCPT TO:<user@[10.0.0.1]>")),
2774 MaybePartialCommand::Full(Command::RcptTo {
2775 address: rcpt_path("user", Domain::V4("10.0.0.1".parse().unwrap())),
2776 parameters: vec![],
2777 })
2778 );
2779 }
2780
2781 #[test]
2782 fn test_rcpt_to_ipv6() {
2783 k9::assert_equal!(
2784 unwrapper(Command::parse("RCPT TO:<user@[IPv6:::1]>")),
2785 MaybePartialCommand::Full(Command::RcptTo {
2786 address: rcpt_path("user", Domain::V6("::1".parse().unwrap())),
2787 parameters: vec![],
2788 })
2789 );
2790 }
2791
2792 #[test]
2793 fn test_rcpt_to_tagged_literal() {
2794 k9::assert_equal!(
2795 unwrapper(Command::parse("RCPT TO:<user@[future:something]>")),
2796 MaybePartialCommand::Full(Command::RcptTo {
2797 address: rcpt_path("user", Domain::Tagged("future:something".into())),
2798 parameters: vec![],
2799 })
2800 );
2801 }
2802
2803 #[test]
2804 fn test_rcpt_to_esmtp_params() {
2805 k9::assert_equal!(
2806 unwrapper(Command::parse("RCPT TO:<user@host> foo bar=baz")),
2807 MaybePartialCommand::Full(Command::RcptTo {
2808 address: rcpt_path("user", Domain::DomainName("host".parse().unwrap())),
2809 parameters: vec![
2810 EsmtpParameter {
2811 name: "foo".into(),
2812 value: None,
2813 },
2814 EsmtpParameter {
2815 name: "bar".into(),
2816 value: Some("baz".into()),
2817 },
2818 ],
2819 })
2820 );
2821 }
2822
2823 #[test]
2824 fn test_rcpt_to_bare_address() {
2825 k9::assert_equal!(
2827 unwrapper(Command::parse("RCPT TO:user@host")),
2828 MaybePartialCommand::Full(Command::RcptTo {
2829 address: rcpt_path("user", Domain::DomainName("host".parse().unwrap())),
2830 parameters: vec![],
2831 })
2832 );
2833 }
2834
2835 #[test]
2836 fn test_rcpt_to_at_domain_list() {
2837 k9::assert_equal!(
2838 unwrapper(Command::parse(
2839 "RCPT TO:<@hosta.int,@jkl.org:userc@d.bar.org>"
2840 )),
2841 MaybePartialCommand::Full(Command::RcptTo {
2842 address: ForwardPath::Path(MailPath {
2843 at_domain_list: vec!["hosta.int".into(), "jkl.org".into()],
2844 mailbox: Mailbox {
2845 local_part: "userc".into(),
2846 domain: Domain::DomainName("d.bar.org".parse().unwrap()),
2847 },
2848 }),
2849 parameters: vec![],
2850 })
2851 );
2852 }
2853
2854 #[test]
2855 fn test_rcpt_to_invalid_ipv4_is_partial() {
2856 k9::assert_equal!(
2858 unwrapper(Command::parse("RCPT TO:<user@[999.999.999.999]>")),
2859 MaybePartialCommand::Partial {
2860 verb: CommandVerb::Rcpt,
2861 remainder: "<user@[999.999.999.999]>".into(),
2862 reason: PartialReason::InvalidRecipientAddress,
2863 }
2864 );
2865 }
2866
2867 #[test]
2868 fn test_rcpt_to_invalid_ipv6_is_partial() {
2869 k9::assert_equal!(
2871 unwrapper(Command::parse("RCPT TO:<user@[IPv6:not-an-ipv6]>")),
2872 MaybePartialCommand::Partial {
2873 verb: CommandVerb::Rcpt,
2874 remainder: "<user@[IPv6:not-an-ipv6]>".into(),
2875 reason: PartialReason::InvalidRecipientAddress,
2876 }
2877 );
2878 }
2879
2880 #[test]
2881 fn test_rcpt_alone_is_partial() {
2882 k9::assert_equal!(
2883 unwrapper(Command::parse("RCPT")),
2884 MaybePartialCommand::Partial {
2885 verb: CommandVerb::Rcpt,
2886 remainder: "".into(),
2887 reason: PartialReason::Syntax,
2888 }
2889 );
2890 }
2891
2892 #[test]
2893 fn test_rcpt_with_garbage_is_partial() {
2894 k9::assert_equal!(
2895 unwrapper(Command::parse("RCPT garbage")),
2896 MaybePartialCommand::Partial {
2897 verb: CommandVerb::Rcpt,
2898 remainder: "garbage".into(),
2899 reason: PartialReason::Syntax,
2900 }
2901 );
2902 }
2903
2904 #[test]
2905 fn test_rcpt_to_invalid_address_syntax() {
2906 k9::assert_equal!(
2908 unwrapper(Command::parse("RCPT TO:<not an address>")),
2909 MaybePartialCommand::Partial {
2910 verb: CommandVerb::Rcpt,
2911 remainder: "<not an address>".into(),
2912 reason: PartialReason::InvalidRecipientAddress,
2913 }
2914 );
2915 }
2916
2917 #[test]
2918 fn test_rcpt_to_valid_address_bad_params() {
2919 k9::assert_equal!(
2921 unwrapper(Command::parse("RCPT TO:<valid@example.com> !!!garbage")),
2922 MaybePartialCommand::Partial {
2923 verb: CommandVerb::Rcpt,
2924 remainder: "!!!garbage".into(),
2925 reason: PartialReason::Syntax,
2926 }
2927 );
2928 }
2929
2930 #[test]
2931 fn test_rcpt_to_bad_address_with_params_is_invalid_address() {
2932 k9::assert_equal!(
2934 unwrapper(Command::parse("RCPT TO:<bad address> NOTIFY=SUCCESS")),
2935 MaybePartialCommand::Partial {
2936 verb: CommandVerb::Rcpt,
2937 remainder: "<bad address> NOTIFY=SUCCESS".into(),
2938 reason: PartialReason::InvalidRecipientAddress,
2939 }
2940 );
2941 }
2942
2943 #[test]
2944 fn test_mail_from_invalid_address_syntax() {
2945 k9::assert_equal!(
2947 unwrapper(Command::parse("MAIL FROM:<not an address>")),
2948 MaybePartialCommand::Partial {
2949 verb: CommandVerb::Mail,
2950 remainder: "<not an address>".into(),
2951 reason: PartialReason::InvalidSenderAddress,
2952 }
2953 );
2954 }
2955
2956 #[test]
2957 fn test_mail_from_valid_address_bad_params() {
2958 k9::assert_equal!(
2960 unwrapper(Command::parse("MAIL FROM:<valid@example.com> !!!garbage")),
2961 MaybePartialCommand::Partial {
2962 verb: CommandVerb::Mail,
2963 remainder: "!!!garbage".into(),
2964 reason: PartialReason::Syntax,
2965 }
2966 );
2967 }
2968
2969 #[test]
2970 fn test_mail_from_null_sender_bad_params() {
2971 k9::assert_equal!(
2973 unwrapper(Command::parse("MAIL FROM:<> !!!garbage")),
2974 MaybePartialCommand::Partial {
2975 verb: CommandVerb::Mail,
2976 remainder: "!!!garbage".into(),
2977 reason: PartialReason::Syntax,
2978 }
2979 );
2980 }
2981
2982 #[test]
2983 fn test_mail_from_bad_address_with_params_is_invalid_address() {
2984 k9::assert_equal!(
2986 unwrapper(Command::parse("MAIL FROM:<bad address> SIZE=1000")),
2987 MaybePartialCommand::Partial {
2988 verb: CommandVerb::Mail,
2989 remainder: "<bad address> SIZE=1000".into(),
2990 reason: PartialReason::InvalidSenderAddress,
2991 }
2992 );
2993 }
2994
2995 fn assert_encode(input: &str, expected: &str) {
3003 let cmd = match unwrapper(Command::parse(input)) {
3004 MaybePartialCommand::Full(c) => c,
3005 other => panic!("expected Full, got {other:?}"),
3006 };
3007 let encoded = cmd.encode();
3008 k9::assert_equal!(encoded, BString::from(expected));
3009 k9::assert_equal!(
3011 unwrapper(Command::parse(encoded.clone())),
3012 MaybePartialCommand::Full(cmd)
3013 );
3014 }
3015
3016 #[test]
3017 fn test_encode_ehlo_domain() {
3018 assert_encode("EHLO example.com", "EHLO example.com\r\n");
3019 }
3020
3021 #[test]
3022 fn test_encode_ehlo_ipv4() {
3023 assert_encode("EHLO [10.0.0.1]", "EHLO [10.0.0.1]\r\n");
3024 }
3025
3026 #[test]
3027 fn test_encode_ehlo_ipv6() {
3028 assert_encode("EHLO [IPv6:::1]", "EHLO [IPv6:::1]\r\n");
3029 }
3030
3031 #[test]
3032 fn test_encode_ehlo_tagged() {
3033 assert_encode("EHLO [future:something]", "EHLO [future:something]\r\n");
3034 }
3035
3036 #[test]
3037 fn test_encode_helo() {
3038 assert_encode("HELO mail.example.com", "HELO mail.example.com\r\n");
3039 }
3040
3041 #[test]
3042 fn test_encode_lhlo() {
3043 assert_encode("LHLO mail.example.com", "LHLO mail.example.com\r\n");
3044 }
3045
3046 #[test]
3047 fn test_encode_noop_none() {
3048 assert_encode("NOOP", "NOOP\r\n");
3049 }
3050
3051 #[test]
3052 fn test_encode_noop_some() {
3053 assert_encode("NOOP something", "NOOP something\r\n");
3054 }
3055
3056 #[test]
3057 fn test_encode_help_none() {
3058 assert_encode("HELP", "HELP\r\n");
3059 }
3060
3061 #[test]
3062 fn test_encode_help_some() {
3063 assert_encode("HELP MAIL", "HELP MAIL\r\n");
3064 }
3065
3066 #[test]
3067 fn test_encode_vrfy_none() {
3068 assert_encode("VRFY", "VRFY\r\n");
3069 }
3070
3071 #[test]
3072 fn test_encode_vrfy_some() {
3073 assert_encode("VRFY user", "VRFY user\r\n");
3074 }
3075
3076 #[test]
3077 fn test_encode_expn_none() {
3078 assert_encode("EXPN", "EXPN\r\n");
3079 }
3080
3081 #[test]
3082 fn test_encode_expn_some() {
3083 assert_encode("EXPN list", "EXPN list\r\n");
3084 }
3085
3086 #[test]
3087 fn test_encode_data() {
3088 assert_encode("DATA", "DATA\r\n");
3089 }
3090
3091 #[test]
3092 fn test_encode_data_dot() {
3093 k9::assert_equal!(Command::DataDot.encode(), BString::from(".\r\n"));
3096 }
3097
3098 #[test]
3099 fn test_encode_rset() {
3100 assert_encode("RSET", "RSET\r\n");
3101 }
3102
3103 #[test]
3104 fn test_encode_quit() {
3105 assert_encode("QUIT", "QUIT\r\n");
3106 }
3107
3108 #[test]
3109 fn test_encode_starttls() {
3110 assert_encode("STARTTLS", "STARTTLS\r\n");
3111 }
3112
3113 #[test]
3114 fn test_encode_mail_from_path() {
3115 assert_encode(
3116 "MAIL FROM:<user@example.com>",
3117 "MAIL FROM:<user@example.com>\r\n",
3118 );
3119 }
3120
3121 #[test]
3122 fn test_encode_mail_from_null_sender() {
3123 assert_encode("MAIL FROM:<>", "MAIL FROM:<>\r\n");
3124 }
3125
3126 #[test]
3127 fn test_encode_mail_from_params() {
3128 assert_encode(
3129 "MAIL FROM:<user@example.com> SIZE=1000 BODY=8BITMIME",
3130 "MAIL FROM:<user@example.com> SIZE=1000 BODY=8BITMIME\r\n",
3131 );
3132 }
3133
3134 #[test]
3135 fn test_encode_mail_from_ipv4() {
3136 assert_encode(
3137 "MAIL FROM:<user@[10.0.0.1]>",
3138 "MAIL FROM:<user@[10.0.0.1]>\r\n",
3139 );
3140 }
3141
3142 #[test]
3143 fn test_encode_mail_from_ipv6() {
3144 assert_encode(
3145 "MAIL FROM:<user@[IPv6:::1]>",
3146 "MAIL FROM:<user@[IPv6:::1]>\r\n",
3147 );
3148 }
3149
3150 #[test]
3151 fn test_encode_rcpt_to_path() {
3152 assert_encode(
3153 "RCPT TO:<user@example.com>",
3154 "RCPT TO:<user@example.com>\r\n",
3155 );
3156 }
3157
3158 #[test]
3159 fn test_encode_rcpt_to_postmaster() {
3160 let cmd = Command::RcptTo {
3162 address: ForwardPath::Postmaster,
3163 parameters: vec![],
3164 };
3165 k9::assert_equal!(cmd.encode(), BString::from("RCPT TO:<Postmaster>\r\n"));
3166 k9::assert_equal!(
3167 unwrapper(Command::parse(cmd.encode())),
3168 MaybePartialCommand::Full(cmd)
3169 );
3170 }
3171
3172 #[test]
3173 fn test_encode_rcpt_to_params() {
3174 assert_encode(
3175 "RCPT TO:<user@example.com> NOTIFY=SUCCESS",
3176 "RCPT TO:<user@example.com> NOTIFY=SUCCESS\r\n",
3177 );
3178 }
3179
3180 #[test]
3181 fn test_encode_auth_no_response() {
3182 assert_encode("AUTH PLAIN", "AUTH PLAIN\r\n");
3183 }
3184
3185 #[test]
3186 fn test_encode_auth_with_response() {
3187 assert_encode("AUTH PLAIN dXNlcjpwYXNz", "AUTH PLAIN dXNlcjpwYXNz\r\n");
3188 }
3189
3190 #[test]
3191 fn test_encode_xclient_single() {
3192 assert_encode(
3193 "XCLIENT NAME=foo.example.com",
3194 "XCLIENT NAME=foo.example.com\r\n",
3195 );
3196 }
3197
3198 #[test]
3199 fn test_encode_xclient_multiple() {
3200 assert_encode(
3201 "XCLIENT NAME=foo.example.com ADDR=10.0.0.1",
3202 "XCLIENT NAME=foo.example.com ADDR=10.0.0.1\r\n",
3203 );
3204 }
3205
3206 #[test]
3207 fn test_encode_xclient_xtext_roundtrip() {
3208 assert_encode(
3212 "XCLIENT NAME=user+40example.com",
3213 "XCLIENT NAME=user@example.com\r\n",
3214 );
3215 }
3216
3217 #[test]
3218 fn test_encode_unknown() {
3219 assert_encode("FOOBAR some args", "FOOBAR some args\r\n");
3220 }
3221
3222 #[test]
3223 fn test_encode_unknown_bare_verb() {
3224 assert_encode("FOOBAR", "FOOBAR\r\n");
3225 }
3226
3227 #[test]
3228 fn test_encode_mail_from_source_route_dropped() {
3229 let parsed = unwrapper(Command::parse("MAIL FROM:<@route.example.com:user@host>"));
3233 let cmd = match parsed {
3234 MaybePartialCommand::Full(c) => c,
3235 other => panic!("expected Full, got {other:?}"),
3236 };
3237 let encoded = cmd.encode();
3238 k9::assert_equal!(encoded, BString::from("MAIL FROM:<user@host>\r\n"));
3239 let expected = Mailbox {
3241 local_part: "user".into(),
3242 domain: Domain::DomainName("host".parse().unwrap()),
3243 };
3244 k9::assert_equal!(
3245 unwrapper(Command::parse(encoded)),
3246 MaybePartialCommand::Full(Command::MailFrom {
3247 address: expected.into(),
3248 parameters: vec![],
3249 })
3250 );
3251 }
3252
3253 fn parse_full(input: &str) -> Command {
3259 match unwrapper(Command::parse(input)) {
3260 MaybePartialCommand::Full(c) => c,
3261 other => panic!("expected Full, got {other:?}"),
3262 }
3263 }
3264
3265 #[test]
3268 fn test_mail_from_utf8_local_part() {
3269 let cmd = parse_full("MAIL FROM:<ü@example.com>");
3271 let mailbox = match cmd {
3272 Command::MailFrom {
3273 address: ReversePath::Path(path),
3274 ..
3275 } => path.mailbox,
3276 other => panic!("unexpected {other:?}"),
3277 };
3278 k9::assert_equal!(mailbox.local_part, String::from("ü"));
3279 k9::assert_equal!(
3280 mailbox.domain,
3281 Domain::DomainName("example.com".parse().unwrap())
3282 );
3283 }
3284
3285 #[test]
3286 fn test_mail_from_utf8_local_part_roundtrip() {
3287 let input = "MAIL FROM:<ü@example.com>";
3290 let cmd = parse_full(input);
3291 k9::assert_equal!(
3292 unwrapper(Command::parse(cmd.encode())),
3293 MaybePartialCommand::Full(cmd)
3294 );
3295 }
3296
3297 #[test]
3298 fn test_mail_from_quoted_utf8_local_part() {
3299 let cmd = parse_full("MAIL FROM:<\"ü\"@example.com>");
3301 let mailbox = match cmd {
3302 Command::MailFrom {
3303 address: ReversePath::Path(path),
3304 ..
3305 } => path.mailbox,
3306 other => panic!("unexpected {other:?}"),
3307 };
3308 k9::assert_equal!(mailbox.local_part, String::from("\"ü\""));
3310 }
3311
3312 #[test]
3313 fn test_rcpt_to_utf8_local_part() {
3314 let cmd = parse_full("RCPT TO:<用户@example.com>");
3316 let mailbox = match cmd {
3317 Command::RcptTo {
3318 address: ForwardPath::Path(path),
3319 ..
3320 } => path.mailbox,
3321 other => panic!("unexpected {other:?}"),
3322 };
3323 k9::assert_equal!(mailbox.local_part, String::from("用户"));
3324 }
3325
3326 #[test]
3329 fn test_mail_from_u_label_domain() {
3330 let cmd = parse_full("MAIL FROM:<user@münchen.de>");
3332 let domain = match cmd {
3333 Command::MailFrom {
3334 address: ReversePath::Path(path),
3335 ..
3336 } => path.mailbox.domain,
3337 other => panic!("unexpected {other:?}"),
3338 };
3339 match domain {
3340 Domain::DomainName(ref s) => {
3341 k9::assert_equal!(s.as_str(), "xn--mnchen-3ya.de");
3342 }
3343 other => panic!("unexpected domain variant {other:?}"),
3344 }
3345 }
3346
3347 #[test]
3348 fn test_mail_from_u_label_domain_encode() {
3349 let cmd = parse_full("MAIL FROM:<user@münchen.de>");
3352 k9::assert_equal!(
3353 cmd.encode(),
3354 BString::from("MAIL FROM:<user@xn--mnchen-3ya.de>\r\n")
3355 );
3356 }
3357
3358 #[test]
3359 fn test_ehlo_u_label_domain() {
3360 let cmd = parse_full("EHLO münchen.de");
3362 match cmd {
3363 Command::Ehlo(Domain::DomainName(ref s)) => {
3364 k9::assert_equal!(s.as_str(), "xn--mnchen-3ya.de");
3365 }
3366 other => panic!("unexpected {other:?}"),
3367 }
3368 }
3369
3370 #[test]
3371 fn test_ehlo_u_label_domain_encode() {
3372 let cmd = parse_full("EHLO münchen.de");
3374 k9::assert_equal!(cmd.encode(), BString::from("EHLO xn--mnchen-3ya.de\r\n"));
3375 }
3376
3377 #[test]
3380 fn test_esmtp_value_utf8() {
3381 let cmd = parse_full("MAIL FROM:<u@h> PARAM=valüe");
3383 let params = match cmd {
3384 Command::MailFrom { parameters, .. } => parameters,
3385 other => panic!("unexpected {other:?}"),
3386 };
3387 k9::assert_equal!(params.len(), 1);
3388 k9::assert_equal!(params[0].name, "PARAM");
3389 k9::assert_equal!(params[0].value, Some("valüe".to_string()));
3390 }
3391
3392 #[test]
3393 fn test_esmtp_value_utf8_roundtrip() {
3394 let input = "MAIL FROM:<u@h> PARAM=valüe";
3396 let cmd = parse_full(input);
3397 k9::assert_equal!(
3398 cmd.encode(),
3399 BString::from("MAIL FROM:<u@h> PARAM=valüe\r\n")
3400 );
3401 k9::assert_equal!(
3402 unwrapper(Command::parse(cmd.encode())),
3403 MaybePartialCommand::Full(parse_full(input))
3404 );
3405 }
3406
3407 #[test]
3408 fn test_esmtp_value_utf8_only() {
3409 let cmd = parse_full("MAIL FROM:<u@h> X=ünïcödé");
3411 let params = match cmd {
3412 Command::MailFrom { parameters, .. } => parameters,
3413 other => panic!("unexpected {other:?}"),
3414 };
3415 k9::assert_equal!(params[0].name, "X");
3416 k9::assert_equal!(params[0].value, Some("ünïcödé".to_string()));
3417 }
3418
3419 #[test]
3422 fn test_reverse_path_null_sender_try_into_mailpath_err() {
3423 use core::convert::TryInto;
3424 let null_sender = ReversePath::NullSender;
3425 let err: &'static str =
3426 <ReversePath as TryInto<MailPath>>::try_into(null_sender).unwrap_err();
3427 k9::assert_equal!(err, "Cannot convert NullSender to MailPath");
3428 }
3429
3430 #[test]
3431 fn test_forward_path_postmaster_try_into_mailpath_err() {
3432 use core::convert::TryInto;
3433 let postmaster = ForwardPath::Postmaster;
3434 let err: &'static str =
3435 <ForwardPath as TryInto<MailPath>>::try_into(postmaster).unwrap_err();
3436 k9::assert_equal!(err, "Cannot convert Postmaster to MailPath");
3437 }
3438
3439 #[test]
3440 fn test_reverse_path_try_from_null_sender_err() {
3441 let null_sender = ReversePath::NullSender;
3442 let err = MailPath::try_from(null_sender).unwrap_err();
3443 k9::assert_equal!(err, "Cannot convert NullSender to MailPath");
3444 }
3445
3446 #[test]
3447 fn test_forward_path_try_from_postmaster_err() {
3448 let postmaster = ForwardPath::Postmaster;
3449 let err = MailPath::try_from(postmaster).unwrap_err();
3450 k9::assert_equal!(err, "Cannot convert Postmaster to MailPath");
3451 }
3452
3453 #[test]
3456 fn test_envelope_address_try_from_mailbox() {
3457 let mailbox = Mailbox {
3458 local_part: String::from("user"),
3459 domain: Domain::DomainName("example.com".parse().unwrap()),
3460 };
3461 let addr = EnvelopeAddress::from(mailbox);
3462 match addr {
3463 EnvelopeAddress::Path(path) => {
3464 k9::assert_equal!(path.mailbox.local_part(), "user");
3465 k9::assert_equal!(
3466 path.mailbox.domain,
3467 Domain::DomainName("example.com".parse().unwrap())
3468 );
3469 }
3470 _ => panic!("Expected Path variant"),
3471 }
3472 }
3473
3474 #[test]
3475 fn test_envelope_address_try_from_null_err() {
3476 let addr = EnvelopeAddress::Null;
3477 let err = Mailbox::try_from(addr).unwrap_err();
3478 k9::assert_equal!(err, "Cannot convert Null to Mailbox");
3479 }
3480
3481 #[test]
3482 fn test_envelope_address_try_from_postmaster_err() {
3483 let addr = EnvelopeAddress::Postmaster;
3484 let err = Mailbox::try_from(addr).unwrap_err();
3485 k9::assert_equal!(err, "Cannot convert Postmaster to Mailbox");
3486 }
3487
3488 #[test]
3489 fn test_envelope_address_try_from_null_to_reverse_path() {
3490 let addr = EnvelopeAddress::Null;
3491 let result = ReversePath::try_from(addr).unwrap();
3492 k9::assert_equal!(result, ReversePath::NullSender);
3493 }
3494
3495 #[test]
3496 fn test_envelope_address_try_from_postmaster_to_forward_path() {
3497 let addr = EnvelopeAddress::Postmaster;
3498 let result = ForwardPath::try_from(addr).unwrap();
3499 k9::assert_equal!(result, ForwardPath::Postmaster);
3500 }
3501
3502 #[test]
3505 fn test_envelope_address_try_into_mailpath_null_err() {
3506 let addr = EnvelopeAddress::Null;
3507 let err = MailPath::try_from(addr).unwrap_err();
3508 k9::assert_equal!(err, "Cannot convert Null to MailPath");
3509 }
3510
3511 #[test]
3512 fn test_envelope_address_try_into_mailpath_postmaster_err() {
3513 let addr = EnvelopeAddress::Postmaster;
3514 let err = MailPath::try_from(addr).unwrap_err();
3515 k9::assert_equal!(err, "Cannot convert Postmaster to MailPath");
3516 }
3517
3518 #[test]
3519 fn test_reverse_path_try_into_forward_path_null_sender_err() {
3520 let rp = ReversePath::NullSender;
3521 let err = ForwardPath::try_from(rp).unwrap_err();
3522 k9::assert_equal!(err, "Cannot convert NullSender to ForwardPath");
3523 }
3524
3525 #[test]
3526 fn test_forward_path_try_into_reverse_path_postmaster_err() {
3527 let fp = ForwardPath::Postmaster;
3528 let err = ReversePath::try_from(fp).unwrap_err();
3529 k9::assert_equal!(err, "Cannot convert Postmaster to ReversePath");
3530 }
3531
3532 #[test]
3535 fn test_mailbox_into_mailpath() {
3536 let mailbox = Mailbox {
3537 local_part: String::from("user"),
3538 domain: Domain::DomainName("example.com".parse().unwrap()),
3539 };
3540 let path: MailPath = mailbox.into();
3541 assert!(path.at_domain_list.is_empty());
3542 k9::assert_equal!(path.mailbox.local_part(), "user");
3543 }
3544
3545 #[test]
3546 fn test_mailbox_into_envelope_address() {
3547 let mailbox = Mailbox {
3548 local_part: String::from("user"),
3549 domain: Domain::DomainName("example.com".parse().unwrap()),
3550 };
3551 let addr: EnvelopeAddress = mailbox.into();
3552 match addr {
3553 EnvelopeAddress::Path(path) => {
3554 k9::assert_equal!(path.mailbox.local_part(), "user");
3555 }
3556 _ => panic!("Expected Path variant"),
3557 }
3558 }
3559
3560 #[test]
3561 fn test_mailbox_into_reverse_path() {
3562 let mailbox = Mailbox {
3563 local_part: String::from("user"),
3564 domain: Domain::DomainName("example.com".parse().unwrap()),
3565 };
3566 let path: ReversePath = mailbox.into();
3567 match path {
3568 ReversePath::Path(p) => {
3569 k9::assert_equal!(p.mailbox.local_part(), "user");
3570 }
3571 _ => panic!("Expected Path variant"),
3572 }
3573 }
3574
3575 #[test]
3576 fn test_mailbox_into_forward_path() {
3577 let mailbox = Mailbox {
3578 local_part: String::from("user"),
3579 domain: Domain::DomainName("example.com".parse().unwrap()),
3580 };
3581 let path: ForwardPath = mailbox.into();
3582 match path {
3583 ForwardPath::Path(p) => {
3584 k9::assert_equal!(p.mailbox.local_part(), "user");
3585 }
3586 _ => panic!("Expected Path variant"),
3587 }
3588 }
3589
3590 #[test]
3593 fn test_mailbox_roundtrip() {
3594 let original = Mailbox {
3595 local_part: String::from("user"),
3596 domain: Domain::DomainName("example.com".parse().unwrap()),
3597 };
3598 let path: MailPath = original.clone().into();
3599 let mailbox: Mailbox = path.mailbox.into();
3600 k9::assert_equal!(original.local_part(), mailbox.local_part());
3601 k9::assert_equal!(original.domain, mailbox.domain);
3602 }
3603
3604 #[test]
3605 fn test_reverse_path_to_forward_path() {
3606 let path = MailPath {
3607 at_domain_list: vec!["route.com".into()],
3608 mailbox: Mailbox {
3609 local_part: String::from("user"),
3610 domain: Domain::DomainName("example.com".parse().unwrap()),
3611 },
3612 };
3613 let rp = ReversePath::Path(path.clone());
3614 let fp: ForwardPath = rp.try_into().unwrap();
3615 match fp {
3616 ForwardPath::Path(p) => {
3617 k9::assert_equal!(p.mailbox.local_part(), "user");
3618 }
3619 _ => panic!("Expected Path variant"),
3620 }
3621 }
3622
3623 #[test]
3626 fn test_mailpath_debug_simple() {
3627 let mailbox = Mailbox {
3628 local_part: "someone".into(),
3629 domain: Domain::DomainName("example.com".parse().unwrap()),
3630 };
3631 let path: MailPath = mailbox.into();
3632 let debug_str = format!("{:?}", path);
3633 k9::assert_equal!(debug_str, r#"MailPath("someone@example.com")"#);
3634 }
3635
3636 #[test]
3637 fn test_mailpath_debug_with_at_domain_list() {
3638 let path = MailPath {
3639 at_domain_list: vec!["route.example.com".into()],
3640 mailbox: Mailbox {
3641 local_part: "user".into(),
3642 domain: Domain::DomainName("host".parse().unwrap()),
3643 },
3644 };
3645 let debug_str = format!("{:?}", path);
3646 k9::assert_equal!(debug_str, r#"MailPath("@route.example.com:user@host")"#);
3647 }
3648
3649 #[test]
3650 fn test_mailpath_debug_multiple_at_domains() {
3651 let path = MailPath {
3652 at_domain_list: vec!["hosta.int".into(), "jkl.org".into()],
3653 mailbox: Mailbox {
3654 local_part: "userc".into(),
3655 domain: Domain::DomainName("d.bar.org".parse().unwrap()),
3656 },
3657 };
3658 let debug_str = format!("{:?}", path);
3659 k9::assert_equal!(
3660 debug_str,
3661 r#"MailPath("@hosta.int,@jkl.org:userc@d.bar.org")"#
3662 );
3663 }
3664
3665 #[test]
3666 fn test_mailpath_debug_non_ascii_utf8() {
3667 let mailbox = Mailbox {
3670 local_part: "üser".into(),
3671 domain: Domain::DomainName("example.com".parse().unwrap()),
3672 };
3673 let path: MailPath = mailbox.into();
3674 let debug_str = format!("{:?}", path);
3675 k9::assert_equal!(debug_str, r#"MailPath("üser@example.com")"#);
3677 }
3678
3679 #[test]
3680 fn test_mailpath_debug_with_ipv4() {
3681 let mailbox = Mailbox {
3682 local_part: "user".into(),
3683 domain: Domain::V4("10.0.0.1".parse().unwrap()),
3684 };
3685 let path: MailPath = mailbox.into();
3686 let debug_str = format!("{:?}", path);
3687 k9::assert_equal!(debug_str, r#"MailPath("user@[10.0.0.1]")"#);
3689 }
3690
3691 #[test]
3692 fn test_mailpath_debug_with_ipv6() {
3693 let mailbox = Mailbox {
3694 local_part: "user".into(),
3695 domain: Domain::V6("::1".parse().unwrap()),
3696 };
3697 let path: MailPath = mailbox.into();
3698 let debug_str = format!("{:?}", path);
3699 k9::assert_equal!(debug_str, r#"MailPath("user@[IPv6:::1]")"#);
3701 }
3702
3703 #[test]
3704 fn test_mailpath_debug_with_tagged_literal() {
3705 let mailbox = Mailbox {
3706 local_part: "user".into(),
3707 domain: Domain::Tagged("future:something".into()),
3708 };
3709 let path: MailPath = mailbox.into();
3710 let debug_str = format!("{:?}", path);
3711 k9::assert_equal!(debug_str, r#"MailPath("user@[future:something]")"#);
3713 }
3714 #[test]
3715 fn test_envelope_address_debug_null() {
3716 let addr = EnvelopeAddress::Null;
3717 let debug_str = format!("{:?}", addr);
3718 k9::assert_equal!(debug_str, "<>");
3719 }
3720
3721 #[test]
3722 fn test_envelope_address_debug_postmaster() {
3723 let addr = EnvelopeAddress::Postmaster;
3724 let debug_str = format!("{:?}", addr);
3725 k9::assert_equal!(debug_str, "<Postmaster>");
3726 }
3727
3728 #[test]
3729 fn test_envelope_address_debug_path_with_non_ascii_utf8() {
3730 let path = MailPath {
3733 at_domain_list: vec!["example.com".into()],
3734 mailbox: Mailbox {
3735 local_part: String::from("üser"),
3736 domain: Domain::DomainName("example.com".parse().unwrap()),
3737 },
3738 };
3739 let addr = EnvelopeAddress::Path(path);
3740 let debug_str = format!("{:?}", addr);
3741 k9::assert_equal!(debug_str, r#"<üser@example.com>"#);
3745 }
3746
3747 #[test]
3748 fn test_envelope_address_display_drops_source_route() {
3749 let path = MailPath {
3750 at_domain_list: vec!["hosta.int".into(), "jkl.org".into()],
3751 mailbox: Mailbox {
3752 local_part: String::from("userc"),
3753 domain: Domain::DomainName("d.bar.org".parse().unwrap()),
3754 },
3755 };
3756 let addr = EnvelopeAddress::Path(path);
3757 k9::assert_equal!(addr.to_string(), "userc@d.bar.org");
3759 if let EnvelopeAddress::Path(ref p) = addr {
3761 k9::assert_equal!(p.to_string(), "@hosta.int,@jkl.org:userc@d.bar.org");
3762 }
3763
3764 let path2 = MailPath {
3766 at_domain_list: vec!["relay.example".into()],
3767 mailbox: Mailbox {
3768 local_part: String::from(r#""info@""#),
3769 domain: Domain::DomainName("example.com".parse().unwrap()),
3770 },
3771 };
3772 let addr2 = EnvelopeAddress::Path(path2);
3773 k9::assert_equal!(addr2.to_string(), r#""info@"@example.com"#);
3774 if let EnvelopeAddress::Path(ref p) = addr2 {
3775 k9::assert_equal!(p.to_string(), r#"@relay.example:"info@"@example.com"#);
3776 }
3777 }
3778
3779 #[test]
3780 fn test_envelope_address_serde_roundtrip_with_source_route() {
3781 let cmd = unwrapper(Command::parse(
3783 "MAIL FROM:<@hosta.int,@jkl.org:userc@d.bar.org>",
3784 ));
3785 if let MaybePartialCommand::Full(Command::MailFrom { address, .. }) = cmd {
3786 if let ReversePath::Path(mail_path) = address {
3787 let addr = EnvelopeAddress::Path(mail_path);
3788 let serialized = serde_json::to_string(&addr).unwrap();
3790 k9::assert_equal!(serialized, r#""userc@d.bar.org""#);
3791 let deserialized: EnvelopeAddress = serde_json::from_str(&serialized).unwrap();
3793 k9::assert_equal!(deserialized.to_string(), "userc@d.bar.org");
3795 } else {
3796 panic!("Expected Path variant");
3797 }
3798 } else {
3799 panic!("Expected Full MailFrom");
3800 }
3801
3802 let cmd2 = unwrapper(Command::parse(
3804 r#"MAIL FROM:<@relay.example:"info@"@example.com>"#,
3805 ));
3806 if let MaybePartialCommand::Full(Command::MailFrom { address, .. }) = cmd2 {
3807 if let ReversePath::Path(mail_path) = address {
3808 let addr = EnvelopeAddress::Path(mail_path);
3809 let serialized = serde_json::to_string(&addr).unwrap();
3810 k9::assert_equal!(serialized, r#""\"info@\"@example.com""#);
3811 let deserialized: EnvelopeAddress = serde_json::from_str(&serialized).unwrap();
3812 k9::assert_equal!(deserialized.to_string(), r#""info@"@example.com"#);
3813 } else {
3814 panic!("Expected Path variant");
3815 }
3816 } else {
3817 panic!("Expected Full MailFrom");
3818 }
3819 }
3820
3821 #[test]
3822 fn test_envelope_address_rejects_obs_local_part() {
3823 EnvelopeAddress::parse(r#""first".last@example.com"#).unwrap_err();
3826 EnvelopeAddress::parse(r#"first."last"@example.com"#).unwrap_err();
3827 EnvelopeAddress::parse(r#""first"."last"@example.com"#).unwrap_err();
3828 }
3829}