Skip to main content

wasm_smtp/
session.rs

1//! SMTP session state machine.
2//!
3//! [`SessionState`] enumerates the well-defined points in an SMTP exchange.
4//! [`crate::client::SmtpClient`] tracks the current state and uses
5//! [`SessionState::can_transition_to`] to reject API misuse before any byte
6//! is sent on the wire. This converts ordering bugs in caller code into
7//! [`crate::error::InvalidInputError`] returns instead of confusing server
8//! responses.
9//!
10//! ## State diagram
11//!
12//! ```text
13//!   Greeting --> Ehlo --> Authentication --> MailFrom --> RcptTo --> Data
14//!                  ^         |   \                ^             |        |
15//!                  |         |    \               |             |        v
16//!         (re-EHLO |         |     \--------------|             |       Quit
17//!          after   |         |        (skip auth) |             |        |
18//!          TLS)    v         v                    |             v        v
19//!               StartTls<----+                    |         MailFrom   Closed
20//!                                                 |         (next msg)
21//!                                              (loop for more recipients)
22//! ```
23//!
24//! `StartTls` is only entered when the caller invokes
25//! [`crate::SmtpClient::starttls`] on a transport that implements
26//! [`crate::transport::StartTlsCapable`]. After the TLS handshake completes
27//! the state machine transitions back to `Ehlo` to re-issue the greeting
28//! per RFC 3207 §4.2, and from there to `Authentication`.
29//!
30//! Any state may also transition directly to `Quit` and then `Closed` on a
31//! caller-initiated shutdown or to `Closed` on a fatal error.
32
33/// The phases of an SMTP exchange tracked by the client.
34///
35/// This enum is `non_exhaustive` so that future SMTP extensions can add
36/// new phases without forcing a major version bump.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38#[non_exhaustive]
39pub enum SessionState {
40    /// Connection has been established but the server greeting has not yet
41    /// been read.
42    Greeting,
43    /// The greeting has been received but `EHLO` has not yet been sent (or
44    /// has not yet succeeded).
45    Ehlo,
46    /// `EHLO` has succeeded. Authentication may be performed, or skipped.
47    Authentication,
48    /// `STARTTLS` has been issued and accepted (`220` from server). The
49    /// transport is being upgraded; on success the state moves to `Ehlo`
50    /// to re-issue the greeting per RFC 3207 §4.2.
51    StartTls,
52    /// Ready to issue `MAIL FROM` for a new transaction.
53    MailFrom,
54    /// `MAIL FROM` has been accepted; ready to issue `RCPT TO`.
55    RcptTo,
56    /// At least one `RCPT TO` has been accepted; ready to issue `DATA`.
57    Data,
58    /// `QUIT` has been sent; the next operation is to close the transport.
59    Quit,
60    /// The session is finished, either cleanly or due to a fatal error.
61    /// No further SMTP operations are permitted.
62    Closed,
63}
64
65impl SessionState {
66    /// Return `true` if the session is over and no further SMTP operations
67    /// are permitted.
68    pub const fn is_terminal(self) -> bool {
69        matches!(self, Self::Closed)
70    }
71
72    /// Return `true` if `next` is a valid follow-on state from `self`.
73    ///
74    /// This encodes the protocol's ordering rules. The
75    /// [`crate::client::SmtpClient`] consults this before performing any
76    /// operation and returns an [`crate::error::InvalidInputError`] if the
77    /// transition is not allowed.
78    // The arms below are intentionally kept separate so that each represents
79    // one named protocol situation. Combining them into a single OR-pattern
80    // would be terser but would lose the per-case documentation, so we
81    // suppress `match_same_arms` for this function only.
82    #[allow(clippy::match_same_arms)]
83    pub const fn can_transition_to(self, next: Self) -> bool {
84        use SessionState::{
85            Authentication, Closed, Data, Ehlo, Greeting, MailFrom, Quit, RcptTo, StartTls,
86        };
87        match (self, next) {
88            // The transport may close at any time, in which case the client
89            // marks itself Closed.
90            (_, Closed) => true,
91            // QUIT may be sent from any active state.
92            (Greeting | Ehlo | Authentication | MailFrom | RcptTo | Data, Quit) => true,
93            // Normal forward progression.
94            (Greeting, Ehlo) => true,
95            (Ehlo, Authentication) => true,
96            // STARTTLS path: after EHLO succeeds the caller may upgrade.
97            (Authentication, StartTls) => true,
98            // After the TLS upgrade we go back to Ehlo so RFC 3207's
99            // re-EHLO requirement is captured by the same code path that
100            // handles the initial EHLO.
101            (StartTls, Ehlo) => true,
102            // Authentication is optional: we can skip from Ehlo straight to
103            // MailFrom for unauthenticated submission, jump from
104            // Authentication to MailFrom after a successful login, or
105            // re-enter MailFrom to start a new transaction on a session
106            // that just completed one (RFC 5321 §3.3 allows multiple
107            // transactions per session).
108            (Ehlo | Authentication | MailFrom, MailFrom) => true,
109            (MailFrom, RcptTo) => true,
110            // Multiple RCPT TO commands stay in RcptTo.
111            (RcptTo, RcptTo) => true,
112            (RcptTo, Data) => true,
113            // After DATA the same connection can start a new transaction.
114            (Data, MailFrom) => true,
115            _ => false,
116        }
117    }
118}