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}