daaki_smtp/connection/session.rs
1//! Session management: RSET, NOOP, VRFY, EXPN, HELP, QUIT, REHLO.
2//!
3//! RFC 5321 Sections 4.1.1.5–4.1.1.10 (session commands).
4
5#[allow(clippy::wildcard_imports)]
6use super::*;
7
8impl SmtpConnection {
9 // -----------------------------------------------------------------------
10 // Session management
11 // -----------------------------------------------------------------------
12
13 /// Reset the session — RSET command (RFC 5321 Section 4.1.1.5).
14 ///
15 /// Aborts the current mail transaction, if any.
16 pub async fn reset(&self, timeout: Duration) -> Result<(), Error> {
17 self.send_simple_command(encode::encode_rset, "RSET", 250, "4.1.1.5", timeout)
18 .await
19 }
20
21 /// Send a NOOP command (RFC 5321 Section 4.1.1.9).
22 ///
23 /// The NOOP command does not affect any state; the server replies
24 /// with `250 OK`. Useful as a keep-alive probe to prevent idle
25 /// timeouts on long-lived connections.
26 pub async fn noop(&self, timeout: Duration) -> Result<(), Error> {
27 self.send_simple_command(encode::encode_noop, "NOOP", 250, "4.1.1.9", timeout)
28 .await
29 }
30
31 /// Verify a user or mailbox — VRFY command (RFC 5321 Section 4.1.1.6).
32 ///
33 /// Returns the server's response, which may be:
34 /// - 250/251: the user is verified (the response text contains the
35 /// mailbox name)
36 /// - 252: cannot verify the user, but will accept messages and
37 /// attempt delivery
38 /// - 502: VRFY not implemented (RFC 5321 Section 3.5.3 allows servers
39 /// to disable this command)
40 /// - 550: user not found
41 ///
42 /// Valid for both SMTP and LMTP connections. RFC 2033 Section 4.3
43 /// only prohibits HELO, EHLO, and TURN in LMTP; VRFY is "not
44 /// required, but SHOULD be used if possible." If `address` contains
45 /// non-ASCII characters and the server advertised `SMTPUTF8`, this
46 /// method sends `VRFY <arg> SMTPUTF8` per RFC 6531 Section 3.7.4.2.
47 pub async fn vrfy(&self, address: &str, timeout: Duration) -> Result<SmtpResponse, Error> {
48 self.send_simple_query(
49 address,
50 "VRFY",
51 "4.1.1.6",
52 encode::encode_vrfy,
53 encode::encode_vrfy_smtputf8,
54 timeout,
55 )
56 .await
57 }
58
59 /// Expand a mailing list — EXPN command (RFC 5321 Section 4.1.1.7).
60 ///
61 /// Returns the server's response, which may be:
62 /// - 250: the list is expanded (multi-line response with members)
63 /// - 502: EXPN not implemented (RFC 5321 Section 3.5.3 allows servers
64 /// to disable this command)
65 /// - 550: list not found
66 ///
67 /// Valid for both SMTP and LMTP connections. RFC 2033 Section 4.3
68 /// only prohibits HELO, EHLO, and TURN in LMTP; EXPN is "not
69 /// required, but SHOULD be used if possible." If `list_name` contains
70 /// non-ASCII characters and the server advertised `SMTPUTF8`, this
71 /// method sends `EXPN <arg> SMTPUTF8` per RFC 6531 Section 3.7.4.2.
72 pub async fn expn(&self, list_name: &str, timeout: Duration) -> Result<SmtpResponse, Error> {
73 self.send_simple_query(
74 list_name,
75 "EXPN",
76 "4.1.1.7",
77 encode::encode_expn,
78 encode::encode_expn_smtputf8,
79 timeout,
80 )
81 .await
82 }
83
84 /// Request human-readable help text from the server (RFC 5321 Section 4.1.1.8).
85 ///
86 /// Returns the server's response, which is typically:
87 /// - 211: system status or topic-specific help
88 /// - 214: general help text
89 /// - 502: HELP not implemented
90 ///
91 /// The optional `topic` argument is encoded as SMTP `String`
92 /// (`HELP SP String CRLF`) when present. If omitted, the command is sent
93 /// as bare `HELP`.
94 pub async fn help(
95 &self,
96 topic: Option<&str>,
97 timeout: Duration,
98 ) -> Result<SmtpResponse, Error> {
99 if let Some(topic) = topic {
100 if topic.is_empty() {
101 return Err(Error::Protocol(
102 "HELP argument must not be empty when provided \
103 (RFC 5321 Section 4.1.1.8: help = \"HELP\" [ SP String ] CRLF)"
104 .into(),
105 ));
106 }
107 Self::validate_no_crlf(topic, "HELP argument")?;
108 Self::validate_ascii_string(topic, "HELP argument")?;
109 }
110
111 tokio::time::timeout(timeout, async {
112 let mut inner = self.inner.lock().await;
113 Self::ensure_not_shutting_down(&inner)?;
114
115 let mut buf = BytesMut::new();
116 if let Some(topic) = topic {
117 encode::encode_help_with_arg(&mut buf, topic)?;
118 } else {
119 encode::encode_help(&mut buf);
120 }
121
122 inner.write_all(&buf).await?;
123 inner.read_response().await
124 })
125 .await
126 .map_err(|_| Error::Timeout)?
127 }
128
129 /// Send a simple command that expects a success response and returns no data.
130 ///
131 /// Used by commands whose success reply is a specific code rather than any
132 /// generic 2xx completion. RFC 5321 Section 4.2.1 defines the reply-code
133 /// classes, but these commands specify one exact success code each.
134 async fn send_simple_command(
135 &self,
136 encoder: fn(&mut BytesMut),
137 cmd_name: &str,
138 expected_code: u16,
139 rfc_section: &str,
140 timeout: Duration,
141 ) -> Result<(), Error> {
142 let mut inner = self.inner.lock().await;
143 Self::ensure_not_shutting_down(&inner)?;
144 tokio::time::timeout(timeout, async {
145 let mut buf = BytesMut::new();
146 encoder(&mut buf);
147 inner.write_all(&buf).await?;
148 let resp = inner.read_response().await?;
149 if resp.code == expected_code {
150 Ok(())
151 } else if resp.is_success() {
152 Err(Error::Protocol(format!(
153 "{cmd_name} response must be {expected_code}, got {} \
154 (RFC 5321 Section {rfc_section})",
155 resp.code
156 )))
157 } else {
158 Err(Self::response_to_error(resp))
159 }
160 })
161 .await
162 .map_err(|_| Error::Timeout)?
163 }
164
165 /// Shared implementation for VRFY and EXPN (RFC 5321 Sections 4.1.1.6–4.1.1.7).
166 ///
167 /// Both commands follow the same pattern: validate the argument (non-empty,
168 /// no CRLF, ASCII-only unless RFC 6531 `SMTPUTF8` is negotiated), encode
169 /// the command, check line length, send, and read the response.
170 async fn send_simple_query(
171 &self,
172 argument: &str,
173 cmd_name: &str,
174 rfc_section: &str,
175 encoder: fn(&mut BytesMut, &str) -> Result<(), Error>,
176 smtp_utf8_encoder: fn(&mut BytesMut, &str) -> Result<(), Error>,
177 timeout: Duration,
178 ) -> Result<SmtpResponse, Error> {
179 // RFC 5321 Section <rfc_section>: "<CMD>" SP String CRLF — the String
180 // production requires at least one character.
181 if argument.is_empty() {
182 return Err(Error::Protocol(format!(
183 "{cmd_name} argument must not be empty \
184 (RFC 5321 Section {rfc_section}: \
185 String = 1*(%d1-9 / %d11 / %d12 / %d14-127))"
186 )));
187 }
188 let param_name = format!("{cmd_name} argument");
189 // RFC 5321 Section 4.1.2: reject arguments containing CR/LF
190 // to prevent SMTP command injection.
191 Self::validate_no_crlf(argument, ¶m_name)?;
192 let use_smtputf8 = !argument.is_ascii();
193 if use_smtputf8 {
194 let supports_smtputf8 = {
195 let inner = self.inner.lock().await;
196 inner.capabilities.supports_smtputf8()
197 };
198 if !supports_smtputf8 {
199 return Err(Error::Protocol(format!(
200 "{cmd_name} argument contains non-ASCII characters, but the server did not advertise SMTPUTF8 \
201 (RFC 6531 Section 3.7.4.2)"
202 )));
203 }
204 Self::validate_utf8_string(argument, ¶m_name)?;
205 } else {
206 // RFC 5321 Sections 4.1.1.6-4.1.1.7 / Section 4.1.2: VRFY and EXPN
207 // take a `String` argument, not an SMTP `Mailbox`, so validate only
208 // the printable-ASCII / no-control-character constraints here.
209 Self::validate_ascii_string(argument, ¶m_name)?;
210 }
211 tokio::time::timeout(timeout, async {
212 let mut inner = self.inner.lock().await;
213 Self::ensure_not_shutting_down(&inner)?;
214 if use_smtputf8 && !inner.capabilities.supports_smtputf8() {
215 return Err(Error::Protocol(
216 "SMTPUTF8 capability disappeared before command dispatch \
217 (RFC 6531 Section 3.7.4.2)"
218 .into(),
219 ));
220 }
221 let mut buf = BytesMut::new();
222 if use_smtputf8 {
223 smtp_utf8_encoder(&mut buf, argument)?;
224 } else {
225 encoder(&mut buf, argument)?;
226 }
227 // RFC 5321 Section 4.5.3.1.4: validate command line length.
228 Self::validate_command_line_length(buf.len(), cmd_name)?;
229 inner.write_all(&buf).await?;
230 inner.read_response().await
231 })
232 .await
233 .map_err(|_| Error::Timeout)?
234 }
235
236 /// Gracefully close the connection — QUIT command (RFC 5321 Section 4.1.1.10).
237 pub async fn quit(&self, timeout: Duration) -> Result<(), Error> {
238 self.send_simple_command(encode::encode_quit, "QUIT", 221, "4.1.1.10", timeout)
239 .await
240 }
241
242 /// Re-issue the EHLO/LHLO greeting to refresh server capabilities
243 /// (RFC 5321 Section 4.1.1.1).
244 ///
245 /// "An EHLO command MAY be issued by a client later in the session."
246 /// This is useful after [`set_ehlo_domain`](Self::set_ehlo_domain) to
247 /// send the new domain to the server, or to refresh the capability
248 /// snapshot (e.g., after the server has been reconfigured).
249 ///
250 /// Updates the internal [`ServerCapabilities`] with the server's
251 /// fresh EHLO response.
252 pub async fn rehlo(&self, timeout: Duration) -> Result<(), Error> {
253 let mut inner = self.inner.lock().await;
254 Self::ensure_not_shutting_down(&inner)?;
255 tokio::time::timeout(timeout, async {
256 Self::ehlo_on_inner(&mut inner, self.protocol).await
257 })
258 .await
259 .map_err(|_| Error::Timeout)?
260 }
261}