daaki_smtp/connection/helpers.rs
1//! Private helpers: validation, MAIL FROM / RCPT TO building, EHLO,
2//! pipelining, error handling, and internal implementation methods.
3//!
4//! RFC 5321 (SMTP), RFC 1870 (SIZE), RFC 6152 (8BITMIME), RFC 6531 (SMTPUTF8),
5//! RFC 3030 (CHUNKING/BINARYMIME), RFC 3461 (DSN), RFC 4954 (AUTH params),
6//! RFC 8689 (REQUIRETLS), RFC 4865 (FUTURERELEASE), RFC 2852 (DELIVERBY),
7//! RFC 6758 (MT-PRIORITY).
8
9#[allow(clippy::wildcard_imports)]
10use super::*;
11
12impl SmtpConnection {
13 // -----------------------------------------------------------------------
14 // Private helpers
15 // -----------------------------------------------------------------------
16
17 /// Validate that an encoded command line does not exceed the 512-octet
18 /// limit (RFC 5321 Section 4.5.3.1.4).
19 ///
20 /// `line_len` is the number of bytes in the command line including the
21 /// terminating CRLF. `cmd_name` is used in the error message.
22 ///
23 /// For MAIL FROM, use [`validate_mail_from_line_length`] instead —
24 /// ESMTP extensions increase the limit beyond 512.
25 pub(super) fn validate_command_line_length(
26 line_len: usize,
27 cmd_name: &str,
28 ) -> Result<(), Error> {
29 if line_len > Self::SMTP_MAX_COMMAND_LINE {
30 return Err(Error::Protocol(format!(
31 "{cmd_name} command line exceeds {}-octet limit ({line_len} octets) \
32 (RFC 5321 Section 4.5.3.1.4)",
33 Self::SMTP_MAX_COMMAND_LINE
34 )));
35 }
36 Ok(())
37 }
38
39 /// Validate that a MAIL FROM command line does not exceed the
40 /// ESMTP-extended limit (RFC 5321 Section 4.5.3.1.4).
41 ///
42 /// ESMTP extensions increase the MAIL FROM limit beyond the base
43 /// 512 octets: RFC 1870 Section 4 (+26 for SIZE), RFC 6152
44 /// Section 7 (+17 for BODY=), RFC 6531 Section 3.4 (+10 for
45 /// SMTPUTF8), RFC 4865 Section 5 (+34 for HOLDFOR/HOLDUNTIL),
46 /// and RFC 4954 Section 3 (+500 for AUTH=).
47 pub(super) fn validate_mail_from_line_length(line_len: usize) -> Result<(), Error> {
48 if line_len > Self::SMTP_MAX_MAIL_FROM_LINE {
49 return Err(Error::Protocol(format!(
50 "MAIL FROM command line exceeds {}-octet limit ({line_len} octets) \
51 (RFC 5321 Section 4.5.3.1.4 / RFC 1870 Section 4 / \
52 RFC 6152 Section 7 / RFC 6531 Section 3.4 / \
53 RFC 4865 Section 5 / RFC 4954 Section 3)",
54 Self::SMTP_MAX_MAIL_FROM_LINE
55 )));
56 }
57 Ok(())
58 }
59
60 /// Validate that a RCPT TO command line does not exceed the applicable
61 /// line-length limit.
62 ///
63 /// RFC 5321 Section 4.5.3.1.4: base limit is 512 octets.
64 /// RFC 3461 Section 5: when DSN parameters (NOTIFY, ORCPT) are present,
65 /// the limit increases by 500 octets to 1012 octets.
66 pub(super) fn validate_rcpt_to_line_length(
67 line_len: usize,
68 has_dsn_params: bool,
69 ) -> Result<(), Error> {
70 let limit = if has_dsn_params {
71 Self::SMTP_MAX_RCPT_TO_DSN_LINE
72 } else {
73 Self::SMTP_MAX_COMMAND_LINE
74 };
75 if line_len > limit {
76 return Err(Error::Protocol(format!(
77 "RCPT TO command line exceeds {limit}-octet limit ({line_len} octets) \
78 (RFC 5321 Section 4.5.3.1.4{})",
79 if has_dsn_params {
80 " / RFC 3461 Section 5"
81 } else {
82 ""
83 }
84 )));
85 }
86 Ok(())
87 }
88
89 /// Validate that a recipient address is non-empty.
90 ///
91 /// RFC 5321 Section 4.1.1.3: `Forward-path = Path = "<" Mailbox ">"`.
92 /// Validate sender and recipient addresses for a send operation.
93 ///
94 /// Shared by `send_with_params`, `send_bdat`, `send_lmtp`, and `send_lmtp_bdat`.
95 /// Since the addresses are already validated by the [`ReversePath`] and
96 /// [`ForwardPath`] constructors, this only checks that at least one
97 /// recipient is present (RFC 5321 Section 3.3).
98 ///
99 /// RFC 5321 Section 3.3.
100 pub(super) fn validate_send_addresses(
101 _from: &ReversePath,
102 recipients: &[ForwardPath],
103 ) -> Result<(), Error> {
104 // RFC 5321 Section 3.3: at least one RCPT TO is required.
105 if recipients.is_empty() {
106 return Err(Error::Protocol(
107 "at least one recipient is required (RFC 5321 Section 3.3)".into(),
108 ));
109 }
110 Ok(())
111 }
112
113 /// Validate that a string parameter does not contain CR or LF bytes
114 /// that could cause SMTP command injection.
115 ///
116 /// RFC 5321 Section 4.1.2: SMTP command arguments follow the `mailbox`
117 /// and `Domain` grammars, which prohibit bare CR and LF.
118 pub(super) fn validate_no_crlf(value: &str, param_name: &str) -> Result<(), Error> {
119 if value.bytes().any(|b| b == b'\r' || b == b'\n') {
120 return Err(Error::Protocol(format!(
121 "{param_name} must not contain CR or LF (RFC 5321 Section 4.1.2)"
122 )));
123 }
124 Ok(())
125 }
126
127 /// Validate a general SMTP `String` argument as printable US-ASCII.
128 ///
129 /// RFC 5321 Sections 4.1.1.6-4.1.1.7 use `String` for VRFY/EXPN
130 /// arguments. This helper enforces the transport-safety constraints:
131 /// no ASCII control characters and no non-ASCII bytes.
132 pub(super) fn validate_ascii_string(value: &str, param_name: &str) -> Result<(), Error> {
133 for &b in value.as_bytes() {
134 if b < 0x20 || b == 0x7F {
135 return Err(Error::Protocol(format!(
136 "{param_name} contains control character (byte 0x{b:02X}); \
137 only printable US-ASCII is permitted \
138 (RFC 5321 Section 4.1.2)"
139 )));
140 }
141 if b > 0x7F {
142 return Err(Error::Protocol(format!(
143 "{param_name} contains non-ASCII characters; \
144 only printable US-ASCII is permitted \
145 (RFC 5321 Section 4.1.2)"
146 )));
147 }
148 }
149 Ok(())
150 }
151
152 /// Validate a VRFY/EXPN `String` argument when RFC 6531 `SMTPUTF8`
153 /// is in use.
154 ///
155 /// RFC 6531 Section 3.7.4.2 allows non-ASCII UTF-8 in VRFY/EXPN
156 /// arguments when the `SMTPUTF8` parameter is sent, but SMTP command
157 /// arguments still must not contain ASCII control characters
158 /// (RFC 5321 Section 4.1.2).
159 pub(super) fn validate_utf8_string(value: &str, param_name: &str) -> Result<(), Error> {
160 for &b in value.as_bytes() {
161 if b < 0x20 || b == 0x7F {
162 return Err(Error::Protocol(format!(
163 "{param_name} contains control character (byte 0x{b:02X}); \
164 SMTPUTF8 does not permit ASCII control characters in VRFY/EXPN arguments \
165 (RFC 5321 Section 4.1.2 / RFC 6531 Section 3.7.4.2)"
166 )));
167 }
168 }
169 Ok(())
170 }
171
172 /// Validate message data for the DATA path.
173 ///
174 /// Delegates to shared validators for NUL bytes, bare CR/LF, and
175 /// line length. The DATA path adds CRLF before the terminating dot,
176 /// so trailing-segment length includes +2 for the implicit CRLF.
177 ///
178 /// # Checks
179 /// 1. **RFC 5321 Section 4.5.2** — no NUL bytes (0x00).
180 /// 2. **RFC 5321 Section 2.3.8** — no bare CR or bare LF.
181 /// 3. **RFC 5321 Section 4.5.3.1.6** — text lines ≤ 1000 octets
182 /// (including CRLF, not counting the transparency dot).
183 pub(super) fn validate_data_message(data: &[u8]) -> Result<(), Error> {
184 Self::validate_no_nul_bytes_in_data(data)?;
185 Self::validate_no_bare_crlf(data)?;
186 // DATA adds CRLF before the terminating dot (RFC 5321 Section
187 // 4.1.1.4), so the trailing segment's effective length is +2.
188 // RFC 5321 Section 4.5.3.1.6: the 1000-octet limit explicitly
189 // excludes "the leading dot duplicated for transparency", so
190 // dot-stuffing overhead is not counted against the limit.
191 Self::validate_text_line_lengths(data, true)?;
192 Ok(())
193 }
194
195 /// Validate that message data contains no NUL bytes (DATA path).
196 ///
197 /// RFC 5321 Section 4.5.2: "In the absence of a server-offered
198 /// SMTP extension explicitly permitting it, a sending SMTP system
199 /// MUST NOT send a message that contains octets with a value of
200 /// 0x00."
201 pub(super) fn validate_no_nul_bytes_in_data(data: &[u8]) -> Result<(), Error> {
202 if data.contains(&0x00) {
203 return Err(Error::Protocol(
204 "message data contains NUL byte (0x00); \
205 SMTP DATA must not contain NUL octets \
206 (RFC 5321 Section 4.5.2)"
207 .into(),
208 ));
209 }
210 Ok(())
211 }
212
213 /// Validate that message data uses CRLF line endings with no bare
214 /// CR or bare LF.
215 ///
216 /// RFC 5321 Section 2.3.8: SMTP uses CRLF as the line ending; a
217 /// bare CR (`\r` not followed by `\n`) or bare LF (`\n` not
218 /// preceded by `\r`) violates the protocol and can cause
219 /// interoperability failures.
220 ///
221 /// Shared by both the DATA and BDAT (without BINARYMIME) paths.
222 /// RFC 3030 Section 3: "The CHUNKING service extension does not
223 /// modify in any way the requirements for the content of the
224 /// message."
225 pub(super) fn validate_no_bare_crlf(data: &[u8]) -> Result<(), Error> {
226 for i in 0..data.len() {
227 if data[i] == b'\r' && data.get(i + 1) != Some(&b'\n') {
228 return Err(Error::Protocol(
229 "message data contains bare CR (\\r not followed by \\n); \
230 SMTP requires CRLF line endings (RFC 5321 Section 2.3.8)"
231 .into(),
232 ));
233 }
234 if data[i] == b'\n' && (i == 0 || data[i - 1] != b'\r') {
235 return Err(Error::Protocol(
236 "message data contains bare LF (\\n not preceded by \\r); \
237 SMTP requires CRLF line endings (RFC 5321 Section 2.3.8)"
238 .into(),
239 ));
240 }
241 }
242 Ok(())
243 }
244
245 /// Validate that no text line exceeds 1000 octets (including CRLF).
246 ///
247 /// RFC 5321 Section 4.5.3.1.6: "The maximum total length of a text
248 /// line including the `<CRLF>` is 1000 octets (not counting the
249 /// leading dot duplicated for transparency)."
250 ///
251 /// `trailing_crlf_padding`: when `true`, the trailing segment
252 /// (data not ending with CRLF) has +2 added to its effective
253 /// length because the DATA path adds CRLF before the terminating
254 /// dot (RFC 5321 Section 4.1.1.4). For BDAT, pass `false` since
255 /// no implicit CRLF is appended.
256 ///
257 /// Shared by both the DATA and BDAT (without BINARYMIME) paths.
258 pub(super) fn validate_text_line_lengths(
259 data: &[u8],
260 trailing_crlf_padding: bool,
261 ) -> Result<(), Error> {
262 let mut line_start: usize = 0;
263
264 for i in 0..data.len() {
265 // RFC 5321 Section 4.5.3.1.6: check line length at each CRLF.
266 if data[i] == b'\n' && i > 0 && data[i - 1] == b'\r' {
267 let line_len = i + 1 - line_start; // includes CRLF
268 if line_len > Self::SMTP_MAX_TEXT_LINE {
269 return Err(Error::Protocol(format!(
270 "message text line exceeds {}-octet limit ({line_len} octets) \
271 (RFC 5321 Section 4.5.3.1.6)",
272 Self::SMTP_MAX_TEXT_LINE
273 )));
274 }
275 line_start = i + 1;
276 }
277 }
278
279 // Check the trailing segment (data not ending with CRLF).
280 let remaining = data.len() - line_start;
281 if remaining > 0 {
282 let effective_len = if trailing_crlf_padding {
283 // DATA adds CRLF before the terminating dot.
284 remaining + 2
285 } else {
286 remaining
287 };
288 if effective_len > Self::SMTP_MAX_TEXT_LINE {
289 return Err(Error::Protocol(format!(
290 "message text line exceeds {}-octet limit ({effective_len} octets) \
291 (RFC 5321 Section 4.5.3.1.6)",
292 Self::SMTP_MAX_TEXT_LINE
293 )));
294 }
295 }
296
297 Ok(())
298 }
299
300 /// RFC 5321 Section 4.5.3.1.6: "The maximum total length of a text
301 /// line including the `<CRLF>` is 1000 octets (not counting the
302 /// leading dot duplicated for transparency)."
303 pub(super) const SMTP_MAX_TEXT_LINE: usize = 1000;
304
305 /// Compute the message size for the RFC 1870 SIZE parameter.
306 ///
307 /// RFC 1870 Section 5: "The message size is defined as the number
308 /// of octets, including CR-LF pairs, but not the SMTP DATA
309 /// command's terminating dot or doubled quoting dots."
310 /// The SIZE parameter uses the raw message size — dot-stuffing
311 /// overhead is SMTP transfer encoding and is excluded.
312 ///
313 /// RFC 1870 Section 3: SIZE includes "the terminating CRLF of the
314 /// last line." When the message does not already end with CRLF, the
315 /// sender adds one before the terminating dot (RFC 5321 Section
316 /// 4.1.1.4), and this implicit CRLF must be counted.
317 pub(super) fn data_message_size(message: &[u8]) -> usize {
318 // RFC 1870 Section 5: "The message size is defined as the number
319 // of octets, including CR-LF pairs, but not the SMTP DATA
320 // command's terminating dot or doubled quoting dots."
321 // The SIZE parameter uses the raw message size — dot-stuffing
322 // overhead is SMTP transfer encoding and is excluded.
323 if message.ends_with(b"\r\n") {
324 message.len()
325 } else {
326 // RFC 1870 Section 3 / RFC 5321 Section 4.1.1.4: the DATA
327 // path adds an implicit CRLF before the terminating dot when
328 // the message does not already end with CRLF. This implicit
329 // CRLF terminates the last line of the message and is part
330 // of the message content, so it is included in the size.
331 message.len() + 2
332 }
333 }
334
335 /// Send the MAIL FROM command to the server and check for success.
336 ///
337 /// RFC 5321 Section 4.1.1.2: Encodes the command with optional ESMTP
338 /// parameters, validates command-line length (Section 4.5.3.1.4),
339 /// sends it, and verifies the server's response.
340 pub(super) async fn send_mail_from(
341 inner: &mut SmtpInner,
342 from: &ReversePath,
343 message: &[u8],
344 message_size: usize,
345 params: Option<&crate::types::MailFromParams>,
346 ) -> Result<(), Error> {
347 let mut buf = BytesMut::new();
348 let is_8bit = Self::message_contains_8bit(message);
349 Self::encode_mail_from_cmd(
350 &inner.capabilities,
351 &mut buf,
352 from,
353 message_size,
354 params,
355 is_8bit,
356 )?;
357 // RFC 5321 Section 4.5.3.1.4 / RFC 1870 Section 4 / RFC 6152
358 // Section 7 / RFC 6531 Section 3.4: MAIL FROM has an extended
359 // command line limit to accommodate ESMTP parameters.
360 Self::validate_mail_from_line_length(buf.len())?;
361 inner.write_all(&buf).await?;
362 let resp = inner.read_response().await?;
363 if !resp.is_success() {
364 return Err(Self::response_to_error(resp));
365 }
366 Ok(())
367 }
368
369 /// Send RCPT TO for each recipient and collect results.
370 ///
371 /// RFC 5321 Section 4.1.1.3: sends a RCPT TO command for each recipient,
372 /// tracking which were accepted and which were rejected. If all recipients
373 /// are rejected, resets the transaction (RSET) and returns
374 /// [`Error::AllRecipientsFailed`].
375 ///
376 /// When `rcpt_params` is `Some`, each recipient is paired by index with
377 /// its corresponding [`RcptToParams`] and encoded via
378 /// [`encode_rcpt_to_full`](crate::codec::encode::encode_rcpt_to_full)
379 /// to include DSN parameters (NOTIFY, ORCPT per RFC 3461 Sections 4.1–4.2).
380 /// When `None`, the plain [`encode_rcpt_to`](crate::codec::encode::encode_rcpt_to)
381 /// is used.
382 ///
383 /// Returns the list of accepted recipient addresses and the list of
384 /// rejected recipients with their server responses (RFC 5321 Section 3.3).
385 pub(super) async fn send_rcpt_to_batch(
386 inner: &mut SmtpInner,
387 recipients: &[ForwardPath],
388 rcpt_params: Option<&[crate::types::RcptToParams]>,
389 ) -> Result<(Vec<ForwardPath>, Vec<crate::types::RejectedRecipient>), Error> {
390 let mut buf = BytesMut::new();
391 let mut accepted = Vec::new();
392 let mut rejected = Vec::new();
393 for (i, fp) in recipients.iter().enumerate() {
394 buf.clear();
395 if let Some(params) = rcpt_params {
396 // RFC 3461 Sections 4.1–4.2: encode with DSN parameters.
397 encode::encode_rcpt_to_full(&mut buf, fp, ¶ms[i])?;
398 } else {
399 encode::encode_rcpt_to(&mut buf, fp)?;
400 }
401 // RFC 5321 Section 4.5.3.1.4 / RFC 3461 Section 5: validate
402 // command line length, using the extended 1012-octet limit when
403 // DSN parameters are present for this recipient.
404 let has_dsn = rcpt_params.is_some_and(|rp| !rp[i].is_empty());
405 Self::validate_rcpt_to_line_length(buf.len(), has_dsn)?;
406 inner.write_all(&buf).await?;
407 let resp = inner.read_response().await?;
408 if resp.is_success() {
409 accepted.push(fp.clone());
410 } else {
411 tracing::debug!(
412 recipient = fp.as_str(),
413 code = resp.code,
414 "RCPT TO rejected"
415 );
416 rejected.push(crate::types::RejectedRecipient {
417 recipient: fp.clone(),
418 response: resp,
419 });
420 }
421 }
422
423 if accepted.is_empty() {
424 // RFC 5321 Section 3.3: abort the mail transaction opened by
425 // MAIL FROM so the connection is left in a clean state.
426 inner.rset_best_effort().await;
427 return Err(Error::AllRecipientsFailed {
428 count: recipients.len(),
429 // Extract just the responses for the error variant.
430 responses: rejected.into_iter().map(|r| r.response).collect(),
431 });
432 }
433
434 Ok((accepted, rejected))
435 }
436
437 /// Validate all prerequisites for the DATA send path and return message size
438 /// (RFC 5321 Section 3.3, RFC 3030 Section 2, RFC 1870 Section 3).
439 ///
440 /// Shared by `send_with_params` and `send_lmtp`. Checks:
441 /// - BODY=BINARYMIME is rejected (incompatible with DATA, RFC 3030 Section 2)
442 /// - ESMTP params are valid for server capabilities (RFC 5321 Section 2.2.1)
443 /// - Message size within server limit (RFC 1870 Sections 3–4)
444 /// - DATA message format: CRLF line endings, line length (RFC 5321 Section 2.3.8)
445 /// - 7-bit content when required (RFC 1652 Section 1 / RFC 6152 Section 3)
446 pub(super) fn validate_data_prerequisites(
447 capabilities: &ServerCapabilities,
448 message: &[u8],
449 params: Option<&crate::types::MailFromParams>,
450 is_tls: bool,
451 ) -> Result<usize, Error> {
452 // RFC 3030 Section 2: "BINARYMIME cannot be used with the DATA
453 // command." The DATA path applies dot-stuffing which corrupts
454 // binary content.
455 if let Some(p) = params {
456 if p.body == Some(crate::types::BodyType::BinaryMime) {
457 return Err(Error::Protocol(
458 "BODY=BINARYMIME cannot be used with DATA; use BDAT/CHUNKING \
459 instead (RFC 3030 Section 2)"
460 .into(),
461 ));
462 }
463 }
464 // RFC 5321 Section 2.2.1: validate ESMTP params against capabilities.
465 Self::validate_params(capabilities, params, is_tls)?;
466 // RFC 1870 Section 3: SIZE uses the original message size (excluding
467 // dot-stuffing overhead, which is protocol overhead per RFC 5321 Section 4.5.2).
468 let message_size = Self::data_message_size(message);
469 // RFC 1870 Section 4: check message size against server limit.
470 Self::check_size_limit(capabilities, message_size)?;
471 // RFC 5321 Section 2.3.8: DATA requires CRLF line endings.
472 Self::validate_data_message(message)?;
473 // RFC 1652 Section 1 / RFC 5321 Section 2.3.1: without 8BITMIME,
474 // SMTP is limited to 7-bit content.
475 // RFC 6152 Section 3: BODY=7BIT declares content is all 7-bit;
476 // enforce this regardless of server capabilities.
477 if Self::is_body_declared_7bit(params) || !capabilities.supports_8bitmime() {
478 Self::validate_7bit_content(message)?;
479 }
480 Ok(message_size)
481 }
482
483 /// Validate all prerequisites for the BDAT send path
484 /// (RFC 3030 Section 3, RFC 1870 Section 4).
485 ///
486 /// Shared by `send_bdat` and `send_lmtp_bdat`. Checks:
487 /// - CHUNKING extension is available (RFC 3030 Section 3)
488 /// - ESMTP params are valid for server capabilities (RFC 5321 Section 2.2.1)
489 /// - Message size within server limit (RFC 1870 Section 4)
490 /// - NUL byte and 7-bit content restrictions (RFC 5321 Section 4.5.2)
491 pub(super) fn validate_bdat_prerequisites(
492 capabilities: &ServerCapabilities,
493 message: &[u8],
494 params: Option<&crate::types::MailFromParams>,
495 is_tls: bool,
496 ) -> Result<(), Error> {
497 // RFC 3030 Section 3: CHUNKING extension required.
498 if !capabilities.supports_chunking() {
499 return Err(Error::Protocol(
500 "server does not support CHUNKING (RFC 3030)".into(),
501 ));
502 }
503 // RFC 5321 Section 2.2.1: validate ESMTP params against capabilities.
504 // Note: BODY=BINARYMIME is valid with BDAT (RFC 3030 Section 2).
505 Self::validate_params(capabilities, params, is_tls)?;
506 // RFC 1870 Section 4: check message size against server limit.
507 Self::check_size_limit(capabilities, message.len())?;
508 // BDAT does not require CRLF line endings or enforce line length limits
509 // (RFC 3030 Section 3). However, NUL and 7-bit content restrictions
510 // still apply without BINARYMIME/8BITMIME.
511 Self::validate_bdat_content(capabilities, message, params)?;
512 Ok(())
513 }
514
515 /// Validate message content for BDAT/CHUNKING paths.
516 ///
517 /// RFC 3030 Section 3: "The CHUNKING service extension does not
518 /// modify in any way the requirements for the content of the
519 /// message." Without BINARYMIME, all standard SMTP content
520 /// requirements apply:
521 ///
522 /// - **RFC 5321 Section 4.5.2** — no NUL bytes.
523 /// - **RFC 5321 Section 2.3.8** — no bare CR or bare LF.
524 /// - **RFC 5321 Section 4.5.3.1.6** — text lines ≤ 1000 octets
525 /// (including CRLF).
526 /// - **RFC 1652 Section 1** — 7-bit content unless 8BITMIME.
527 /// - **RFC 6152 Section 3** — BODY=7BIT forces 7-bit.
528 ///
529 /// RFC 3030 Section 4 allows arbitrary octets for BINARYMIME payloads,
530 /// but RFC 3030 Section 2 still requires canonical CRLF line endings for
531 /// top-level `text/*` MIME entities.
532 pub(super) fn validate_bdat_content(
533 capabilities: &ServerCapabilities,
534 message: &[u8],
535 params: Option<&crate::types::MailFromParams>,
536 ) -> Result<(), Error> {
537 let is_binary = params.is_some_and(|p| p.body == Some(crate::types::BodyType::BinaryMime));
538 if is_binary {
539 // RFC 3030 Section 2: a BINARYMIME message is still expected to be
540 // canonical MIME. In particular, top-level text/* content MUST use
541 // CRLF line endings even though non-text bodies may carry arbitrary
542 // octets.
543 if Self::binarymime_requires_canonical_text_lines(message) {
544 Self::validate_no_bare_crlf(message)?;
545 }
546 return Ok(());
547 }
548
549 Self::validate_no_nul_bytes(message)?;
550 // RFC 3030 Section 3: content requirements are unchanged
551 // without BINARYMIME — enforce CRLF and line length.
552 Self::validate_no_bare_crlf(message)?;
553 // BDAT does not append CRLF before a terminator (no dot
554 // terminator), so trailing_crlf_padding is false.
555 Self::validate_text_line_lengths(message, false)?;
556 if Self::is_body_declared_7bit(params) || !capabilities.supports_8bitmime() {
557 Self::validate_7bit_content(message)?;
558 }
559 Ok(())
560 }
561
562 /// Returns `true` when a BINARYMIME payload still needs canonical line
563 /// validation because its top-level MIME type contains line-oriented
564 /// message syntax.
565 ///
566 /// RFC 3030 Section 2 says the MIME message itself must be properly
567 /// formed and specifically calls out `text/*` as requiring
568 /// CRLF-terminated lines. The same canonical requirement applies to the
569 /// textual structure of `multipart/*` and `message/*` entities, which
570 /// carry MIME boundaries, nested headers, and other RFC 5322-style line
571 /// syntax. When `Content-Type` is missing or malformed, MIME defaults to
572 /// `text/plain` (RFC 2045 Section 5.2), so the conservative choice is to
573 /// keep canonical line validation enabled.
574 pub(super) fn binarymime_requires_canonical_text_lines(message: &[u8]) -> bool {
575 match Self::top_level_mime_type(message) {
576 Some(mime_type) => {
577 mime_type.starts_with("text/")
578 || mime_type.starts_with("multipart/")
579 || mime_type.starts_with("message/")
580 }
581 None => true,
582 }
583 }
584
585 /// Extract the top-level MIME media type from the RFC 5322 header block.
586 ///
587 /// Returns the normalized `type/subtype` token without parameters, or
588 /// `None` when no usable `Content-Type` field is present.
589 ///
590 /// RFC 2045 Section 5.1 defines `Content-Type`; RFC 2045 Section 5.2
591 /// defaults a missing field to `text/plain`.
592 pub(super) fn top_level_mime_type(message: &[u8]) -> Option<String> {
593 let header_len = Self::header_block_len(message);
594 if header_len == 0 {
595 return None;
596 }
597
598 let content_type = Self::header_field_value(&message[..header_len], "content-type")?;
599 let raw_type = content_type
600 .split(';')
601 .next()
602 .unwrap_or_default()
603 .trim()
604 .to_ascii_lowercase();
605 let slash = raw_type.find('/')?;
606 let type_part = raw_type[..slash].trim();
607 let subtype_part = raw_type[slash + 1..].trim();
608 if type_part.is_empty() || subtype_part.is_empty() {
609 return None;
610 }
611 Some(format!("{type_part}/{subtype_part}"))
612 }
613
614 /// Extract a case-insensitive RFC 5322 header field value from a header block.
615 ///
616 /// Continuation lines are unfolded by joining them with a single space
617 /// (RFC 5322 Section 2.2.3).
618 pub(super) fn header_field_value(header_block: &[u8], target_name: &str) -> Option<String> {
619 let mut current_name: Option<String> = None;
620 let mut current_value = String::new();
621
622 for raw_line in header_block.split(|&byte| byte == b'\n') {
623 let line = raw_line.strip_suffix(b"\r").unwrap_or(raw_line);
624 if line.is_empty() {
625 break;
626 }
627
628 if matches!(line.first(), Some(b' ' | b'\t')) {
629 if current_name.as_deref() == Some(target_name) {
630 if !current_value.is_empty() {
631 current_value.push(' ');
632 }
633 current_value.push_str(String::from_utf8_lossy(line).trim());
634 }
635 continue;
636 }
637
638 if current_name.as_deref() == Some(target_name) {
639 return Some(current_value);
640 }
641
642 let colon = line.iter().position(|&byte| byte == b':')?;
643 current_name = Some(String::from_utf8_lossy(&line[..colon]).to_ascii_lowercase());
644 current_value.clear();
645 current_value.push_str(String::from_utf8_lossy(&line[colon + 1..]).trim());
646 }
647
648 (current_name.as_deref() == Some(target_name)).then_some(current_value)
649 }
650
651 /// Check if MAIL FROM params declare BODY=7BIT.
652 ///
653 /// RFC 6152 Section 3: BODY=7BIT declares content is all 7-bit;
654 /// the library must enforce this regardless of server capabilities.
655 pub(super) fn is_body_declared_7bit(params: Option<&crate::types::MailFromParams>) -> bool {
656 params.is_some_and(|p| p.body == Some(crate::types::BodyType::SevenBit))
657 }
658
659 /// Validate that message data does not contain NUL bytes.
660 ///
661 /// RFC 5321 Section 4.5.2: "In the absence of a server-offered SMTP
662 /// extension explicitly permitting it, a sending SMTP system MUST NOT
663 /// send a message that contains octets with a value of 0x00."
664 ///
665 /// The BINARYMIME extension (RFC 3030 Section 4) permits NUL bytes;
666 /// CHUNKING alone does not.
667 pub(super) fn validate_no_nul_bytes(data: &[u8]) -> Result<(), Error> {
668 if data.contains(&0x00) {
669 return Err(Error::Protocol(
670 "message data contains NUL byte (0x00); \
671 SMTP MUST NOT send NUL octets without BINARYMIME \
672 (RFC 5321 Section 4.5.2 / RFC 3030 Section 4)"
673 .into(),
674 ));
675 }
676 Ok(())
677 }
678
679 /// Validate that message data contains only 7-bit content.
680 ///
681 /// RFC 1652 Section 1: "In the absence of [the 8BITMIME] extension,
682 /// an SMTP client MUST NOT transmit 8-bit data in the mail body."
683 ///
684 /// RFC 5321 Section 2.3.1: without 8BITMIME, SMTP is limited to the
685 /// transmission of 7-bit US-ASCII content.
686 ///
687 /// This check applies on non-BINARYMIME send paths whenever the server
688 /// does NOT advertise 8BITMIME.
689 pub(super) fn validate_7bit_content(data: &[u8]) -> Result<(), Error> {
690 for &b in data {
691 if b > 0x7F {
692 return Err(Error::Protocol(
693 "message contains 8-bit content (bytes > 0x7F) but the server \
694 does not advertise 8BITMIME; use BODY=8BITMIME with a server \
695 that supports it, or encode the message as 7-bit \
696 (RFC 1652 Section 1 / RFC 5321 Section 2.3.1)"
697 .into(),
698 ));
699 }
700 }
701 Ok(())
702 }
703
704 /// Encode a MAIL FROM command into `buf`, merging caller-supplied
705 /// [`MailFromParams`] with the SIZE parameter derived from the server's
706 /// advertised capabilities (RFC 1870 Section 3).
707 ///
708 /// When `params` is `None`, the basic `MAIL FROM:<addr>` form is used
709 /// with an optional SIZE parameter. When `params` is `Some`, the
710 /// extended form is used, including BODY= (RFC 1652) and SMTPUTF8
711 /// (RFC 6531) parameters if set by the caller.
712 ///
713 /// RFC 6152 Section 3: when the message contains 8-bit data
714 /// (`message_is_8bit` is true) and the server supports 8BITMIME,
715 /// `BODY=8BITMIME` is automatically included in MAIL FROM — even
716 /// when the caller did not supply explicit [`MailFromParams`].
717 pub(super) fn encode_mail_from_cmd(
718 capabilities: &ServerCapabilities,
719 buf: &mut BytesMut,
720 from: &ReversePath,
721 message_len: usize,
722 params: Option<&crate::types::MailFromParams>,
723 message_is_8bit: bool,
724 ) -> Result<(), Error> {
725 if let Some(p) = params {
726 // Merge caller params with auto-SIZE.
727 let mut merged = p.clone();
728 if capabilities.supports_size() && merged.size.is_none() {
729 merged.size = Some(message_len as u64);
730 }
731 // RFC 6531 Section 3.4: "If the SMTPUTF8 MAIL FROM parameter
732 // is used, the BODY parameter (RFC 6152) MUST also be used,
733 // with a value of '8BITMIME'." Auto-add BODY=8BITMIME when
734 // SMTPUTF8 is set and the caller hasn't set a BODY parameter.
735 //
736 // RFC 6152 Section 3: also auto-declare BODY=8BITMIME when
737 // the message contains 8-bit content and the server advertises
738 // 8BITMIME.
739 if merged.body.is_none()
740 && (merged.smtputf8 || (message_is_8bit && capabilities.supports_8bitmime()))
741 {
742 merged.body = Some(crate::types::BodyType::EightBitMime);
743 }
744 // RFC 5321 Section 4.5.2: 7-bit is the default encoding.
745 // Silently omit the redundant BODY=7BIT parameter when the
746 // server does not advertise 8BITMIME or BINARYMIME, since
747 // the BODY keyword is an ESMTP extension (RFC 6152 Section 3).
748 if merged.body == Some(crate::types::BodyType::SevenBit)
749 && !capabilities.supports_8bit_or_binary()
750 {
751 merged.body = None;
752 }
753 encode::encode_mail_from_full(buf, from, &merged)?;
754 } else if message_is_8bit && capabilities.supports_8bitmime() {
755 // RFC 6152 Section 3: "When a client SMTP transmits a message
756 // body consisting of 8bit data to a server SMTP, it must also
757 // transmit a BODY=8BITMIME parameter on the MAIL FROM command."
758 let auto_params = crate::types::MailFromParams {
759 size: if capabilities.supports_size() {
760 Some(message_len as u64)
761 } else {
762 None
763 },
764 body: Some(crate::types::BodyType::EightBitMime),
765 smtputf8: false,
766 ..Default::default()
767 };
768 encode::encode_mail_from_full(buf, from, &auto_params)?;
769 } else {
770 let size = if capabilities.supports_size() {
771 Some(message_len as u64)
772 } else {
773 None
774 };
775 encode::encode_mail_from(buf, from, size)?;
776 }
777 Ok(())
778 }
779
780 /// Check whether message data contains any 8-bit octets (bytes > 0x7F).
781 ///
782 /// Used to auto-detect 8-bit content for the BODY=8BITMIME declaration
783 /// per RFC 6152 Section 3.
784 pub(super) fn message_contains_8bit(data: &[u8]) -> bool {
785 data.iter().any(|&b| b > 0x7F)
786 }
787
788 /// Check whether the RFC 5322 header block contains UTF-8 octets that
789 /// require the SMTPUTF8 extension.
790 ///
791 /// RFC 6531 Section 3.1 item 4 requires the SMTPUTF8 MAIL FROM parameter
792 /// when the message being sent is internationalized. RFC 6532 Sections 3.2
793 /// and 3.6 permit UTF-8 directly in header fields, so any non-ASCII octet
794 /// before the blank-line header/body separator means the message needs
795 /// SMTPUTF8 support.
796 pub(super) fn message_headers_require_smtputf8(data: &[u8]) -> bool {
797 let header_end = Self::header_block_len(data);
798 data[..header_end].iter().any(|&b| b > 0x7F)
799 }
800
801 /// Determine the length of the leading RFC 5322 header block, if present.
802 ///
803 /// RFC 5322 Section 2.2: header fields are a sequence of field-name lines
804 /// optionally followed by folded continuation lines. Raw BDAT payloads and
805 /// malformed messages may omit headers entirely; in those cases this
806 /// returns 0 so body octets are not mistaken for header data.
807 pub(super) fn header_block_len(data: &[u8]) -> usize {
808 let mut offset = 0usize;
809 let mut saw_header = false;
810 let mut saw_complete_line = false;
811
812 while offset < data.len() {
813 let line_end = data[offset..]
814 .iter()
815 .position(|&byte| byte == b'\n')
816 .map_or(data.len(), |idx| offset + idx);
817 let line = if line_end > offset && data[line_end.saturating_sub(1)] == b'\r' {
818 &data[offset..line_end - 1]
819 } else {
820 &data[offset..line_end]
821 };
822
823 if line.is_empty() {
824 return if saw_header { offset } else { 0 };
825 }
826
827 let is_continuation = matches!(line.first(), Some(b' ' | b'\t'));
828 if is_continuation {
829 if !saw_header {
830 return 0;
831 }
832 } else if Self::looks_like_header_field_line(line) {
833 saw_header = true;
834 } else {
835 return if saw_header { offset } else { 0 };
836 }
837
838 if line_end == data.len() {
839 // RFC 5322 Section 2.2 models header fields as lines. If the
840 // entire payload is a single unterminated header-looking line,
841 // treat it conservatively as body data so raw payloads such as
842 // `Note: café` are not reclassified as SMTPUTF8 headers.
843 return if saw_header && saw_complete_line {
844 data.len()
845 } else {
846 0
847 };
848 }
849 saw_complete_line = true;
850 offset = line_end + 1;
851 }
852
853 if saw_header {
854 data.len()
855 } else {
856 0
857 }
858 }
859
860 /// Check whether a line begins with a syntactically valid RFC 5322 field name.
861 ///
862 /// RFC 5322 Section 3.6: `field = field-name ":" unstructured`.
863 /// RFC 5322 Section 3.6.8: `field-name = 1*ftext`.
864 pub(super) fn looks_like_header_field_line(line: &[u8]) -> bool {
865 let Some(colon_idx) = line.iter().position(|&byte| byte == b':') else {
866 return false;
867 };
868 colon_idx > 0
869 && line[..colon_idx]
870 .iter()
871 .all(|&byte| (33..=57).contains(&byte) || (59..=126).contains(&byte))
872 }
873
874 /// Check whether a send operation requires the SMTPUTF8 extension.
875 ///
876 /// RFC 6531 Section 3.1 item 4 requires SMTPUTF8 when the message being
877 /// sent is internationalized. RFC 6531 Section 3.3 extends SMTP mailbox
878 /// syntax to UTF-8 for MAIL FROM and RCPT TO only when SMTPUTF8 is active.
879 pub(super) fn send_requires_smtputf8(
880 from: &ReversePath,
881 recipients: &[ForwardPath],
882 message: &[u8],
883 ) -> bool {
884 from.requires_smtputf8()
885 || recipients.iter().any(ForwardPath::requires_smtputf8)
886 || Self::message_headers_require_smtputf8(message)
887 }
888
889 /// Merge caller-supplied MAIL FROM parameters with SMTPUTF8 inferred
890 /// from the envelope and raw message.
891 ///
892 /// RFC 6531 Section 3.1 item 4: SMTPUTF8 is required for
893 /// internationalized messages.
894 /// RFC 6531 Section 3.3: non-ASCII envelope addresses require SMTPUTF8.
895 /// RFC 6531 Section 3.4: the MAIL FROM parameter must be present when
896 /// SMTPUTF8 is required and supported.
897 pub(super) fn effective_mail_from_params(
898 capabilities: &ServerCapabilities,
899 from: &ReversePath,
900 recipients: &[ForwardPath],
901 message: &[u8],
902 params: Option<&crate::types::MailFromParams>,
903 ) -> Result<crate::types::MailFromParams, Error> {
904 let mut effective = params.cloned().unwrap_or_default();
905 if Self::send_requires_smtputf8(from, recipients, message) {
906 if !capabilities.supports_smtputf8() {
907 return Err(Error::SmtpUtf8Required);
908 }
909 effective.smtputf8 = true;
910 }
911 Ok(effective)
912 }
913
914 /// Check message size against the server's advertised SIZE limit.
915 ///
916 /// RFC 1870 Section 4: the client SHOULD NOT send a message larger
917 /// than the server's declared fixed maximum message size.
918 pub(super) fn check_size_limit(
919 capabilities: &ServerCapabilities,
920 message_len: usize,
921 ) -> Result<(), Error> {
922 if let Some(limit) = capabilities.size_limit() {
923 if message_len as u64 > limit {
924 return Err(Error::Protocol(format!(
925 "message size ({message_len} bytes) exceeds server SIZE limit \
926 ({limit} bytes) (RFC 1870 Section 4)"
927 )));
928 }
929 }
930 Ok(())
931 }
932
933 /// Validate that the server advertises support for each ESMTP
934 /// extension the caller is requesting via [`MailFromParams`].
935 ///
936 /// RFC 5321 Section 2.2.1: "An SMTP client MUST NOT use SMTP
937 /// service extensions that have not been offered by the server."
938 pub(super) fn validate_params(
939 capabilities: &ServerCapabilities,
940 params: Option<&crate::types::MailFromParams>,
941 is_tls: bool,
942 ) -> Result<(), Error> {
943 let Some(p) = params else { return Ok(()) };
944 Self::validate_extension_caps(capabilities, p, is_tls)?;
945 Self::validate_smtputf8_body(p)?;
946 Self::validate_body_type(capabilities, p)
947 }
948
949 /// RFC 5321 Section 2.2.1: "An SMTP client MUST NOT use SMTP service
950 /// extensions that have not been offered by the server."
951 #[allow(clippy::too_many_lines)]
952 pub(super) fn validate_extension_caps(
953 capabilities: &ServerCapabilities,
954 p: &crate::types::MailFromParams,
955 is_tls: bool,
956 ) -> Result<(), Error> {
957 if p.size.is_some() && !capabilities.supports_size() {
958 return Err(Error::Protocol(
959 "SIZE parameter requires server SIZE support \
960 (RFC 1870 Section 3 / RFC 5321 Section 2.2.1)"
961 .into(),
962 ));
963 }
964 if p.smtputf8 && !capabilities.supports_smtputf8() {
965 return Err(Error::Protocol(
966 "SMTPUTF8 parameter requires server SMTPUTF8 support \
967 (RFC 6531 Section 3.4)"
968 .into(),
969 ));
970 }
971 if p.smtputf8 && !capabilities.supports_8bitmime() {
972 return Err(Error::Protocol(
973 "SMTPUTF8 requires server 8BITMIME support; \
974 the server advertises SMTPUTF8 but not 8BITMIME \
975 (RFC 6531 Section 3.4 / Section 3.2)"
976 .into(),
977 ));
978 }
979 if p.requiretls && !capabilities.supports_requiretls() {
980 return Err(Error::Protocol(
981 "REQUIRETLS parameter requires server REQUIRETLS support \
982 (RFC 8689 Section 3 / RFC 5321 Section 2.2.1)"
983 .into(),
984 ));
985 }
986 // RFC 8689 Section 3: "the SMTP session used to send a given
987 // message MUST itself be TLS-protected."
988 if p.requiretls && !is_tls {
989 return Err(Error::Protocol(
990 "REQUIRETLS requires a TLS-protected session; \
991 the current connection is not encrypted \
992 (RFC 8689 Section 3)"
993 .into(),
994 ));
995 }
996 if (p.ret.is_some() || p.envid.is_some()) && !capabilities.supports_dsn() {
997 return Err(Error::Protocol(
998 "DSN parameters (RET/ENVID) require server DSN support \
999 (RFC 3461 Section 4 / RFC 5321 Section 2.2.1)"
1000 .into(),
1001 ));
1002 }
1003 if p.auth.is_some() && !capabilities.supports_auth_extension() {
1004 return Err(Error::Protocol(
1005 "AUTH parameter requires server AUTH support \
1006 (RFC 4954 Section 5 / RFC 5321 Section 2.2.1)"
1007 .into(),
1008 ));
1009 }
1010 // SmtpAuthParam::Mailbox contains a validated Mailbox newtype that
1011 // cannot be empty by construction (RFC 5321 Section 4.1.2).
1012 if (p.hold_for.is_some() || p.hold_until.is_some())
1013 && !capabilities.supports_future_release()
1014 {
1015 return Err(Error::Protocol(
1016 "HOLDFOR/HOLDUNTIL parameters require server FUTURERELEASE support \
1017 (RFC 4865 Section 5 / RFC 5321 Section 2.2.1)"
1018 .into(),
1019 ));
1020 }
1021 // RFC 4865 Section 5: HOLDFOR and HOLDUNTIL are mutually exclusive.
1022 // A MAIL FROM command MUST contain at most one of these parameters.
1023 if p.hold_for.is_some() && p.hold_until.is_some() {
1024 return Err(Error::Protocol(
1025 "HOLDFOR and HOLDUNTIL are mutually exclusive; \
1026 only one may be specified per MAIL FROM command \
1027 (RFC 4865 Section 5)"
1028 .into(),
1029 ));
1030 }
1031 // RFC 4865 Section 4: "the value MUST be less than or equal to the
1032 // [server-advertised] maximum hold interval."
1033 if let Some(hold_for) = p.hold_for {
1034 validate_hold_for_seconds(hold_for)?;
1035 if let Some(max_interval) = capabilities.future_release_max_interval() {
1036 if hold_for > max_interval {
1037 return Err(Error::Protocol(format!(
1038 "HOLDFOR value {hold_for}s exceeds server maximum \
1039 {max_interval}s (RFC 4865 Section 4)"
1040 )));
1041 }
1042 }
1043 }
1044 // RFC 4865 Section 4: "the client MUST NOT set a hold-until time
1045 // that is later than the [server-advertised] max-datetime."
1046 // Both values are RFC 3339 datetime strings. We parse them to
1047 // UTC epoch seconds for correct chronological comparison across
1048 // timezone offsets.
1049 if let Some(ref hold_until) = p.hold_until {
1050 validate_hold_until_datetime(hold_until)?;
1051 if let Some(max_datetime) = capabilities.future_release_max_datetime() {
1052 let hold_key = parse_rfc3339_to_utc_key(hold_until).ok_or_else(|| {
1053 Error::Protocol(format!(
1054 "HOLDUNTIL must be a valid RFC 3339 date-time: {hold_until} \
1055 (RFC 4865 Section 5 / RFC 3339 Section 5.6)"
1056 ))
1057 })?;
1058 let exceeds = match parse_rfc3339_to_utc_key(max_datetime) {
1059 Some(max_key) => hold_key > max_key,
1060 // Postel's law: if the server advertises a malformed
1061 // max-datetime, ignore that malformed bound rather than
1062 // reject a valid client parameter.
1063 None => false,
1064 };
1065 if exceeds {
1066 return Err(Error::Protocol(format!(
1067 "HOLDUNTIL value {hold_until} exceeds server maximum \
1068 {max_datetime} (RFC 4865 Section 4)"
1069 )));
1070 }
1071 }
1072 }
1073 if p.deliver_by.is_some() && !capabilities.supports_deliver_by() {
1074 return Err(Error::Protocol(
1075 "BY parameter requires server DELIVERBY support \
1076 (RFC 2852 Section 3 / RFC 5321 Section 2.2.1)"
1077 .into(),
1078 ));
1079 }
1080 // RFC 2852 Section 2: the EHLO DELIVERBY parameter is the minimum
1081 // deliver-by time the server will accept for by-mode Return.
1082 if let Some(ref db) = p.deliver_by {
1083 validate_deliver_by_value(db)?;
1084 if db.mode == crate::types::DeliverByMode::Return && db.seconds > 0 {
1085 if let Some(min_secs) = capabilities.deliver_by_min() {
1086 #[allow(clippy::cast_sign_loss)]
1087 let secs = db.seconds as u64;
1088 if secs < min_secs {
1089 return Err(Error::Protocol(format!(
1090 "DELIVERBY value {}s is below server minimum \
1091 {min_secs}s (RFC 2852 Section 2)",
1092 db.seconds
1093 )));
1094 }
1095 }
1096 }
1097 }
1098 Self::validate_future_release_deliver_by_relation(p)?;
1099 if p.mt_priority.is_some() && !capabilities.supports_mt_priority() {
1100 return Err(Error::Protocol(
1101 "MT-PRIORITY parameter requires server MT-PRIORITY support \
1102 (RFC 6758 Section 4 / RFC 5321 Section 2.2.1)"
1103 .into(),
1104 ));
1105 }
1106 // RFC 6758 Section 4: priority values range from -9 to 9 (the full
1107 // STANAG 4406 range). Values outside this range are invalid.
1108 if let Some(v) = p.mt_priority {
1109 if !(-9..=9).contains(&v) {
1110 return Err(Error::Protocol(format!(
1111 "MT-PRIORITY value {v} is outside the valid range -9..=9 \
1112 (RFC 6758 Section 4)"
1113 )));
1114 }
1115 }
1116 Ok(())
1117 }
1118
1119 /// RFC 4865 Section 5.2.1: the future release time MAY NOT be farther
1120 /// in the future than the DELIVERBY deadline.
1121 pub(super) fn validate_future_release_deliver_by_relation(
1122 p: &crate::types::MailFromParams,
1123 ) -> Result<(), Error> {
1124 let Some(deliver_by) = p.deliver_by else {
1125 return Ok(());
1126 };
1127
1128 if let Some(hold_for) = p.hold_for {
1129 match u64::try_from(deliver_by.seconds) {
1130 Ok(by_seconds) if hold_for <= by_seconds => {}
1131 _ => {
1132 return Err(Error::Protocol(format!(
1133 "HOLDFOR value {hold_for}s exceeds DELIVERBY deadline {}s \
1134 (RFC 4865 Section 5.2.1)",
1135 deliver_by.seconds
1136 )));
1137 }
1138 }
1139 }
1140
1141 if let Some(ref hold_until) = p.hold_until {
1142 let hold_key = parse_rfc3339_to_utc_key(hold_until).ok_or_else(|| {
1143 Error::Protocol(format!(
1144 "HOLDUNTIL must be a valid RFC 3339 date-time: {hold_until} \
1145 (RFC 4865 Section 5 / RFC 3339 Section 5.6)"
1146 ))
1147 })?;
1148 let now = SystemTime::now().duration_since(UNIX_EPOCH).map_err(|_| {
1149 Error::Protocol(
1150 "system clock is before the Unix epoch; cannot validate HOLDUNTIL against DELIVERBY \
1151 (RFC 4865 Section 5.2.1)"
1152 .into(),
1153 )
1154 })?;
1155 let now_secs = i64::try_from(now.as_secs()).map_err(|_| {
1156 Error::Protocol(
1157 "system clock exceeds supported range for DELIVERBY comparison \
1158 (RFC 4865 Section 5.2.1)"
1159 .into(),
1160 )
1161 })?;
1162 let deadline_secs = now_secs.checked_add(deliver_by.seconds).ok_or_else(|| {
1163 Error::Protocol(
1164 "DELIVERBY deadline overflow during FUTURERELEASE validation \
1165 (RFC 4865 Section 5.2.1)"
1166 .into(),
1167 )
1168 })?;
1169 let deadline_key = (deadline_secs, now.subsec_nanos());
1170 if hold_key > deadline_key {
1171 return Err(Error::Protocol(format!(
1172 "HOLDUNTIL value {hold_until} exceeds DELIVERBY deadline {}s \
1173 (RFC 4865 Section 5.2.1)",
1174 deliver_by.seconds
1175 )));
1176 }
1177 }
1178
1179 Ok(())
1180 }
1181
1182 /// RFC 6531 Section 3.6: SMTPUTF8 requires 8-bit-capable BODY type.
1183 ///
1184 /// BODY=7BIT is incompatible with SMTPUTF8 because internationalized
1185 /// headers contain non-ASCII octets. Both BODY=8BITMIME (RFC 6152)
1186 /// and BODY=BINARYMIME (RFC 3030) are valid:
1187 ///
1188 /// > The SMTPUTF8 extension MAY be used as follows:
1189 /// > - with the BODY=8BITMIME parameter [RFC6152], or
1190 /// > - with the BODY=BINARYMIME parameter, if the SMTP server
1191 /// > advertises BINARYMIME [RFC3030].
1192 pub(super) fn validate_smtputf8_body(p: &crate::types::MailFromParams) -> Result<(), Error> {
1193 if p.smtputf8 && p.body == Some(crate::types::BodyType::SevenBit) {
1194 return Err(Error::Protocol(
1195 "BODY=7BIT cannot be used with SMTPUTF8; \
1196 RFC 6531 Section 3.6 requires BODY=8BITMIME or \
1197 BODY=BINARYMIME when SMTPUTF8 is used"
1198 .into(),
1199 ));
1200 }
1201 Ok(())
1202 }
1203
1204 /// RFC 6152 Section 3 / RFC 3030 Section 2: BODY type capability checks.
1205 ///
1206 /// BODY=7BIT is always accepted regardless of server capabilities
1207 /// because 7-bit is the default encoding (RFC 5321 Section 4.5.2).
1208 /// When the server does not advertise 8BITMIME, the encoder silently
1209 /// omits the redundant `BODY=7BIT` parameter.
1210 pub(super) fn validate_body_type(
1211 capabilities: &ServerCapabilities,
1212 p: &crate::types::MailFromParams,
1213 ) -> Result<(), Error> {
1214 // Explicit SevenBit arm kept for RFC-citation documentation;
1215 // its body matches the wildcard intentionally.
1216 #[allow(clippy::match_same_arms)]
1217 match p.body {
1218 // RFC 5321 Section 4.5.2: 7-bit is the default transfer
1219 // encoding — BODY=7BIT is a no-op declaration that does not
1220 // require 8BITMIME. The encoder silently omits it when the
1221 // server lacks 8BITMIME support.
1222 Some(crate::types::BodyType::SevenBit) => {}
1223 Some(crate::types::BodyType::EightBitMime) if !capabilities.supports_8bitmime() => {
1224 return Err(Error::Protocol(
1225 "BODY=8BITMIME requires server 8BITMIME support \
1226 (RFC 6152 Section 3)"
1227 .into(),
1228 ));
1229 }
1230 Some(crate::types::BodyType::BinaryMime) if !capabilities.supports_binarymime() => {
1231 return Err(Error::Protocol(
1232 "BODY=BINARYMIME requires server BINARYMIME support \
1233 (RFC 3030 Section 2)"
1234 .into(),
1235 ));
1236 }
1237 Some(crate::types::BodyType::BinaryMime) if !capabilities.supports_chunking() => {
1238 return Err(Error::Protocol(
1239 "BODY=BINARYMIME requires server CHUNKING support \
1240 (RFC 3030 Section 2)"
1241 .into(),
1242 ));
1243 }
1244 _ => {}
1245 }
1246 Ok(())
1247 }
1248
1249 /// Validates that DSN RCPT TO parameters (NOTIFY, ORCPT) are only
1250 /// used when the server advertises DSN support.
1251 ///
1252 /// RFC 5321 Section 2.2.1: "An SMTP client MUST NOT use SMTP service
1253 /// extensions that have not been offered by the server."
1254 /// RFC 3461 Sections 4.1–4.2: NOTIFY and ORCPT are DSN extensions.
1255 pub(super) fn validate_rcpt_params(
1256 capabilities: &ServerCapabilities,
1257 rcpt_params: &[crate::types::RcptToParams],
1258 ) -> Result<(), Error> {
1259 let has_dsn_params = rcpt_params.iter().any(|p| !p.is_empty());
1260 if has_dsn_params && !capabilities.supports_dsn() {
1261 return Err(Error::Protocol(
1262 "DSN RCPT TO parameters (NOTIFY/ORCPT) require server DSN support \
1263 (RFC 3461 Sections 4.1–4.2 / RFC 5321 Section 2.2.1)"
1264 .into(),
1265 ));
1266 }
1267 Ok(())
1268 }
1269
1270 /// Send EHLO or LHLO on a bare [`SmtpInner`], and parse capabilities.
1271 ///
1272 /// Used during both initial connection setup (before the mutex is
1273 /// constructed) and at runtime (with the guard dereferenced).
1274 ///
1275 /// SMTP uses EHLO (RFC 5321 Section 4.1.1.1). If EHLO is rejected,
1276 /// falls back to HELO (RFC 5321 Section 4.1.4).
1277 /// LMTP uses LHLO (RFC 2033 Section 4.1) with no HELO fallback.
1278 pub(super) async fn ehlo_on_inner(
1279 inner: &mut SmtpInner,
1280 protocol: Protocol,
1281 ) -> Result<(), Error> {
1282 // RFC 5321 Section 4.1.4: if we previously fell back to HELO
1283 // (server doesn't support ESMTP), continue using HELO for the
1284 // remainder of the session. LMTP always uses LHLO.
1285 if inner.helo_mode && protocol == Protocol::Smtp {
1286 // RFC 5321 Section 4.1.1.1 publishes `helo = "HELO" SP Domain`
1287 // but also says that clients without a meaningful domain name
1288 // SHOULD send an address-literal. The domain is pre-validated
1289 // by the DomainOrLiteral type.
1290 let mut buf = BytesMut::new();
1291 encode::encode_helo(&mut buf, &inner.ehlo_domain)?;
1292 Self::validate_command_line_length(buf.len(), "HELO")?;
1293 inner.write_all(&buf).await?;
1294 let resp = inner.read_response().await?;
1295 if resp.code != 250 {
1296 if resp.is_success() {
1297 return Err(Error::Protocol(format!(
1298 "HELO response must be 250, got {} \
1299 (RFC 5321 Section 4.1.1.1)",
1300 resp.code
1301 )));
1302 }
1303 return Err(Self::response_to_error(resp));
1304 }
1305 let greeting_name = resp
1306 .lines
1307 .first()
1308 .map(|l| match l.find(' ') {
1309 Some(pos) => l[..pos].to_owned(),
1310 None => l.clone(),
1311 })
1312 .unwrap_or_default();
1313 inner.capabilities = ServerCapabilities {
1314 greeting_name,
1315 ..Default::default()
1316 };
1317 return Ok(());
1318 }
1319
1320 // The ehlo_domain is pre-validated by the DomainOrLiteral type.
1321 // RFC 5321 Section 4.1.1.1: the EHLO argument is a Domain or
1322 // address-literal. RFC 6531 (SMTPUTF8) does NOT extend this grammar.
1323 let mut buf = BytesMut::new();
1324
1325 match protocol {
1326 Protocol::Smtp => encode::encode_ehlo(&mut buf, &inner.ehlo_domain)?,
1327 Protocol::Lmtp => encode::encode_lhlo(&mut buf, &inner.ehlo_domain)?,
1328 }
1329 // RFC 5321 Section 4.5.3.1.4: validate command line length.
1330 let cmd_name = match protocol {
1331 Protocol::Smtp => "EHLO",
1332 Protocol::Lmtp => "LHLO",
1333 };
1334 Self::validate_command_line_length(buf.len(), cmd_name)?;
1335 inner.write_all(&buf).await?;
1336 let resp = inner.read_response().await?;
1337 if resp.code == 250 {
1338 inner.capabilities = Self::parse_capabilities(&resp);
1339 return Ok(());
1340 }
1341
1342 // RFC 5321 Section 4.1.1.1: EHLO success response MUST use
1343 // reply code 250. A non-250 2xx response does not conform to the
1344 // ehlo-ok-rsp grammar and must not be used to parse capabilities.
1345 if resp.is_success() {
1346 return Err(Error::Protocol(format!(
1347 "EHLO response must be 250, got {} \
1348 (RFC 5321 Section 4.1.1.1)",
1349 resp.code
1350 )));
1351 }
1352
1353 // RFC 5321 Section 4.1.4: EHLO rejection codes that warrant HELO fallback.
1354 // 500 = command not recognized (server doesn't speak ESMTP)
1355 // 501 = syntax error in parameters (server may not understand EHLO format)
1356 // 502 = command not implemented (server explicitly lacks ESMTP)
1357 // 504 = command parameter not implemented
1358 //
1359 // 550 is deliberately excluded — it indicates a policy rejection
1360 // of the client identity, not lack of ESMTP support. HELO fallback
1361 // would be futile and potentially mask the real issue.
1362 //
1363 // Other 5xx codes (e.g., 521 "does not accept mail" per RFC 7504,
1364 // or 554 "transaction failed") indicate problems unrelated to ESMTP
1365 // support where HELO won't help.
1366 //
1367 // Transient (4xx) errors like 421 ("service not available, closing
1368 // transmission channel") indicate a server-level problem, NOT that
1369 // the server doesn't support ESMTP. HELO fallback would be futile
1370 // in that case.
1371 //
1372 // LMTP has no HELO fallback (RFC 2033 Section 4.3: HELO is not
1373 // valid in LMTP).
1374 let helo_fallback_codes = [500, 501, 502, 504];
1375 if protocol == Protocol::Lmtp || !helo_fallback_codes.contains(&resp.code) {
1376 return Err(Self::response_to_error(resp));
1377 }
1378
1379 tracing::debug!(
1380 code = resp.code,
1381 "EHLO rejected with 5xx, falling back to HELO (RFC 5321 Section 4.1.4)"
1382 );
1383
1384 buf.clear();
1385 encode::encode_helo(&mut buf, &inner.ehlo_domain)?;
1386 // RFC 5321 Section 4.5.3.1.4: validate command line length.
1387 Self::validate_command_line_length(buf.len(), "HELO")?;
1388 inner.write_all(&buf).await?;
1389 let resp = inner.read_response().await?;
1390 // RFC 5321 Section 4.1.1.1: helo-ok-rsp = "250" SP Domain
1391 // [ SP helo-greet ] CRLF — only code 250 is valid for HELO success.
1392 if resp.code != 250 {
1393 if resp.is_success() {
1394 // Non-250 2xx — protocol violation, same treatment as EHLO.
1395 return Err(Error::Protocol(format!(
1396 "HELO response must be 250, got {} \
1397 (RFC 5321 Section 4.1.1.1)",
1398 resp.code
1399 )));
1400 }
1401 return Err(Self::response_to_error(resp));
1402 }
1403 // HELO does not advertise extensions — use empty capabilities,
1404 // but preserve the server's greeting domain from the HELO response
1405 // (RFC 5321 Section 4.1.1.1: helo-ok-rsp = "250" SP Domain [ SP helo-greet ] CRLF).
1406 let greeting_name = resp
1407 .lines
1408 .first()
1409 .map(|l| match l.find(' ') {
1410 Some(pos) => l[..pos].to_owned(),
1411 None => l.clone(),
1412 })
1413 .unwrap_or_default();
1414 inner.capabilities = ServerCapabilities {
1415 greeting_name,
1416 ..Default::default()
1417 };
1418 // RFC 5321 Section 4.1.4: remember that this server doesn't
1419 // support ESMTP so subsequent rehlo calls use HELO too.
1420 inner.helo_mode = true;
1421 Ok(())
1422 }
1423
1424 /// Parse EHLO/LHLO response lines into `ServerCapabilities`.
1425 ///
1426 /// Delegates to [`decode::parse_ehlo_capabilities`] which handles
1427 /// case-insensitive keyword matching (RFC 5321 Section 2.4) and
1428 /// preserves original mechanism names in `AuthMechanism::Other`
1429 /// (RFC 4954 Section 3).
1430 pub(super) fn parse_capabilities(response: &SmtpResponse) -> ServerCapabilities {
1431 decode::parse_ehlo_capabilities(response)
1432 }
1433
1434 /// RFC 5321 Section 4.5.3.1.4: maximum total length of a command
1435 /// line including the command word and the `<CRLF>` is 512 octets.
1436 pub(super) const SMTP_MAX_COMMAND_LINE: usize = 512;
1437
1438 /// Maximum total length of a MAIL FROM command line including
1439 /// ESMTP extension parameters (RFC 5321 Section 4.5.3.1.4).
1440 ///
1441 /// ESMTP extensions increase the base 512-octet limit:
1442 /// - RFC 1870 Section 4: +26 for the SIZE keyword and value
1443 /// - RFC 6152 Section 7: +17 for the BODY= parameter
1444 /// - RFC 6531 Section 3.4: +10 for the SMTPUTF8 keyword
1445 /// - RFC 8689 Section 3: +11 for the REQUIRETLS keyword
1446 /// - RFC 3461 Section 4.3/4.4: +109 for RET= and ENVID= params
1447 /// - RFC 4865 Section 5: +34 for HOLDFOR/HOLDUNTIL
1448 /// - RFC 2852 Section 4: +17 for BY= parameter
1449 /// - RFC 6758 Section 4: +16 for MT-PRIORITY= parameter
1450 /// - RFC 4954 Section 3: +500 for AUTH= submitter identity
1451 pub(super) const SMTP_MAX_MAIL_FROM_LINE: usize =
1452 512 + 26 + 17 + 10 + 11 + 109 + 34 + 17 + 16 + 500;
1453
1454 /// RFC 3461 Section 5: maximum total length of a RCPT TO command
1455 /// line when DSN parameters (NOTIFY, ORCPT) are present.
1456 ///
1457 /// RFC 3461 Section 5 increases the RCPT TO line limit by 500 octets
1458 /// beyond the base 512-octet limit (RFC 5321 Section 4.5.3.1.4),
1459 /// yielding a maximum of 1012 octets.
1460 pub(super) const SMTP_MAX_RCPT_TO_DSN_LINE: usize = 1012;
1461
1462 /// RFC 5321 Section 4.5.3.1.3: maximum total length of a
1463 /// reverse-path or forward-path including the punctuation
1464 /// and element separators (i.e., the `<` and `>` delimiters).
1465 ///
1466 /// Path-length validation is now performed by [`ReversePath::new`]
1467 /// and [`ForwardPath::new`] at construction time. This constant is
1468 /// retained for use in test assertions.
1469 #[cfg(test)]
1470 pub(super) const SMTP_MAX_PATH_LENGTH: usize = 256;
1471
1472 /// Maximum reply line length the client will accept, including the
1473 /// 3-digit reply code and the terminating CRLF.
1474 ///
1475 /// RFC 5321 Section 4.5.3.1.5 specifies a 512-octet limit for
1476 /// compliant servers. However, per Postel's law ("be liberal in
1477 /// what you accept"), we use a permissive 8192-octet limit to
1478 /// tolerate non-conformant servers (e.g., Exchange, Postfix) that
1479 /// sometimes exceed the RFC limit in practice.
1480 pub(super) const SMTP_MAX_REPLY_LINE: usize = 8192;
1481
1482 /// RFC 4954 Section 12: maximum length of an `auth-response` line
1483 /// (the base64-encoded SASL data sent during a two-step AUTH
1484 /// exchange), excluding the terminating CRLF.
1485 pub(super) const SMTP_MAX_AUTH_RESPONSE: usize = 12288;
1486
1487 /// Maximum response buffer size (64 KiB).
1488 ///
1489 /// RFC 5321 Section 4.5.3.1.5 specifies reply lines should be at
1490 /// most 512 octets. We allow a generous 64 KiB to accommodate
1491 /// multi-line EHLO responses with many extensions, while still
1492 /// protecting against a malicious server exhausting memory.
1493 pub(super) const MAX_RESPONSE_BUFFER: usize = 64 * 1024;
1494
1495 /// Try to parse a complete SMTP response from the buffer.
1496 ///
1497 /// Returns `None` if the buffer does not yet contain a complete response.
1498 /// On success, returns the parsed [`SmtpResponse`] together with the
1499 /// number of bytes consumed from `buf`, so the caller can advance past
1500 /// the response in a single step — eliminating the desynchronization
1501 /// risk of computing the byte length separately.
1502 ///
1503 /// Parses multi-line responses per RFC 5321 Section 4.2.
1504 pub(super) fn try_parse_response(buf: &[u8]) -> Result<Option<(SmtpResponse, usize)>, Error> {
1505 let mut lines = Vec::new();
1506 let mut code: Option<u16> = None;
1507 let mut first_enhanced: Option<EnhancedStatusCode> = None;
1508 let mut pos = 0;
1509
1510 loop {
1511 // Find the next line ending: CRLF (RFC 5321 Section 2.3.8) or
1512 // bare LF (Postel's law — tolerate non-conformant servers that
1513 // omit CR, e.g., misconfigured Postfix, custom implementations).
1514 let (line_end_offset, terminator_len) = {
1515 let slice = &buf[pos..];
1516 // Find the earliest line terminator: CRLF (RFC 5321 Section 2.3.8)
1517 // or bare LF (Postel's law — tolerate non-conformant servers that
1518 // omit CR, e.g., misconfigured Postfix, custom implementations).
1519 // We must find the EARLIEST of the two to avoid merging two logical
1520 // response lines when bare LF precedes CRLF in the buffer.
1521 match slice.iter().position(|&b| b == b'\n') {
1522 Some(lf_pos) if lf_pos > 0 && slice[lf_pos - 1] == b'\r' => {
1523 // CRLF: the line content ends before the CR.
1524 (lf_pos - 1, 2)
1525 }
1526 Some(lf_pos) => {
1527 // Bare LF — tolerate per Postel's law (RFC 1122 Section 1.2.2).
1528 (lf_pos, 1)
1529 }
1530 None => return Ok(None), // Incomplete — need more data.
1531 }
1532 };
1533
1534 let line = &buf[pos..pos + line_end_offset];
1535 let line_total_len = line_end_offset + terminator_len; // line content + terminator
1536
1537 // RFC 5321 Section 4.5.3.1.5: reply line should not exceed
1538 // 512 octets. We enforce a permissive limit of 8192 octets
1539 // per Postel's law to tolerate non-conformant servers.
1540 if line_total_len > Self::SMTP_MAX_REPLY_LINE {
1541 return Err(Error::Parse(format!(
1542 "reply line exceeds {}-octet limit ({} octets) \
1543 (RFC 5321 Section 4.5.3.1.5, relaxed per Postel's law)",
1544 Self::SMTP_MAX_REPLY_LINE,
1545 line_total_len
1546 )));
1547 }
1548
1549 if line.len() < 3 {
1550 return Err(Error::Parse("response line too short".into()));
1551 }
1552
1553 // Parse 3-digit reply code.
1554 let code_str = std::str::from_utf8(&line[..3])
1555 .map_err(|_| Error::Parse("invalid reply code bytes".into()))?;
1556 let line_code: u16 = code_str
1557 .parse()
1558 .map_err(|_| Error::Parse(format!("invalid reply code: {code_str}")))?;
1559
1560 // RFC 5321 Section 4.2: reply-code = %x32-35 %x30-35 %x30-39
1561 // Strict ABNF limits the second digit to 0-5, but per Postel's
1562 // law we accept 0-9 to tolerate non-conformant servers. The
1563 // first digit is still restricted to 2-5 (valid reply classes).
1564 let d0 = line[0];
1565 if !(b'2'..=b'5').contains(&d0) {
1566 return Err(Error::Parse(format!(
1567 "reply code {line_code} outside valid range \
1568 (RFC 5321 Section 4.2: first digit must be 2-5)"
1569 )));
1570 }
1571
1572 // RFC 5321 Section 4.2: In a multi-line reply, every line MUST
1573 // use the same reply code. A mismatch is a server protocol
1574 // violation, not a client-side parse failure.
1575 if let Some(c) = code {
1576 if c != line_code {
1577 return Err(Error::Protocol(format!(
1578 "reply code changed mid-response: {c} vs {line_code} \
1579 (RFC 5321 Section 4.2)"
1580 )));
1581 }
1582 }
1583 code = Some(line_code);
1584
1585 // RFC 5321 Section 4.2: Reply-line separator must be '-'
1586 // (continuation), SP (final line with text), or absent
1587 // (code-only final line). Any other byte is invalid.
1588 let is_final = if line.len() == 3 {
1589 true // Just the code, no separator.
1590 } else {
1591 match line[3] {
1592 b'-' => false,
1593 b' ' => true,
1594 other => {
1595 return Err(Error::Parse(format!(
1596 "invalid separator byte 0x{other:02X} at position 3 \
1597 (RFC 5321 Section 4.2: expected '-' or SP)"
1598 )));
1599 }
1600 }
1601 };
1602
1603 // Extract text after the separator.
1604 let raw_text = if line.len() > 4 {
1605 String::from_utf8_lossy(&line[4..]).into_owned()
1606 } else {
1607 String::new()
1608 };
1609
1610 // Try to strip an enhanced status code from the start of the text
1611 // (RFC 2034 Section 3). Preserve only the first one found.
1612 // RFC 2034 Section 4: the enhanced code class MUST match the
1613 // reply code class — strip_enhanced_code validates this internally.
1614 let text = if let Some((esc, rest)) = decode::strip_enhanced_code(&raw_text, line_code)
1615 {
1616 if first_enhanced.is_none() {
1617 first_enhanced = Some(esc);
1618 }
1619 rest.to_owned()
1620 } else {
1621 raw_text
1622 };
1623 lines.push(text);
1624
1625 pos += line_end_offset + terminator_len; // Skip past line terminator.
1626
1627 if is_final {
1628 let reply_code = code.ok_or_else(|| Error::Parse("empty response".into()))?;
1629 return Ok(Some((
1630 SmtpResponse {
1631 code: reply_code,
1632 enhanced_code: first_enhanced,
1633 lines,
1634 },
1635 pos,
1636 )));
1637 }
1638 }
1639 }
1640
1641 /// Convert a non-success `SmtpResponse` into the appropriate `Error`.
1642 pub(super) fn response_to_error(resp: SmtpResponse) -> Error {
1643 let message = resp.text();
1644 let code = resp.code;
1645 if resp.is_permanent_error() {
1646 Error::Permanent {
1647 code,
1648 message,
1649 response: resp,
1650 }
1651 } else if resp.is_transient_error() {
1652 Error::Transient {
1653 code,
1654 message,
1655 response: resp,
1656 }
1657 } else {
1658 Error::Protocol(format!("unexpected response code {code}: {message}"))
1659 }
1660 }
1661
1662 /// Create a default TLS configuration using the system root certificates.
1663 ///
1664 /// Installs the ring `CryptoProvider` if no provider has been set yet.
1665 pub(super) fn default_tls_config() -> Arc<rustls::ClientConfig> {
1666 // Install ring as the crypto provider (no-op if already installed).
1667 let _ = rustls::crypto::ring::default_provider().install_default();
1668
1669 let root_store: rustls::RootCertStore =
1670 webpki_roots::TLS_SERVER_ROOTS.iter().cloned().collect();
1671 let config = rustls::ClientConfig::builder()
1672 .with_root_certificates(root_store)
1673 .with_no_client_auth();
1674 Arc::new(config)
1675 }
1676
1677 /// Set the domain sent in EHLO/LHLO (RFC 5321 Section 4.1.1.1).
1678 ///
1679 /// The argument should be the client's FQDN or, when no FQDN is
1680 /// available, an address literal (RFC 5321 Section 4.1.4).
1681 ///
1682 /// Returns `Error::Protocol` if `domain` is empty or contains
1683 /// non-printable-ASCII bytes (RFC 5321 Section 4.1.1.1).
1684 ///
1685 /// This method acquires the internal mutex.
1686 pub async fn set_ehlo_domain(&self, domain: &str) -> Result<(), Error> {
1687 let domain = DomainOrLiteral::new(domain.to_owned())?;
1688 self.inner.lock().await.ehlo_domain = domain;
1689 Ok(())
1690 }
1691}