Skip to main content

daaki_smtp/connection/
lifecycle.rs

1//! Connection lifecycle: connect, EHLO, capabilities, protocol info.
2//!
3//! RFC 5321 Section 3.1 (SMTP session initiation), RFC 2033 Section 4.1
4//! (LMTP LHLO handshake), RFC 8314 Section 3 (implicit TLS),
5//! RFC 3207 (STARTTLS upgrade).
6
7#[allow(clippy::wildcard_imports)]
8use super::*;
9
10impl SmtpConnection {
11    // -----------------------------------------------------------------------
12    // Connection lifecycle
13    // -----------------------------------------------------------------------
14
15    /// Connect to an SMTP server and perform the initial EHLO handshake
16    /// (RFC 5321 Section 3.1).
17    ///
18    /// For `TlsMode::Implicit`, connects over TLS immediately (RFC 8314 Section 3).
19    /// For `TlsMode::StartTls`, connects in cleartext, performs EHLO, upgrades
20    /// via STARTTLS (RFC 3207), then performs a second EHLO.
21    pub async fn connect(
22        host: &str,
23        port: u16,
24        tls_mode: TlsMode,
25        timeout: Duration,
26    ) -> Result<Self, Error> {
27        let tls_config = Self::default_tls_config();
28        Self::connect_inner(host, port, tls_mode, timeout, tls_config, Protocol::Smtp).await
29    }
30
31    /// Connect to an SMTP server with a custom TLS configuration
32    /// (RFC 5321 Section 3.1).
33    pub async fn connect_with_tls_config(
34        host: &str,
35        port: u16,
36        tls_mode: TlsMode,
37        timeout: Duration,
38        tls_config: Arc<rustls::ClientConfig>,
39    ) -> Result<Self, Error> {
40        Self::connect_inner(host, port, tls_mode, timeout, tls_config, Protocol::Smtp).await
41    }
42
43    /// Connect to an LMTP server and perform the initial LHLO handshake
44    /// (RFC 2033 Section 4.1).
45    ///
46    /// LMTP uses LHLO instead of EHLO and returns per-recipient responses
47    /// after DATA (RFC 2033 Section 4.2).
48    pub async fn connect_lmtp(
49        host: &str,
50        port: u16,
51        tls_mode: TlsMode,
52        timeout: Duration,
53    ) -> Result<Self, Error> {
54        let tls_config = Self::default_tls_config();
55        Self::connect_inner(host, port, tls_mode, timeout, tls_config, Protocol::Lmtp).await
56    }
57
58    /// Connect to an LMTP server with a custom TLS configuration
59    /// (RFC 2033 Section 4.1).
60    pub async fn connect_lmtp_with_tls_config(
61        host: &str,
62        port: u16,
63        tls_mode: TlsMode,
64        timeout: Duration,
65        tls_config: Arc<rustls::ClientConfig>,
66    ) -> Result<Self, Error> {
67        Self::connect_inner(host, port, tls_mode, timeout, tls_config, Protocol::Lmtp).await
68    }
69
70    /// Return the connection protocol (SMTP or LMTP).
71    pub fn protocol(&self) -> Protocol {
72        self.protocol
73    }
74
75    /// Return a snapshot of the server's advertised capabilities from the
76    /// last EHLO/LHLO.
77    ///
78    /// Callers should inspect capabilities before choosing protocol paths:
79    /// e.g., check [`ServerCapabilities::supports_chunking`] before using
80    /// [`send_bdat`](Self::send_bdat) (RFC 3030), or
81    /// [`ServerCapabilities::supports_8bitmime`] before declaring
82    /// `BODY=8BITMIME` (RFC 1652).
83    ///
84    /// This method acquires the internal mutex; the returned value is an
85    /// owned clone so no borrow is held after the call returns.
86    pub async fn capabilities(&self) -> ServerCapabilities {
87        self.inner.lock().await.capabilities.clone()
88    }
89
90    /// Returns `true` if the server has sent a 421 response, indicating it
91    /// is shutting down the transmission channel (RFC 5321 Section 3.8).
92    ///
93    /// Once this returns `true`, no further commands should be sent on this
94    /// connection — subsequent send attempts will fail immediately.
95    pub async fn is_shutting_down(&self) -> bool {
96        self.inner.lock().await.server_shutting_down
97    }
98
99    /// Returns `true` if authentication has been completed on this session
100    /// (RFC 4954 Section 3).
101    ///
102    /// Useful for checking session state before attempting to send mail
103    /// or issue additional AUTH commands.
104    pub async fn is_authenticated(&self) -> bool {
105        self.inner.lock().await.authenticated
106    }
107
108    /// RFC 5321 Section 3.8: once a 421 response has been received, the
109    /// server will close the transmission channel and the client must not
110    /// attempt further commands on that connection.
111    pub(super) fn ensure_not_shutting_down(inner: &SmtpInner) -> Result<(), Error> {
112        if inner.server_shutting_down {
113            return Err(Error::Protocol(
114                "connection is shutting down after 421 (RFC 5321 Section 3.8)".into(),
115            ));
116        }
117        Ok(())
118    }
119
120    /// Inner connection logic shared by SMTP and LMTP constructors.
121    ///
122    /// Operates on a bare [`SmtpInner`] during the setup phase (before the
123    /// mutex is constructed) to avoid lock overhead during handshake.
124    async fn connect_inner(
125        host: &str,
126        port: u16,
127        tls_mode: TlsMode,
128        timeout: Duration,
129        tls_config: Arc<rustls::ClientConfig>,
130        protocol: Protocol,
131    ) -> Result<Self, Error> {
132        tokio::time::timeout(timeout, async {
133            let tcp = TcpStream::connect((host, port)).await?;
134            let default_ehlo_domain = default_ehlo_domain()?;
135
136            let mut inner = match tls_mode {
137                TlsMode::Implicit => {
138                    // RFC 8314 Section 3: connect directly over TLS.
139                    let server_name = rustls::pki_types::ServerName::try_from(host.to_owned())
140                        .map_err(|e| Error::Protocol(format!("invalid server name: {e}")))?;
141                    let connector = TlsConnector::from(tls_config.clone());
142                    let tls_stream = connector.connect(server_name, tcp).await?;
143                    SmtpInner {
144                        stream: SmtpStream::Tls(Box::new(tls_stream)),
145                        read_buf: BytesMut::with_capacity(4096),
146                        capabilities: ServerCapabilities::default(),
147                        // RFC 5321 Section 4.1.1.1: EHLO argument is the client's
148                        // FQDN, not the server's hostname. When no better
149                        // identity is known, RFC 5321 Section 4.1.4 says to
150                        // substitute an address-literal.
151                        ehlo_domain: default_ehlo_domain.clone(),
152                        authenticated: false,
153                        server_shutting_down: false,
154                        helo_mode: false,
155                    }
156                }
157                // TlsMode is #[non_exhaustive]; StartTls and None both start
158                // with a plaintext TCP stream — future variants would need
159                // explicit handling here.
160                TlsMode::StartTls | TlsMode::None | _ => SmtpInner {
161                    stream: SmtpStream::Plain(tcp),
162                    read_buf: BytesMut::with_capacity(4096),
163                    capabilities: ServerCapabilities::default(),
164                    ehlo_domain: default_ehlo_domain,
165                    authenticated: false,
166                    server_shutting_down: false,
167                    helo_mode: false,
168                },
169            };
170
171            // Read the server greeting (RFC 5321 Section 3.1).
172            let greeting = inner.read_response().await?;
173
174            // RFC 5321 Section 3.1: "the SMTP server issues a positive
175            // response with the 220 service ready greeting."  Only 220
176            // is a valid greeting code; any other 2xx is a protocol
177            // violation (compare RFC 3207 Section 4 which also requires
178            // exactly 220 for STARTTLS).
179            if greeting.code != 220 {
180                // RFC 5321 Section 4.1.1.10: the client SHOULD send QUIT
181                // before closing the connection for any non-220 greeting,
182                // whether it is a 4xx/5xx rejection or a non-standard 2xx.
183                let mut quit_buf = BytesMut::new();
184                encode::encode_quit(&mut quit_buf);
185                let _ = inner.write_all(&quit_buf).await;
186                let _ = inner.read_response().await;
187
188                if !greeting.is_success() {
189                    // RFC 5321 Section 3.1: 4xx/5xx rejection greeting.
190                    return Err(Self::response_to_error(greeting));
191                }
192                // Non-220 2xx — protocol violation by the server.
193                return Err(Error::Protocol(format!(
194                    "server greeting must be 220, got {} \
195                     (RFC 5321 Section 3.1)",
196                    greeting.code
197                )));
198            }
199
200            // Initial EHLO/LHLO.
201            // RFC 5321 Section 4.1.1.10: from this point on, the 220
202            // greeting has established a session. If any subsequent step
203            // fails, we must send QUIT before closing the connection.
204            if let Err(e) = Self::ehlo_on_inner(&mut inner, protocol).await {
205                inner.quit_best_effort().await;
206                return Err(e);
207            }
208
209            // STARTTLS upgrade (RFC 3207).
210            if tls_mode == TlsMode::StartTls {
211                if !inner.capabilities.supports_starttls() {
212                    inner.quit_best_effort().await;
213                    return Err(Error::StartTlsUnavailable);
214                }
215                let mut buf = BytesMut::new();
216                encode::encode_starttls(&mut buf);
217                inner.write_all(&buf).await?;
218                let resp = inner.read_response().await?;
219                // RFC 3207 Section 4: the only valid success response to
220                // STARTTLS is 220. Other codes (454, 501) indicate failure.
221                if resp.code != 220 {
222                    // RFC 5321 Section 4.1.1.10: send QUIT before closing.
223                    inner.quit_best_effort().await;
224                    return Err(if resp.is_success() {
225                        // Non-220 2xx is unexpected per RFC 3207 §4.
226                        Error::Protocol(format!(
227                            "STARTTLS response must be 220, got {} \
228                             (RFC 3207 Section 4)",
229                            resp.code
230                        ))
231                    } else {
232                        Self::response_to_error(resp)
233                    });
234                }
235                // Upgrade the connection to TLS.
236                let plain_stream = match inner.stream {
237                    SmtpStream::Plain(s) => s,
238                    SmtpStream::Tls(_) => {
239                        return Err(Error::Protocol("already TLS".into()));
240                    }
241                };
242                let server_name = rustls::pki_types::ServerName::try_from(host.to_owned())
243                    .map_err(|e| Error::Protocol(format!("invalid server name: {e}")))?;
244                let connector = TlsConnector::from(tls_config);
245                let tls_stream = connector.connect(server_name, plain_stream).await?;
246                inner.stream = SmtpStream::Tls(Box::new(tls_stream));
247                inner.read_buf.clear();
248                // RFC 3207 Section 4.2: after TLS handshake the SMTP session is
249                // reset to initial state. Clear the authenticated flag and
250                // helo_mode as defense-in-depth — the TLS-upgraded session
251                // may support ESMTP even if the plaintext session did not.
252                inner.authenticated = false;
253                inner.helo_mode = false;
254                // RFC 3207 Section 4.2: the SMTP session resets to initial
255                // state after TLS. Clear stale plaintext capabilities so
256                // that, if the re-EHLO below fails, callers never see
257                // capabilities that were advertised on the insecure channel.
258                inner.capabilities = ServerCapabilities::default();
259
260                // Re-EHLO/LHLO after TLS (RFC 3207 Section 4.2).
261                if let Err(e) = Self::ehlo_on_inner(&mut inner, protocol).await {
262                    inner.quit_best_effort().await;
263                    return Err(e);
264                }
265            }
266
267            Ok(Self {
268                inner: tokio::sync::Mutex::new(inner),
269                protocol,
270            })
271        })
272        .await
273        .map_err(|_| Error::Timeout)?
274    }
275}