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