Skip to main content

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, &param_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, &param_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, &param_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}