smtp_codec/
types.rs

1use std::io::Write;
2
3#[cfg(feature = "serdex")]
4use serde::{Deserialize, Serialize};
5
6use crate::utils::escape_quoted;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub enum Command {
10    Ehlo {
11        domain_or_address: DomainOrAddress,
12    },
13    Helo {
14        domain_or_address: DomainOrAddress,
15    },
16    Mail {
17        reverse_path: String,
18        parameters: Vec<Parameter>,
19    },
20    Rcpt {
21        forward_path: String,
22        parameters: Vec<Parameter>,
23    },
24    Data,
25    Rset,
26    /// This command asks the receiver to confirm that the argument
27    /// identifies a user or mailbox.  If it is a user name, information is
28    /// returned as specified in Section 3.5.
29    ///
30    /// This command has no effect on the reverse-path buffer, the forward-
31    /// path buffer, or the mail data buffer.
32    Vrfy {
33        user_or_mailbox: AtomOrQuoted,
34    },
35    /// This command asks the receiver to confirm that the argument
36    /// identifies a mailing list, and if so, to return the membership of
37    /// that list.  If the command is successful, a reply is returned
38    /// containing information as described in Section 3.5.  This reply will
39    /// have multiple lines except in the trivial case of a one-member list.
40    ///
41    /// This command has no effect on the reverse-path buffer, the forward-
42    /// path buffer, or the mail data buffer, and it may be issued at any
43    /// time.
44    Expn {
45        mailing_list: AtomOrQuoted,
46    },
47    /// This command causes the server to send helpful information to the
48    /// client.  The command MAY take an argument (e.g., any command name)
49    /// and return more specific information as a response.
50    ///
51    /// SMTP servers SHOULD support HELP without arguments and MAY support it
52    /// with arguments.
53    ///
54    /// This command has no effect on the reverse-path buffer, the forward-
55    /// path buffer, or the mail data buffer, and it may be issued at any
56    /// time.
57    Help {
58        argument: Option<AtomOrQuoted>,
59    },
60    /// This command does not affect any parameters or previously entered
61    /// commands.  It specifies no action other than that the receiver send a
62    /// "250 OK" reply.
63    ///
64    ///  If a parameter string is specified, servers SHOULD ignore it.
65    ///
66    /// This command has no effect on the reverse-path buffer, the forward-
67    /// path buffer, or the mail data buffer, and it may be issued at any
68    /// time.
69    Noop {
70        argument: Option<AtomOrQuoted>,
71    },
72    /// This command specifies that the receiver MUST send a "221 OK" reply,
73    /// and then close the transmission channel.
74    ///
75    /// The receiver MUST NOT intentionally close the transmission channel
76    /// until it receives and replies to a QUIT command (even if there was an
77    /// error).  The sender MUST NOT intentionally close the transmission
78    /// channel until it sends a QUIT command, and it SHOULD wait until it
79    /// receives the reply (even if there was an error response to a previous
80    /// command).  If the connection is closed prematurely due to violations
81    /// of the above or system or network failure, the server MUST cancel any
82    /// pending transaction, but not undo any previously completed
83    /// transaction, and generally MUST act as if the command or transaction
84    /// in progress had received a temporary error (i.e., a 4yz response).
85    ///
86    /// The QUIT command may be issued at any time.  Any current uncompleted
87    /// mail transaction will be aborted.
88    Quit,
89    // Extensions
90    StartTLS,
91    // AUTH LOGIN
92    AuthLogin(Option<String>),
93    // AUTH PLAIN
94    AuthPlain(Option<String>),
95}
96
97#[derive(Clone, Debug, PartialEq, Eq)]
98pub enum DomainOrAddress {
99    Domain(String),
100    Address(String),
101}
102
103impl DomainOrAddress {
104    pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
105        match self {
106            DomainOrAddress::Domain(domain) => write!(writer, "{}", domain),
107            DomainOrAddress::Address(address) => write!(writer, "[{}]", address),
108        }
109    }
110}
111
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct Parameter {
114    keyword: String,
115    value: Option<String>,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum AtomOrQuoted {
120    Atom(String),
121    Quoted(String),
122}
123
124impl Command {
125    pub fn name(&self) -> &'static str {
126        match self {
127            Command::Ehlo { .. } => "EHLO",
128            Command::Helo { .. } => "HELO",
129            Command::Mail { .. } => "MAIL",
130            Command::Rcpt { .. } => "RCPT",
131            Command::Data => "DATA",
132            Command::Rset => "RSET",
133            Command::Vrfy { .. } => "VRFY",
134            Command::Expn { .. } => "EXPN",
135            Command::Help { .. } => "HELP",
136            Command::Noop { .. } => "NOOP",
137            Command::Quit => "QUIT",
138            // Extensions
139            Command::StartTLS => "STARTTLS",
140            // TODO: SMTP AUTH LOGIN
141            Command::AuthLogin(_) => "AUTHLOGIN",
142            // TODO: SMTP AUTH PLAIN
143            Command::AuthPlain(_) => "AUTHPLAIN",
144        }
145    }
146
147    pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
148        use Command::*;
149
150        match self {
151            // helo = "HELO" SP Domain CRLF
152            Helo { domain_or_address } => {
153                writer.write_all(b"HELO ")?;
154                domain_or_address.serialize(writer)?;
155            }
156            // ehlo = "EHLO" SP ( Domain / address-literal ) CRLF
157            Ehlo { domain_or_address } => {
158                writer.write_all(b"EHLO ")?;
159                domain_or_address.serialize(writer)?;
160            }
161            // mail = "MAIL FROM:" Reverse-path [SP Mail-parameters] CRLF
162            Mail {
163                reverse_path,
164                parameters,
165            } => {
166                writer.write_all(b"MAIL FROM:<")?;
167                writer.write_all(reverse_path.as_bytes())?;
168                writer.write_all(b">")?;
169
170                for parameter in parameters {
171                    writer.write_all(b" ")?;
172                    parameter.serialize(writer)?;
173                }
174            }
175            // rcpt = "RCPT TO:" ( "<Postmaster@" Domain ">" / "<Postmaster>" / Forward-path ) [SP Rcpt-parameters] CRLF
176            Rcpt {
177                forward_path,
178                parameters,
179            } => {
180                writer.write_all(b"RCPT TO:<")?;
181                writer.write_all(forward_path.as_bytes())?;
182                writer.write_all(b">")?;
183
184                for parameter in parameters {
185                    writer.write_all(b" ")?;
186                    parameter.serialize(writer)?;
187                }
188            }
189            // data = "DATA" CRLF
190            Data => writer.write_all(b"DATA")?,
191            // rset = "RSET" CRLF
192            Rset => writer.write_all(b"RSET")?,
193            // vrfy = "VRFY" SP String CRLF
194            Vrfy { user_or_mailbox } => {
195                writer.write_all(b"VRFY ")?;
196                user_or_mailbox.serialize(writer)?;
197            }
198            // expn = "EXPN" SP String CRLF
199            Expn { mailing_list } => {
200                writer.write_all(b"EXPN ")?;
201                mailing_list.serialize(writer)?;
202            }
203            // help = "HELP" [ SP String ] CRLF
204            Help { argument: None } => writer.write_all(b"HELP")?,
205            Help {
206                argument: Some(data),
207            } => {
208                writer.write_all(b"HELP ")?;
209                data.serialize(writer)?;
210            }
211            // noop = "NOOP" [ SP String ] CRLF
212            Noop { argument: None } => writer.write_all(b"NOOP")?,
213            Noop {
214                argument: Some(data),
215            } => {
216                writer.write_all(b"NOOP ")?;
217                data.serialize(writer)?;
218            }
219            // quit = "QUIT" CRLF
220            Quit => writer.write_all(b"QUIT")?,
221            // ----- Extensions -----
222            // starttls = "STARTTLS" CRLF
223            StartTLS => writer.write_all(b"STARTTLS")?,
224            // auth_login_command = "AUTH LOGIN" [SP username] CRLF
225            AuthLogin(None) => {
226                writer.write_all(b"AUTH LOGIN")?;
227            }
228            AuthLogin(Some(data)) => {
229                writer.write_all(b"AUTH LOGIN ")?;
230                writer.write_all(data.as_bytes())?;
231            }
232            // auth_plain_command = "AUTH PLAIN" [SP base64] CRLF
233            AuthPlain(None) => {
234                writer.write_all(b"AUTH PLAIN")?;
235            }
236            AuthPlain(Some(data)) => {
237                writer.write_all(b"AUTH PLAIN ")?;
238                writer.write_all(data.as_bytes())?;
239            }
240        }
241
242        write!(writer, "\r\n")
243    }
244}
245
246impl Parameter {
247    pub fn new<K: Into<String>, V: Into<String>>(keyword: K, value: Option<V>) -> Parameter {
248        Parameter {
249            keyword: keyword.into(),
250            value: value.map(Into::into),
251        }
252    }
253
254    pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
255        writer.write_all(self.keyword.as_bytes())?;
256
257        if let Some(ref value) = self.value {
258            writer.write_all(b"=")?;
259            writer.write_all(value.as_bytes())?;
260        }
261
262        Ok(())
263    }
264}
265
266impl AtomOrQuoted {
267    pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
268        match self {
269            AtomOrQuoted::Atom(atom) => {
270                writer.write_all(atom.as_bytes())?;
271            }
272            AtomOrQuoted::Quoted(quoted) => {
273                writer.write_all(b"\"")?;
274                writer.write_all(escape_quoted(quoted).as_bytes())?;
275                writer.write_all(b"\"")?;
276            }
277        }
278
279        Ok(())
280    }
281}
282
283// -------------------------------------------------------------------------------------------------
284
285#[cfg_attr(feature = "serdex", derive(Serialize, Deserialize))]
286#[derive(Debug, Clone, PartialEq, Eq)]
287pub enum Response {
288    Greeting {
289        domain: String,
290        text: String,
291    },
292    Ehlo {
293        domain: String,
294        greet: Option<String>,
295        capabilities: Vec<Capability>,
296    },
297    Other {
298        code: u16,
299        text: String,
300    },
301}
302
303impl Response {
304    pub fn greeting<D, T>(domain: D, text: T) -> Response
305    where
306        D: Into<String>,
307        T: Into<String>,
308    {
309        Response::Greeting {
310            domain: domain.into(),
311            text: text.into(),
312        }
313    }
314
315    pub fn ehlo<D, G>(domain: D, greet: Option<G>, capabilities: Vec<Capability>) -> Response
316    where
317        D: Into<String>,
318        G: Into<String>,
319    {
320        Response::Ehlo {
321            domain: domain.into(),
322            greet: greet.map(Into::into),
323            capabilities,
324        }
325    }
326
327    pub fn other<T>(code: u16, text: T) -> Response
328    where
329        T: Into<String>,
330    {
331        Response::Other {
332            code,
333            text: text.into(),
334        }
335    }
336
337    pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
338        match self {
339            Response::Greeting { domain, text } => {
340                let lines = text.lines().collect::<Vec<_>>();
341
342                if let Some((first, tail)) = lines.split_first() {
343                    if let Some((last, head)) = tail.split_last() {
344                        write!(writer, "220-{} {}\r\n", domain, first)?;
345
346                        for line in head {
347                            write!(writer, "220-{}\r\n", line)?;
348                        }
349
350                        write!(writer, "220 {}\r\n", last)?;
351                    } else {
352                        write!(writer, "220 {} {}\r\n", domain, first)?;
353                    }
354                } else {
355                    write!(writer, "220 {}\r\n", domain)?;
356                }
357            }
358            Response::Ehlo {
359                domain,
360                greet,
361                capabilities,
362            } => {
363                let greet = match greet {
364                    Some(greet) => format!(" {}", greet),
365                    None => "".to_string(),
366                };
367
368                if let Some((tail, head)) = capabilities.split_last() {
369                    writer.write_all(format!("250-{}{}\r\n", domain, greet).as_bytes())?;
370
371                    for capability in head {
372                        writer.write_all(b"250-")?;
373                        capability.serialize(writer)?;
374                        writer.write_all(b"\r\n")?;
375                    }
376
377                    writer.write_all(b"250 ")?;
378                    tail.serialize(writer)?;
379                    writer.write_all(b"\r\n")?;
380                } else {
381                    writer.write_all(format!("250 {}{}\r\n", domain, greet).as_bytes())?;
382                }
383            }
384            Response::Other { code, text } => {
385                let lines = text.lines().collect::<Vec<_>>();
386
387                if let Some((last, head)) = lines.split_last() {
388                    for line in head {
389                        write!(writer, "{}-{}\r\n", code, line)?;
390                    }
391
392                    write!(writer, "{} {}\r\n", code, last)?;
393                } else {
394                    write!(writer, "{}\r\n", code)?;
395                }
396            }
397        }
398
399        Ok(())
400    }
401}
402
403// -------------------------------------------------------------------------------------------------
404
405#[cfg_attr(feature = "serdex", derive(Serialize, Deserialize))]
406#[derive(Debug, Clone, PartialEq, Eq)]
407pub enum Capability {
408    // Send as mail [RFC821]
409    // The description of SEND was updated by [RFC1123] and then its actual use was deprecated in [RFC2821]
410    // SEND,
411
412    // Send as mail or to terminal [RFC821]
413    // The description of SOML was updated by [RFC1123] and then its actual use was deprecated in [RFC2821]
414    // SOML,
415
416    // Send as mail and to terminal [RFC821]
417    // The description of SAML was updated by [RFC1123] and then its actual use was deprecated in [RFC2821]
418    // SAML,
419
420    // Interchange the client and server roles [RFC821]
421    // The actual use of TURN was deprecated in [RFC2821]
422    // TURN,
423
424    // SMTP Responsible Submitter [RFC4405]
425    // Deprecated by [https://datatracker.ietf.org/doc/status-change-change-sender-id-to-historic].
426    // SUBMITTER,
427
428    // Internationalized email address [RFC5336]
429    // Experimental; deprecated in [RFC6531].
430    // UTF8SMTP,
431
432    // ---------------------------------------------------------------------------------------------
433    /// Verbose [Eric Allman]
434    // VERB,
435
436    /// One message transaction only [Eric Allman]
437    // ONEX,
438
439    // ---------------------------------------------------------------------------------------------
440
441    /// Expand the mailing list [RFC821]
442    /// Command description updated by [RFC5321]
443    EXPN,
444    /// Supply helpful information [RFC821]
445    /// Command description updated by [RFC5321]
446    Help,
447
448    /// SMTP and Submit transport of 8bit MIME content [RFC6152]
449    EightBitMIME,
450
451    /// Message size declaration [RFC1870]
452    Size(u32),
453
454    /// Chunking [RFC3030]
455    Chunking,
456
457    /// Binary MIME [RFC3030]
458    BinaryMIME,
459
460    /// Checkpoint/Restart [RFC1845]
461    Checkpoint,
462
463    /// Deliver By [RFC2852]
464    DeliverBy,
465
466    /// Command Pipelining [RFC2920]
467    Pipelining,
468
469    /// Delivery Status Notification [RFC3461]
470    DSN,
471
472    /// Extended Turn [RFC1985]
473    /// SMTP [RFC5321] only. Not for use on Submit port 587.
474    ETRN,
475
476    /// Enhanced Status Codes [RFC2034]
477    EnhancedStatusCodes,
478
479    /// Start TLS [RFC3207]
480    StartTLS,
481
482    /// Notification of no soliciting [RFC3865]
483    // NoSoliciting,
484
485    /// Message Tracking [RFC3885]
486    MTRK,
487
488    /// Authenticated TURN [RFC2645]
489    /// SMTP [RFC5321] only. Not for use on Submit port 587.
490    ATRN,
491
492    /// Authentication [RFC4954]
493    Auth(Vec<AuthMechanism>),
494
495    /// Remote Content [RFC4468]
496    /// Submit [RFC6409] only. Not for use with SMTP on port 25.
497    BURL,
498
499    /// Future Message Release [RFC4865]
500    // FutureRelease,
501
502    /// Content Conversion Permission [RFC4141]
503    // ConPerm,
504
505    /// Content Conversion Negotiation [RFC4141]
506    // ConNeg,
507
508    /// Internationalized email address [RFC6531]
509    SMTPUTF8,
510
511    /// Priority Message Handling [RFC6710]
512    // MTPRIORITY,
513
514    /// Require Recipient Valid Since [RFC7293]
515    RRVS,
516
517    /// Require TLS [RFC8689]
518    RequireTLS,
519
520    // Observed ...
521    // TIME,
522    // XACK,
523    // VERP,
524    // VRFY,
525    /// Other
526    Other {
527        keyword: String,
528        params: Vec<String>,
529    },
530}
531
532impl Capability {
533    pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
534        match self {
535            Capability::EXPN => writer.write_all(b"EXPN"),
536            Capability::Help => writer.write_all(b"HELP"),
537            Capability::EightBitMIME => writer.write_all(b"8BITMIME"),
538            Capability::Size(number) => writer.write_all(format!("SIZE {}", number).as_bytes()),
539            Capability::Chunking => writer.write_all(b"CHUNKING"),
540            Capability::BinaryMIME => writer.write_all(b"BINARYMIME"),
541            Capability::Checkpoint => writer.write_all(b"CHECKPOINT"),
542            Capability::DeliverBy => writer.write_all(b"DELIVERBY"),
543            Capability::Pipelining => writer.write_all(b"PIPELINING"),
544            Capability::DSN => writer.write_all(b"DSN"),
545            Capability::ETRN => writer.write_all(b"ETRN"),
546            Capability::EnhancedStatusCodes => writer.write_all(b"ENHANCEDSTATUSCODES"),
547            Capability::StartTLS => writer.write_all(b"STARTTLS"),
548            Capability::MTRK => writer.write_all(b"MTRK"),
549            Capability::ATRN => writer.write_all(b"ATRN"),
550            Capability::Auth(mechanisms) => {
551                if let Some((tail, head)) = mechanisms.split_last() {
552                    writer.write_all(b"AUTH ")?;
553
554                    for mechanism in head {
555                        mechanism.serialize(writer)?;
556                        writer.write_all(b" ")?;
557                    }
558
559                    tail.serialize(writer)
560                } else {
561                    writer.write_all(b"AUTH")
562                }
563            }
564            Capability::BURL => writer.write_all(b"BURL"),
565            Capability::SMTPUTF8 => writer.write_all(b"SMTPUTF8"),
566            Capability::RRVS => writer.write_all(b"RRVS"),
567            Capability::RequireTLS => writer.write_all(b"REQUIRETLS"),
568            Capability::Other { keyword, params } => {
569                if let Some((tail, head)) = params.split_last() {
570                    writer.write_all(keyword.as_bytes())?;
571                    writer.write_all(b" ")?;
572
573                    for param in head {
574                        writer.write_all(param.as_bytes())?;
575                        writer.write_all(b" ")?;
576                    }
577
578                    writer.write_all(tail.as_bytes())
579                } else {
580                    writer.write_all(keyword.as_bytes())
581                }
582            }
583        }
584    }
585}
586
587#[cfg_attr(feature = "serdex", derive(Serialize, Deserialize))]
588#[derive(Debug, Clone, PartialEq, Eq)]
589pub enum AuthMechanism {
590    Plain,
591    Login,
592    GSSAPI,
593
594    CramMD5,
595    CramSHA1,
596    ScramMD5,
597    DigestMD5,
598    NTLM,
599
600    Other(String),
601}
602
603impl AuthMechanism {
604    pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
605        match self {
606            AuthMechanism::Plain => writer.write_all(b"PLAIN"),
607            AuthMechanism::Login => writer.write_all(b"LOGIN"),
608            AuthMechanism::GSSAPI => writer.write_all(b"GSSAPI"),
609
610            AuthMechanism::CramMD5 => writer.write_all(b"CRAM-MD5"),
611            AuthMechanism::CramSHA1 => writer.write_all(b"CRAM-SHA1"),
612            AuthMechanism::ScramMD5 => writer.write_all(b"SCRAM-MD5"),
613            AuthMechanism::DigestMD5 => writer.write_all(b"DIGEST-MD5"),
614            AuthMechanism::NTLM => writer.write_all(b"NTLM"),
615
616            AuthMechanism::Other(other) => writer.write_all(other.as_bytes()),
617        }
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use crate::types::{Capability, Response};
624
625    #[test]
626    fn test_serialize_greeting() {
627        let tests = &[
628            (
629                Response::Greeting {
630                    domain: "example.org".into(),
631                    text: "".into(),
632                },
633                b"220 example.org\r\n".as_ref(),
634            ),
635            (
636                Response::Greeting {
637                    domain: "example.org".into(),
638                    text: "A".into(),
639                },
640                b"220 example.org A\r\n".as_ref(),
641            ),
642            (
643                Response::Greeting {
644                    domain: "example.org".into(),
645                    text: "A\nB".into(),
646                },
647                b"220-example.org A\r\n220 B\r\n".as_ref(),
648            ),
649            (
650                Response::Greeting {
651                    domain: "example.org".into(),
652                    text: "A\nB\nC".into(),
653                },
654                b"220-example.org A\r\n220-B\r\n220 C\r\n".as_ref(),
655            ),
656        ];
657
658        for (test, expected) in tests.into_iter() {
659            let mut got = Vec::new();
660            test.serialize(&mut got).unwrap();
661            assert_eq!(expected, &got);
662        }
663    }
664
665    #[test]
666    fn test_serialize_ehlo() {
667        let tests = &[
668            (
669                Response::Ehlo {
670                    domain: "example.org".into(),
671                    greet: None,
672                    capabilities: vec![],
673                },
674                b"250 example.org\r\n".as_ref(),
675            ),
676            (
677                Response::Ehlo {
678                    domain: "example.org".into(),
679                    greet: Some("...".into()),
680                    capabilities: vec![],
681                },
682                b"250 example.org ...\r\n".as_ref(),
683            ),
684            (
685                Response::Ehlo {
686                    domain: "example.org".into(),
687                    greet: Some("...".into()),
688                    capabilities: vec![Capability::StartTLS],
689                },
690                b"250-example.org ...\r\n250 STARTTLS\r\n".as_ref(),
691            ),
692            (
693                Response::Ehlo {
694                    domain: "example.org".into(),
695                    greet: Some("...".into()),
696                    capabilities: vec![Capability::StartTLS, Capability::Size(12345)],
697                },
698                b"250-example.org ...\r\n250-STARTTLS\r\n250 SIZE 12345\r\n".as_ref(),
699            ),
700        ];
701
702        for (test, expected) in tests.into_iter() {
703            let mut got = Vec::new();
704            test.serialize(&mut got).unwrap();
705            assert_eq!(expected, &got);
706        }
707    }
708
709    #[test]
710    fn test_serialize_other() {
711        let tests = &[
712            (
713                Response::Other {
714                    code: 333,
715                    text: "".into(),
716                },
717                b"333\r\n".as_ref(),
718            ),
719            (
720                Response::Other {
721                    code: 333,
722                    text: "A".into(),
723                },
724                b"333 A\r\n".as_ref(),
725            ),
726            (
727                Response::Other {
728                    code: 333,
729                    text: "A\nB".into(),
730                },
731                b"333-A\r\n333 B\r\n".as_ref(),
732            ),
733        ];
734
735        for (test, expected) in tests.into_iter() {
736            let mut got = Vec::new();
737            test.serialize(&mut got).unwrap();
738            assert_eq!(expected, &got);
739        }
740    }
741}