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