1use std::{borrow::Cow, fmt, io::Write, ops::Deref};
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6use crate::utils::escape_quoted;
7
8mod utils;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum Command {
12 Ehlo {
13 domain_or_address: DomainOrAddress,
14 },
15 Helo {
16 domain_or_address: DomainOrAddress,
17 },
18 Mail {
19 reverse_path: String,
20 parameters: Vec<Parameter>,
21 },
22 Rcpt {
23 forward_path: String,
24 parameters: Vec<Parameter>,
25 },
26 Data,
27 Rset,
28 Vrfy {
35 user_or_mailbox: AtomOrQuoted,
36 },
37 Expn {
47 mailing_list: AtomOrQuoted,
48 },
49 Help {
60 argument: Option<AtomOrQuoted>,
61 },
62 Noop {
72 argument: Option<AtomOrQuoted>,
73 },
74 Quit,
91 StartTLS,
93 AuthLogin(Option<String>),
95 AuthPlain(Option<String>),
97}
98
99#[derive(Clone, Debug, PartialEq, Eq)]
100pub enum DomainOrAddress {
101 Domain(String),
102 Address(String),
103}
104
105impl DomainOrAddress {
106 pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
107 match self {
108 DomainOrAddress::Domain(domain) => write!(writer, "{}", domain),
109 DomainOrAddress::Address(address) => write!(writer, "[{}]", address),
110 }
111 }
112}
113
114#[derive(Clone, Debug, PartialEq, Eq)]
115#[non_exhaustive]
116pub enum Parameter {
117 Size(u32),
119 Other {
120 keyword: String,
121 value: Option<String>,
122 },
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub enum AtomOrQuoted {
127 Atom(String),
128 Quoted(String),
129}
130
131impl Command {
132 pub fn name(&self) -> &'static str {
133 match self {
134 Command::Ehlo { .. } => "EHLO",
135 Command::Helo { .. } => "HELO",
136 Command::Mail { .. } => "MAIL",
137 Command::Rcpt { .. } => "RCPT",
138 Command::Data => "DATA",
139 Command::Rset => "RSET",
140 Command::Vrfy { .. } => "VRFY",
141 Command::Expn { .. } => "EXPN",
142 Command::Help { .. } => "HELP",
143 Command::Noop { .. } => "NOOP",
144 Command::Quit => "QUIT",
145 Command::StartTLS => "STARTTLS",
147 Command::AuthLogin(_) => "AUTHLOGIN",
149 Command::AuthPlain(_) => "AUTHPLAIN",
151 }
152 }
153
154 pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
155 use Command::*;
156
157 match self {
158 Helo { domain_or_address } => {
160 writer.write_all(b"HELO ")?;
161 domain_or_address.serialize(writer)?;
162 }
163 Ehlo { domain_or_address } => {
165 writer.write_all(b"EHLO ")?;
166 domain_or_address.serialize(writer)?;
167 }
168 Mail {
170 reverse_path,
171 parameters,
172 } => {
173 writer.write_all(b"MAIL FROM:<")?;
174 writer.write_all(reverse_path.as_bytes())?;
175 writer.write_all(b">")?;
176
177 for parameter in parameters {
178 writer.write_all(b" ")?;
179 parameter.serialize(writer)?;
180 }
181 }
182 Rcpt {
184 forward_path,
185 parameters,
186 } => {
187 writer.write_all(b"RCPT TO:<")?;
188 writer.write_all(forward_path.as_bytes())?;
189 writer.write_all(b">")?;
190
191 for parameter in parameters {
192 writer.write_all(b" ")?;
193 parameter.serialize(writer)?;
194 }
195 }
196 Data => writer.write_all(b"DATA")?,
198 Rset => writer.write_all(b"RSET")?,
200 Vrfy { user_or_mailbox } => {
202 writer.write_all(b"VRFY ")?;
203 user_or_mailbox.serialize(writer)?;
204 }
205 Expn { mailing_list } => {
207 writer.write_all(b"EXPN ")?;
208 mailing_list.serialize(writer)?;
209 }
210 Help { argument: None } => writer.write_all(b"HELP")?,
212 Help {
213 argument: Some(data),
214 } => {
215 writer.write_all(b"HELP ")?;
216 data.serialize(writer)?;
217 }
218 Noop { argument: None } => writer.write_all(b"NOOP")?,
220 Noop {
221 argument: Some(data),
222 } => {
223 writer.write_all(b"NOOP ")?;
224 data.serialize(writer)?;
225 }
226 Quit => writer.write_all(b"QUIT")?,
228 StartTLS => writer.write_all(b"STARTTLS")?,
231 AuthLogin(None) => {
233 writer.write_all(b"AUTH LOGIN")?;
234 }
235 AuthLogin(Some(data)) => {
236 writer.write_all(b"AUTH LOGIN ")?;
237 writer.write_all(data.as_bytes())?;
238 }
239 AuthPlain(None) => {
241 writer.write_all(b"AUTH PLAIN")?;
242 }
243 AuthPlain(Some(data)) => {
244 writer.write_all(b"AUTH PLAIN ")?;
245 writer.write_all(data.as_bytes())?;
246 }
247 }
248
249 write!(writer, "\r\n")
250 }
251}
252
253impl Parameter {
254 pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
255 match self {
256 Parameter::Size(size) => {
257 write!(writer, "SIZE={}", size)?;
258 }
259 Parameter::Other { keyword, value } => {
260 writer.write_all(keyword.as_bytes())?;
261
262 if let Some(ref value) = value {
263 writer.write_all(b"=")?;
264 writer.write_all(value.as_bytes())?;
265 }
266 }
267 };
268
269 Ok(())
270 }
271}
272
273impl AtomOrQuoted {
274 pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
275 match self {
276 AtomOrQuoted::Atom(atom) => {
277 writer.write_all(atom.as_bytes())?;
278 }
279 AtomOrQuoted::Quoted(quoted) => {
280 writer.write_all(b"\"")?;
281 writer.write_all(escape_quoted(quoted).as_bytes())?;
282 writer.write_all(b"\"")?;
283 }
284 }
285
286 Ok(())
287 }
288}
289
290#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
293#[derive(Debug, Clone, PartialEq, Eq)]
294#[non_exhaustive]
295pub enum Response {
296 Greeting {
297 domain: String,
298 text: String,
299 },
300 Ehlo {
301 domain: String,
302 greet: Option<String>,
303 capabilities: Vec<Capability>,
304 },
305 Other {
306 code: ReplyCode,
307 lines: Vec<TextString<'static>>,
308 },
309}
310
311impl Response {
312 pub fn greeting<D, T>(domain: D, text: T) -> Response
313 where
314 D: Into<String>,
315 T: Into<String>,
316 {
317 Response::Greeting {
318 domain: domain.into(),
319 text: text.into(),
320 }
321 }
322
323 pub fn ehlo<D, G>(domain: D, greet: Option<G>, capabilities: Vec<Capability>) -> Response
324 where
325 D: Into<String>,
326 G: Into<String>,
327 {
328 Response::Ehlo {
329 domain: domain.into(),
330 greet: greet.map(Into::into),
331 capabilities,
332 }
333 }
334
335 pub fn other<T>(code: ReplyCode, text: TextString<'static>) -> Response
336 where
337 T: Into<String>,
338 {
339 Response::Other {
340 code,
341 lines: vec![text],
342 }
343 }
344
345 pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
346 match self {
347 Response::Greeting { domain, text } => {
348 let lines = text.lines().collect::<Vec<_>>();
349
350 if let Some((first, tail)) = lines.split_first() {
351 if let Some((last, head)) = tail.split_last() {
352 write!(writer, "220-{} {}\r\n", domain, first)?;
353
354 for line in head {
355 write!(writer, "220-{}\r\n", line)?;
356 }
357
358 write!(writer, "220 {}\r\n", last)?;
359 } else {
360 write!(writer, "220 {} {}\r\n", domain, first)?;
361 }
362 } else {
363 write!(writer, "220 {}\r\n", domain)?;
364 }
365 }
366 Response::Ehlo {
367 domain,
368 greet,
369 capabilities,
370 } => {
371 let greet = match greet {
372 Some(greet) => format!(" {}", greet),
373 None => "".to_string(),
374 };
375
376 if let Some((tail, head)) = capabilities.split_last() {
377 writer.write_all(format!("250-{}{}\r\n", domain, greet).as_bytes())?;
378
379 for capability in head {
380 writer.write_all(b"250-")?;
381 capability.serialize(writer)?;
382 writer.write_all(b"\r\n")?;
383 }
384
385 writer.write_all(b"250 ")?;
386 tail.serialize(writer)?;
387 writer.write_all(b"\r\n")?;
388 } else {
389 writer.write_all(format!("250 {}{}\r\n", domain, greet).as_bytes())?;
390 }
391 }
392 Response::Other { code, lines } => {
393 let code = u16::from(*code);
394 for line in lines.iter().take(lines.len().saturating_sub(1)) {
395 write!(writer, "{}-{}\r\n", code, line,)?;
396 }
397
398 match lines.last() {
399 Some(s) => write!(writer, "{} {}\r\n", code, s)?,
400 None => write!(writer, "{}\r\n", code)?,
401 };
402 }
403 }
404
405 Ok(())
406 }
407}
408
409#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
412#[derive(Debug, Clone, PartialEq, Eq)]
413#[non_exhaustive]
414pub enum Capability {
415 EXPN,
451 Help,
454
455 EightBitMIME,
457
458 Size(u32),
460
461 Chunking,
463
464 BinaryMIME,
466
467 Checkpoint,
469
470 DeliverBy,
472
473 Pipelining,
475
476 DSN,
478
479 ETRN,
482
483 EnhancedStatusCodes,
485
486 StartTLS,
488
489 MTRK,
494
495 ATRN,
498
499 Auth(Vec<AuthMechanism>),
501
502 BURL,
505
506 SMTPUTF8,
517
518 RRVS,
523
524 RequireTLS,
526
527 Other {
534 keyword: String,
535 params: Vec<String>,
536 },
537}
538
539impl Capability {
540 pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
541 match self {
542 Capability::EXPN => writer.write_all(b"EXPN"),
543 Capability::Help => writer.write_all(b"HELP"),
544 Capability::EightBitMIME => writer.write_all(b"8BITMIME"),
545 Capability::Size(number) => writer.write_all(format!("SIZE {}", number).as_bytes()),
546 Capability::Chunking => writer.write_all(b"CHUNKING"),
547 Capability::BinaryMIME => writer.write_all(b"BINARYMIME"),
548 Capability::Checkpoint => writer.write_all(b"CHECKPOINT"),
549 Capability::DeliverBy => writer.write_all(b"DELIVERBY"),
550 Capability::Pipelining => writer.write_all(b"PIPELINING"),
551 Capability::DSN => writer.write_all(b"DSN"),
552 Capability::ETRN => writer.write_all(b"ETRN"),
553 Capability::EnhancedStatusCodes => writer.write_all(b"ENHANCEDSTATUSCODES"),
554 Capability::StartTLS => writer.write_all(b"STARTTLS"),
555 Capability::MTRK => writer.write_all(b"MTRK"),
556 Capability::ATRN => writer.write_all(b"ATRN"),
557 Capability::Auth(mechanisms) => {
558 if let Some((tail, head)) = mechanisms.split_last() {
559 writer.write_all(b"AUTH ")?;
560
561 for mechanism in head {
562 mechanism.serialize(writer)?;
563 writer.write_all(b" ")?;
564 }
565
566 tail.serialize(writer)
567 } else {
568 writer.write_all(b"AUTH")
569 }
570 }
571 Capability::BURL => writer.write_all(b"BURL"),
572 Capability::SMTPUTF8 => writer.write_all(b"SMTPUTF8"),
573 Capability::RRVS => writer.write_all(b"RRVS"),
574 Capability::RequireTLS => writer.write_all(b"REQUIRETLS"),
575 Capability::Other { keyword, params } => {
576 if let Some((tail, head)) = params.split_last() {
577 writer.write_all(keyword.as_bytes())?;
578 writer.write_all(b" ")?;
579
580 for param in head {
581 writer.write_all(param.as_bytes())?;
582 writer.write_all(b" ")?;
583 }
584
585 writer.write_all(tail.as_bytes())
586 } else {
587 writer.write_all(keyword.as_bytes())
588 }
589 }
590 }
591 }
592}
593
594#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
595#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
596pub enum ReplyCode {
597 SystemStatus,
599 HelpMessage,
604 Ready,
606 ClosingChannel,
608 Ok,
610 UserNotLocalWillForward,
612 CannotVrfy,
614 StartMailInput,
616 NotAvailable,
620 MailboxTemporarilyUnavailable,
624 ProcessingError,
626 InsufficientStorage,
628 UnableToAccommodateParameters,
630 SyntaxError,
632 ParameterSyntaxError,
634 CommandNotImplemented,
636 BadSequence,
638 ParameterNotImplemented,
640 NoMailService,
642 MailboxPermanentlyUnavailable,
646 UserNotLocal,
648 ExceededStorageAllocation,
650 MailboxNameNotAllowed,
654 TransactionFailed,
658 ParametersNotImplemented,
660 Other(u16),
662}
663
664impl ReplyCode {
665 pub fn is_completed(&self) -> bool {
666 let code = u16::from(*self);
667 code > 199 && code < 300
668 }
669
670 pub fn is_accepted(&self) -> bool {
671 let code = u16::from(*self);
672 code > 299 && code < 400
673 }
674
675 pub fn is_temporary_error(&self) -> bool {
676 let code = u16::from(*self);
677 code > 399 && code < 500
678 }
679
680 pub fn is_permanent_error(&self) -> bool {
681 let code = u16::from(*self);
682 code > 499 && code < 600
683 }
684}
685
686impl From<u16> for ReplyCode {
687 fn from(value: u16) -> Self {
688 match value {
689 211 => ReplyCode::SystemStatus,
690 214 => ReplyCode::HelpMessage,
691 220 => ReplyCode::Ready,
692 221 => ReplyCode::ClosingChannel,
693 250 => ReplyCode::Ok,
694 251 => ReplyCode::UserNotLocalWillForward,
695 252 => ReplyCode::CannotVrfy,
696 354 => ReplyCode::StartMailInput,
697 421 => ReplyCode::NotAvailable,
698 450 => ReplyCode::MailboxTemporarilyUnavailable,
699 451 => ReplyCode::ProcessingError,
700 452 => ReplyCode::InsufficientStorage,
701 455 => ReplyCode::UnableToAccommodateParameters,
702 500 => ReplyCode::SyntaxError,
703 501 => ReplyCode::ParameterSyntaxError,
704 502 => ReplyCode::CommandNotImplemented,
705 503 => ReplyCode::BadSequence,
706 504 => ReplyCode::ParameterNotImplemented,
707 521 => ReplyCode::NoMailService,
708 550 => ReplyCode::MailboxPermanentlyUnavailable,
709 551 => ReplyCode::UserNotLocal,
710 552 => ReplyCode::ExceededStorageAllocation,
711 553 => ReplyCode::MailboxNameNotAllowed,
712 554 => ReplyCode::TransactionFailed,
713 555 => ReplyCode::ParametersNotImplemented,
714 _ => ReplyCode::Other(value),
715 }
716 }
717}
718
719impl From<ReplyCode> for u16 {
720 fn from(value: ReplyCode) -> Self {
721 match value {
722 ReplyCode::SystemStatus => 211,
723 ReplyCode::HelpMessage => 214,
724 ReplyCode::Ready => 220,
725 ReplyCode::ClosingChannel => 221,
726 ReplyCode::Ok => 250,
727 ReplyCode::UserNotLocalWillForward => 251,
728 ReplyCode::CannotVrfy => 252,
729 ReplyCode::StartMailInput => 354,
730 ReplyCode::NotAvailable => 421,
731 ReplyCode::MailboxTemporarilyUnavailable => 450,
732 ReplyCode::ProcessingError => 451,
733 ReplyCode::InsufficientStorage => 452,
734 ReplyCode::UnableToAccommodateParameters => 455,
735 ReplyCode::SyntaxError => 500,
736 ReplyCode::ParameterSyntaxError => 501,
737 ReplyCode::CommandNotImplemented => 502,
738 ReplyCode::BadSequence => 503,
739 ReplyCode::ParameterNotImplemented => 504,
740 ReplyCode::NoMailService => 521,
741 ReplyCode::MailboxPermanentlyUnavailable => 550,
742 ReplyCode::UserNotLocal => 551,
743 ReplyCode::ExceededStorageAllocation => 552,
744 ReplyCode::MailboxNameNotAllowed => 553,
745 ReplyCode::TransactionFailed => 554,
746 ReplyCode::ParametersNotImplemented => 555,
747 ReplyCode::Other(v) => v,
748 }
749 }
750}
751
752#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
753#[derive(Debug, Clone, PartialEq, Eq)]
754#[non_exhaustive]
755pub enum AuthMechanism {
756 Plain,
757 Login,
758 GSSAPI,
759
760 CramMD5,
761 CramSHA1,
762 ScramMD5,
763 DigestMD5,
764 NTLM,
765
766 Other(String),
767}
768
769impl AuthMechanism {
770 pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
771 match self {
772 AuthMechanism::Plain => writer.write_all(b"PLAIN"),
773 AuthMechanism::Login => writer.write_all(b"LOGIN"),
774 AuthMechanism::GSSAPI => writer.write_all(b"GSSAPI"),
775
776 AuthMechanism::CramMD5 => writer.write_all(b"CRAM-MD5"),
777 AuthMechanism::CramSHA1 => writer.write_all(b"CRAM-SHA1"),
778 AuthMechanism::ScramMD5 => writer.write_all(b"SCRAM-MD5"),
779 AuthMechanism::DigestMD5 => writer.write_all(b"DIGEST-MD5"),
780 AuthMechanism::NTLM => writer.write_all(b"NTLM"),
781
782 AuthMechanism::Other(other) => writer.write_all(other.as_bytes()),
783 }
784 }
785}
786
787#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
789#[derive(Clone, Debug, Eq, PartialEq)]
790pub struct TextString<'a>(pub(crate) Cow<'a, str>);
791
792impl<'a> TextString<'a> {
793 pub fn new(s: &'a str) -> Result<Self, InvalidTextString> {
794 if s.is_empty() {
795 return Err(InvalidTextString(()));
796 }
797
798 match s.as_bytes().iter().all(|&b| is_text_string_byte(b)) {
799 true => Ok(TextString(Cow::Borrowed(s))),
800 false => Err(InvalidTextString(())),
801 }
802 }
803
804 pub fn new_unchecked(s: &'a str) -> Self {
805 #[cfg(debug_assertions)]
806 return TextString::new(s).expect("String should have been valid but wasn't.");
807
808 #[cfg(not(debug_assertions))]
809 return TextString(Cow::Borrowed(s));
810 }
811
812 pub fn into_owned(self) -> TextString<'static> {
813 TextString(self.0.into_owned().into())
814 }
815}
816
817impl Deref for TextString<'_> {
818 type Target = str;
819
820 fn deref(&self) -> &Self::Target {
821 &self.0
822 }
823}
824
825impl fmt::Display for TextString<'_> {
826 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
827 write!(f, "{}", self.0)
828 }
829}
830
831#[derive(Debug)]
832pub struct InvalidTextString(());
833
834impl fmt::Display for InvalidTextString {
835 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
836 write!(f, "input contains invalid characters")
837 }
838}
839
840impl std::error::Error for InvalidTextString {}
841
842fn is_text_string_byte(byte: u8) -> bool {
845 matches!(byte, 9 | 32..=126)
846}
847
848#[cfg(test)]
851mod tests {
852 use super::{Capability, ReplyCode, Response, TextString};
853
854 #[test]
855 fn test_serialize_greeting() {
856 let tests = &[
857 (
858 Response::Greeting {
859 domain: "example.org".into(),
860 text: "".into(),
861 },
862 b"220 example.org\r\n".as_ref(),
863 ),
864 (
865 Response::Greeting {
866 domain: "example.org".into(),
867 text: "A".into(),
868 },
869 b"220 example.org A\r\n".as_ref(),
870 ),
871 (
872 Response::Greeting {
873 domain: "example.org".into(),
874 text: "A\nB".into(),
875 },
876 b"220-example.org A\r\n220 B\r\n".as_ref(),
877 ),
878 (
879 Response::Greeting {
880 domain: "example.org".into(),
881 text: "A\nB\nC".into(),
882 },
883 b"220-example.org A\r\n220-B\r\n220 C\r\n".as_ref(),
884 ),
885 ];
886
887 for (test, expected) in tests.iter() {
888 let mut got = Vec::new();
889 test.serialize(&mut got).unwrap();
890 assert_eq!(expected, &got);
891 }
892 }
893
894 #[test]
895 fn test_serialize_ehlo() {
896 let tests = &[
897 (
898 Response::Ehlo {
899 domain: "example.org".into(),
900 greet: None,
901 capabilities: vec![],
902 },
903 b"250 example.org\r\n".as_ref(),
904 ),
905 (
906 Response::Ehlo {
907 domain: "example.org".into(),
908 greet: Some("...".into()),
909 capabilities: vec![],
910 },
911 b"250 example.org ...\r\n".as_ref(),
912 ),
913 (
914 Response::Ehlo {
915 domain: "example.org".into(),
916 greet: Some("...".into()),
917 capabilities: vec![Capability::StartTLS],
918 },
919 b"250-example.org ...\r\n250 STARTTLS\r\n".as_ref(),
920 ),
921 (
922 Response::Ehlo {
923 domain: "example.org".into(),
924 greet: Some("...".into()),
925 capabilities: vec![Capability::StartTLS, Capability::Size(12345)],
926 },
927 b"250-example.org ...\r\n250-STARTTLS\r\n250 SIZE 12345\r\n".as_ref(),
928 ),
929 ];
930
931 for (test, expected) in tests.iter() {
932 let mut got = Vec::new();
933 test.serialize(&mut got).unwrap();
934 assert_eq!(expected, &got);
935 }
936 }
937
938 #[test]
939 fn test_serialize_other() {
940 let tests = &[
941 (
942 Response::Other {
943 code: ReplyCode::StartMailInput,
944 lines: vec![],
945 },
946 b"354\r\n".as_ref(),
947 ),
948 (
949 Response::Other {
950 code: ReplyCode::StartMailInput,
951 lines: vec![TextString::new("A").unwrap()],
952 },
953 b"354 A\r\n".as_ref(),
954 ),
955 (
956 Response::Other {
957 code: ReplyCode::StartMailInput,
958 lines: vec![TextString::new("A").unwrap(), TextString::new("B").unwrap()],
959 },
960 b"354-A\r\n354 B\r\n".as_ref(),
961 ),
962 ];
963
964 for (test, expected) in tests.iter() {
965 let mut got = Vec::new();
966 test.serialize(&mut got).unwrap();
967 assert_eq!(expected, &got);
968 }
969 }
970}