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}