daaki-smtp 0.2.0

An async SMTP client library
Documentation
//! Session management: RSET, NOOP, VRFY, EXPN, HELP, QUIT, REHLO.
//!
//! RFC 5321 Sections 4.1.1.5–4.1.1.10 (session commands).

#[allow(clippy::wildcard_imports)]
use super::*;

impl SmtpConnection {
    // -----------------------------------------------------------------------
    // Session management
    // -----------------------------------------------------------------------

    /// Reset the session — RSET command (RFC 5321 Section 4.1.1.5).
    ///
    /// Aborts the current mail transaction, if any.
    pub async fn reset(&self, timeout: Duration) -> Result<(), Error> {
        self.send_simple_command(encode::encode_rset, "RSET", 250, "4.1.1.5", timeout)
            .await
    }

    /// Send a NOOP command (RFC 5321 Section 4.1.1.9).
    ///
    /// The NOOP command does not affect any state; the server replies
    /// with `250 OK`. Useful as a keep-alive probe to prevent idle
    /// timeouts on long-lived connections.
    pub async fn noop(&self, timeout: Duration) -> Result<(), Error> {
        self.send_simple_command(encode::encode_noop, "NOOP", 250, "4.1.1.9", timeout)
            .await
    }

    /// Verify a user or mailbox — VRFY command (RFC 5321 Section 4.1.1.6).
    ///
    /// Returns the server's response, which may be:
    /// - 250/251: the user is verified (the response text contains the
    ///   mailbox name)
    /// - 252: cannot verify the user, but will accept messages and
    ///   attempt delivery
    /// - 502: VRFY not implemented (RFC 5321 Section 3.5.3 allows servers
    ///   to disable this command)
    /// - 550: user not found
    ///
    /// Valid for both SMTP and LMTP connections. RFC 2033 Section 4.3
    /// only prohibits HELO, EHLO, and TURN in LMTP; VRFY is "not
    /// required, but SHOULD be used if possible." If `address` contains
    /// non-ASCII characters and the server advertised `SMTPUTF8`, this
    /// method sends `VRFY <arg> SMTPUTF8` per RFC 6531 Section 3.7.4.2.
    pub async fn vrfy(&self, address: &str, timeout: Duration) -> Result<SmtpResponse, Error> {
        self.send_simple_query(
            address,
            "VRFY",
            "4.1.1.6",
            encode::encode_vrfy,
            encode::encode_vrfy_smtputf8,
            timeout,
        )
        .await
    }

    /// Expand a mailing list — EXPN command (RFC 5321 Section 4.1.1.7).
    ///
    /// Returns the server's response, which may be:
    /// - 250: the list is expanded (multi-line response with members)
    /// - 502: EXPN not implemented (RFC 5321 Section 3.5.3 allows servers
    ///   to disable this command)
    /// - 550: list not found
    ///
    /// Valid for both SMTP and LMTP connections. RFC 2033 Section 4.3
    /// only prohibits HELO, EHLO, and TURN in LMTP; EXPN is "not
    /// required, but SHOULD be used if possible." If `list_name` contains
    /// non-ASCII characters and the server advertised `SMTPUTF8`, this
    /// method sends `EXPN <arg> SMTPUTF8` per RFC 6531 Section 3.7.4.2.
    pub async fn expn(&self, list_name: &str, timeout: Duration) -> Result<SmtpResponse, Error> {
        self.send_simple_query(
            list_name,
            "EXPN",
            "4.1.1.7",
            encode::encode_expn,
            encode::encode_expn_smtputf8,
            timeout,
        )
        .await
    }

    /// Request human-readable help text from the server (RFC 5321 Section 4.1.1.8).
    ///
    /// Returns the server's response, which is typically:
    /// - 211: system status or topic-specific help
    /// - 214: general help text
    /// - 502: HELP not implemented
    ///
    /// The optional `topic` argument is encoded as SMTP `String`
    /// (`HELP SP String CRLF`) when present. If omitted, the command is sent
    /// as bare `HELP`.
    pub async fn help(
        &self,
        topic: Option<&str>,
        timeout: Duration,
    ) -> Result<SmtpResponse, Error> {
        if let Some(topic) = topic {
            if topic.is_empty() {
                return Err(Error::Protocol(
                    "HELP argument must not be empty when provided \
                     (RFC 5321 Section 4.1.1.8: help = \"HELP\" [ SP String ] CRLF)"
                        .into(),
                ));
            }
            Self::validate_no_crlf(topic, "HELP argument")?;
            Self::validate_ascii_string(topic, "HELP argument")?;
        }

        tokio::time::timeout(timeout, async {
            let mut inner = self.inner.lock().await;
            Self::ensure_not_shutting_down(&inner)?;

            let mut buf = BytesMut::new();
            if let Some(topic) = topic {
                encode::encode_help_with_arg(&mut buf, topic)?;
            } else {
                encode::encode_help(&mut buf);
            }

            inner.write_all(&buf).await?;
            inner.read_response().await
        })
        .await
        .map_err(|_| Error::Timeout)?
    }

    /// Send a simple command that expects a success response and returns no data.
    ///
    /// Used by commands whose success reply is a specific code rather than any
    /// generic 2xx completion. RFC 5321 Section 4.2.1 defines the reply-code
    /// classes, but these commands specify one exact success code each.
    async fn send_simple_command(
        &self,
        encoder: fn(&mut BytesMut),
        cmd_name: &str,
        expected_code: u16,
        rfc_section: &str,
        timeout: Duration,
    ) -> Result<(), Error> {
        let mut inner = self.inner.lock().await;
        Self::ensure_not_shutting_down(&inner)?;
        tokio::time::timeout(timeout, async {
            let mut buf = BytesMut::new();
            encoder(&mut buf);
            inner.write_all(&buf).await?;
            let resp = inner.read_response().await?;
            if resp.code == expected_code {
                Ok(())
            } else if resp.is_success() {
                Err(Error::Protocol(format!(
                    "{cmd_name} response must be {expected_code}, got {} \
                     (RFC 5321 Section {rfc_section})",
                    resp.code
                )))
            } else {
                Err(Self::response_to_error(resp))
            }
        })
        .await
        .map_err(|_| Error::Timeout)?
    }

    /// Shared implementation for VRFY and EXPN (RFC 5321 Sections 4.1.1.6–4.1.1.7).
    ///
    /// Both commands follow the same pattern: validate the argument (non-empty,
    /// no CRLF, ASCII-only unless RFC 6531 `SMTPUTF8` is negotiated), encode
    /// the command, check line length, send, and read the response.
    async fn send_simple_query(
        &self,
        argument: &str,
        cmd_name: &str,
        rfc_section: &str,
        encoder: fn(&mut BytesMut, &str) -> Result<(), Error>,
        smtp_utf8_encoder: fn(&mut BytesMut, &str) -> Result<(), Error>,
        timeout: Duration,
    ) -> Result<SmtpResponse, Error> {
        // RFC 5321 Section <rfc_section>: "<CMD>" SP String CRLF — the String
        // production requires at least one character.
        if argument.is_empty() {
            return Err(Error::Protocol(format!(
                "{cmd_name} argument must not be empty \
                 (RFC 5321 Section {rfc_section}: \
                 String = 1*(%d1-9 / %d11 / %d12 / %d14-127))"
            )));
        }
        let param_name = format!("{cmd_name} argument");
        // RFC 5321 Section 4.1.2: reject arguments containing CR/LF
        // to prevent SMTP command injection.
        Self::validate_no_crlf(argument, &param_name)?;
        let use_smtputf8 = !argument.is_ascii();
        if use_smtputf8 {
            let supports_smtputf8 = {
                let inner = self.inner.lock().await;
                inner.capabilities.supports_smtputf8()
            };
            if !supports_smtputf8 {
                return Err(Error::Protocol(format!(
                    "{cmd_name} argument contains non-ASCII characters, but the server did not advertise SMTPUTF8 \
                     (RFC 6531 Section 3.7.4.2)"
                )));
            }
            Self::validate_utf8_string(argument, &param_name)?;
        } else {
            // RFC 5321 Sections 4.1.1.6-4.1.1.7 / Section 4.1.2: VRFY and EXPN
            // take a `String` argument, not an SMTP `Mailbox`, so validate only
            // the printable-ASCII / no-control-character constraints here.
            Self::validate_ascii_string(argument, &param_name)?;
        }
        tokio::time::timeout(timeout, async {
            let mut inner = self.inner.lock().await;
            Self::ensure_not_shutting_down(&inner)?;
            if use_smtputf8 && !inner.capabilities.supports_smtputf8() {
                return Err(Error::Protocol(
                    "SMTPUTF8 capability disappeared before command dispatch \
                     (RFC 6531 Section 3.7.4.2)"
                        .into(),
                ));
            }
            let mut buf = BytesMut::new();
            if use_smtputf8 {
                smtp_utf8_encoder(&mut buf, argument)?;
            } else {
                encoder(&mut buf, argument)?;
            }
            // RFC 5321 Section 4.5.3.1.4: validate command line length.
            Self::validate_command_line_length(buf.len(), cmd_name)?;
            inner.write_all(&buf).await?;
            inner.read_response().await
        })
        .await
        .map_err(|_| Error::Timeout)?
    }

    /// Gracefully close the connection — QUIT command (RFC 5321 Section 4.1.1.10).
    pub async fn quit(&self, timeout: Duration) -> Result<(), Error> {
        self.send_simple_command(encode::encode_quit, "QUIT", 221, "4.1.1.10", timeout)
            .await
    }

    /// Re-issue the EHLO/LHLO greeting to refresh server capabilities
    /// (RFC 5321 Section 4.1.1.1).
    ///
    /// "An EHLO command MAY be issued by a client later in the session."
    /// This is useful after [`set_ehlo_domain`](Self::set_ehlo_domain) to
    /// send the new domain to the server, or to refresh the capability
    /// snapshot (e.g., after the server has been reconfigured).
    ///
    /// Updates the internal [`ServerCapabilities`] with the server's
    /// fresh EHLO response.
    pub async fn rehlo(&self, timeout: Duration) -> Result<(), Error> {
        let mut inner = self.inner.lock().await;
        Self::ensure_not_shutting_down(&inner)?;
        tokio::time::timeout(timeout, async {
            Self::ehlo_on_inner(&mut inner, self.protocol).await
        })
        .await
        .map_err(|_| Error::Timeout)?
    }
}