Skip to main content

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, &params[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}