smtp_types/
lib.rs

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    /// This command asks the receiver to confirm that the argument
29    /// identifies a user or mailbox.  If it is a user name, information is
30    /// returned as specified in Section 3.5.
31    ///
32    /// This command has no effect on the reverse-path buffer, the forward-
33    /// path buffer, or the mail data buffer.
34    Vrfy {
35        user_or_mailbox: AtomOrQuoted,
36    },
37    /// This command asks the receiver to confirm that the argument
38    /// identifies a mailing list, and if so, to return the membership of
39    /// that list.  If the command is successful, a reply is returned
40    /// containing information as described in Section 3.5.  This reply will
41    /// have multiple lines except in the trivial case of a one-member list.
42    ///
43    /// This command has no effect on the reverse-path buffer, the forward-
44    /// path buffer, or the mail data buffer, and it may be issued at any
45    /// time.
46    Expn {
47        mailing_list: AtomOrQuoted,
48    },
49    /// This command causes the server to send helpful information to the
50    /// client.  The command MAY take an argument (e.g., any command name)
51    /// and return more specific information as a response.
52    ///
53    /// SMTP servers SHOULD support HELP without arguments and MAY support it
54    /// with arguments.
55    ///
56    /// This command has no effect on the reverse-path buffer, the forward-
57    /// path buffer, or the mail data buffer, and it may be issued at any
58    /// time.
59    Help {
60        argument: Option<AtomOrQuoted>,
61    },
62    /// This command does not affect any parameters or previously entered
63    /// commands.  It specifies no action other than that the receiver send a
64    /// "250 OK" reply.
65    ///
66    ///  If a parameter string is specified, servers SHOULD ignore it.
67    ///
68    /// This command has no effect on the reverse-path buffer, the forward-
69    /// path buffer, or the mail data buffer, and it may be issued at any
70    /// time.
71    Noop {
72        argument: Option<AtomOrQuoted>,
73    },
74    /// This command specifies that the receiver MUST send a "221 OK" reply,
75    /// and then close the transmission channel.
76    ///
77    /// The receiver MUST NOT intentionally close the transmission channel
78    /// until it receives and replies to a QUIT command (even if there was an
79    /// error).  The sender MUST NOT intentionally close the transmission
80    /// channel until it sends a QUIT command, and it SHOULD wait until it
81    /// receives the reply (even if there was an error response to a previous
82    /// command).  If the connection is closed prematurely due to violations
83    /// of the above or system or network failure, the server MUST cancel any
84    /// pending transaction, but not undo any previously completed
85    /// transaction, and generally MUST act as if the command or transaction
86    /// in progress had received a temporary error (i.e., a 4yz response).
87    ///
88    /// The QUIT command may be issued at any time.  Any current uncompleted
89    /// mail transaction will be aborted.
90    Quit,
91    // Extensions
92    StartTLS,
93    // AUTH LOGIN
94    AuthLogin(Option<String>),
95    // AUTH PLAIN
96    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    /// Message size declaration [RFC1870]
118    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            // Extensions
146            Command::StartTLS => "STARTTLS",
147            // TODO: SMTP AUTH LOGIN
148            Command::AuthLogin(_) => "AUTHLOGIN",
149            // TODO: SMTP AUTH PLAIN
150            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 = "HELO" SP Domain CRLF
159            Helo { domain_or_address } => {
160                writer.write_all(b"HELO ")?;
161                domain_or_address.serialize(writer)?;
162            }
163            // ehlo = "EHLO" SP ( Domain / address-literal ) CRLF
164            Ehlo { domain_or_address } => {
165                writer.write_all(b"EHLO ")?;
166                domain_or_address.serialize(writer)?;
167            }
168            // mail = "MAIL FROM:" Reverse-path [SP Mail-parameters] CRLF
169            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 = "RCPT TO:" ( "<Postmaster@" Domain ">" / "<Postmaster>" / Forward-path ) [SP Rcpt-parameters] CRLF
183            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 = "DATA" CRLF
197            Data => writer.write_all(b"DATA")?,
198            // rset = "RSET" CRLF
199            Rset => writer.write_all(b"RSET")?,
200            // vrfy = "VRFY" SP String CRLF
201            Vrfy { user_or_mailbox } => {
202                writer.write_all(b"VRFY ")?;
203                user_or_mailbox.serialize(writer)?;
204            }
205            // expn = "EXPN" SP String CRLF
206            Expn { mailing_list } => {
207                writer.write_all(b"EXPN ")?;
208                mailing_list.serialize(writer)?;
209            }
210            // help = "HELP" [ SP String ] CRLF
211            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 = "NOOP" [ SP String ] CRLF
219            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 = "QUIT" CRLF
227            Quit => writer.write_all(b"QUIT")?,
228            // ----- Extensions -----
229            // starttls = "STARTTLS" CRLF
230            StartTLS => writer.write_all(b"STARTTLS")?,
231            // auth_login_command = "AUTH LOGIN" [SP username] CRLF
232            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            // auth_plain_command = "AUTH PLAIN" [SP base64] CRLF
240            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// -------------------------------------------------------------------------------------------------
291
292#[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// -------------------------------------------------------------------------------------------------
410
411#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
412#[derive(Debug, Clone, PartialEq, Eq)]
413#[non_exhaustive]
414pub enum Capability {
415    // Send as mail [RFC821]
416    // The description of SEND was updated by [RFC1123] and then its actual use was deprecated in [RFC2821]
417    // SEND,
418
419    // Send as mail or to terminal [RFC821]
420    // The description of SOML was updated by [RFC1123] and then its actual use was deprecated in [RFC2821]
421    // SOML,
422
423    // Send as mail and to terminal [RFC821]
424    // The description of SAML was updated by [RFC1123] and then its actual use was deprecated in [RFC2821]
425    // SAML,
426
427    // Interchange the client and server roles [RFC821]
428    // The actual use of TURN was deprecated in [RFC2821]
429    // TURN,
430
431    // SMTP Responsible Submitter [RFC4405]
432    // Deprecated by [https://datatracker.ietf.org/doc/status-change-change-sender-id-to-historic].
433    // SUBMITTER,
434
435    // Internationalized email address [RFC5336]
436    // Experimental; deprecated in [RFC6531].
437    // UTF8SMTP,
438
439    // ---------------------------------------------------------------------------------------------
440    /// Verbose [Eric Allman]
441    // VERB,
442
443    /// One message transaction only [Eric Allman]
444    // ONEX,
445
446    // ---------------------------------------------------------------------------------------------
447
448    /// Expand the mailing list [RFC821]
449    /// Command description updated by [RFC5321]
450    EXPN,
451    /// Supply helpful information [RFC821]
452    /// Command description updated by [RFC5321]
453    Help,
454
455    /// SMTP and Submit transport of 8bit MIME content [RFC6152]
456    EightBitMIME,
457
458    /// Message size declaration [RFC1870]
459    Size(u32),
460
461    /// Chunking [RFC3030]
462    Chunking,
463
464    /// Binary MIME [RFC3030]
465    BinaryMIME,
466
467    /// Checkpoint/Restart [RFC1845]
468    Checkpoint,
469
470    /// Deliver By [RFC2852]
471    DeliverBy,
472
473    /// Command Pipelining [RFC2920]
474    Pipelining,
475
476    /// Delivery Status Notification [RFC3461]
477    DSN,
478
479    /// Extended Turn [RFC1985]
480    /// SMTP [RFC5321] only. Not for use on Submit port 587.
481    ETRN,
482
483    /// Enhanced Status Codes [RFC2034]
484    EnhancedStatusCodes,
485
486    /// Start TLS [RFC3207]
487    StartTLS,
488
489    /// Notification of no soliciting [RFC3865]
490    // NoSoliciting,
491
492    /// Message Tracking [RFC3885]
493    MTRK,
494
495    /// Authenticated TURN [RFC2645]
496    /// SMTP [RFC5321] only. Not for use on Submit port 587.
497    ATRN,
498
499    /// Authentication [RFC4954]
500    Auth(Vec<AuthMechanism>),
501
502    /// Remote Content [RFC4468]
503    /// Submit [RFC6409] only. Not for use with SMTP on port 25.
504    BURL,
505
506    /// Future Message Release [RFC4865]
507    // FutureRelease,
508
509    /// Content Conversion Permission [RFC4141]
510    // ConPerm,
511
512    /// Content Conversion Negotiation [RFC4141]
513    // ConNeg,
514
515    /// Internationalized email address [RFC6531]
516    SMTPUTF8,
517
518    /// Priority Message Handling [RFC6710]
519    // MTPRIORITY,
520
521    /// Require Recipient Valid Since [RFC7293]
522    RRVS,
523
524    /// Require TLS [RFC8689]
525    RequireTLS,
526
527    // Observed ...
528    // TIME,
529    // XACK,
530    // VERP,
531    // VRFY,
532    /// Other
533    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    /// 211 System status, or system help reply
598    SystemStatus,
599    /// 214 Help message
600    ///
601    /// Information on how to use the receiver or the meaning of a particular non-standard
602    /// command; this reply is useful only to the human user.
603    HelpMessage,
604    /// 220 <domain> Service ready
605    Ready,
606    /// 221 <domain> Service closing transmission channel
607    ClosingChannel,
608    /// 250 Requested mail action okay, completed
609    Ok,
610    /// 251 User not local; will forward to <forward-path>
611    UserNotLocalWillForward,
612    /// 252 Cannot VRFY user, but will accept message and attempt delivery
613    CannotVrfy,
614    /// 354 Start mail input; end with <CRLF>.<CRLF>
615    StartMailInput,
616    /// 421 <domain> Service not available, closing transmission channel
617    ///
618    /// This may be a reply to any command if the service knows it must shut down.
619    NotAvailable,
620    /// 450 Requested mail action not taken: mailbox unavailable
621    ///
622    /// E.g., mailbox busy or temporarily blocked for policy reasons.
623    MailboxTemporarilyUnavailable,
624    /// 451 Requested action aborted: local error in processing
625    ProcessingError,
626    /// 452 Requested action not taken: insufficient system storage
627    InsufficientStorage,
628    /// 455 Server unable to accommodate parameters
629    UnableToAccommodateParameters,
630    /// 500 Syntax error, command unrecognized
631    SyntaxError,
632    /// 501 Syntax error in parameters or arguments
633    ParameterSyntaxError,
634    /// 502 Command not implemented
635    CommandNotImplemented,
636    /// 503 Bad sequence of commands
637    BadSequence,
638    /// 504 Command parameter not implemented
639    ParameterNotImplemented,
640    /// 521 <domain> does not accept mail (see RFC 1846)
641    NoMailService,
642    /// 550 Requested action not taken: mailbox unavailable
643    ///
644    /// E.g. mailbox not found, no access, or command rejected for policy reasons.
645    MailboxPermanentlyUnavailable,
646    /// 551 User not local; please try <forward-path>
647    UserNotLocal,
648    /// 552 Requested mail action aborted: exceeded storage allocation
649    ExceededStorageAllocation,
650    /// 553 Requested action not taken: mailbox name not allowed
651    ///
652    /// E.g. mailbox syntax incorrect.
653    MailboxNameNotAllowed,
654    /// 554 Transaction failed
655    ///
656    /// Or, in the case of a connection-opening response, "No SMTP service here".
657    TransactionFailed,
658    /// 555 MAIL FROM/RCPT TO parameters not recognized or not implemented
659    ParametersNotImplemented,
660    /// Miscellaneous reply codes
661    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/// A string containing of tab, space and printable ASCII characters
788#[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
842// -------------------------------------------------------------------------------------------------
843
844fn is_text_string_byte(byte: u8) -> bool {
845    matches!(byte, 9 | 32..=126)
846}
847
848// -------------------------------------------------------------------------------------------------
849
850#[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}