Skip to main content

io_smtp/
client.rs

1//! # Standard, blocking SMTP client
2//!
3//! Holds a single stream (any blocking `Read + Write` impl) and exposes one
4//! method per common coroutine. SMTP has no long-lived session context like
5//! IMAP: capabilities are returned by [`greeting`] / [`ehlo`] and consumed by
6//! the caller, and each coroutine is otherwise stateless.
7//!
8//! The bare [`new`] constructor takes a pre-connected stream; callers handle
9//! TCP and TLS themselves. With one of the TLS feature flags enabled
10//! (`rustls-ring`, `rustls-aws`, `native-tls`), [`connect`] is also available
11//! and produces a ready-to-use authenticated client end-to-end: it opens the
12//! transport (plain TCP for `smtp://`, implicit TLS for `smtps://`), reads the
13//! greeting, sends the initial EHLO, optionally performs the STARTTLS upgrade
14//! and a fresh EHLO over TLS, then runs the chosen SASL mechanism if one was
15//! provided.
16//!
17//! [`new`]: SmtpClientStd::new
18//! [`connect`]: SmtpClientStd::connect
19//! [`greeting`]: SmtpClientStd::greeting
20//! [`ehlo`]: SmtpClientStd::ehlo
21
22use core::{any::Any, fmt};
23
24#[cfg(any(
25    feature = "rustls-aws",
26    feature = "rustls-ring",
27    feature = "native-tls"
28))]
29use alloc::string::ToString;
30use alloc::{borrow::Cow, boxed::Box, string::String, vec, vec::Vec};
31
32use std::io::{self, Read, Write};
33
34#[cfg(any(
35    feature = "rustls-aws",
36    feature = "rustls-ring",
37    feature = "native-tls"
38))]
39use bounded_static::IntoBoundedStatic;
40#[cfg(feature = "scram")]
41#[cfg(any(
42    feature = "rustls-aws",
43    feature = "rustls-ring",
44    feature = "native-tls"
45))]
46use pimalaya_stream::sasl::SaslScramSha256;
47#[cfg(any(
48    feature = "rustls-aws",
49    feature = "rustls-ring",
50    feature = "native-tls"
51))]
52use pimalaya_stream::{
53    sasl::{Sasl, SaslAnonymous, SaslLogin, SaslOauthbearer, SaslPlain, SaslXoauth2},
54    std::stream::StreamStd,
55    tls::Tls,
56};
57#[cfg(feature = "scram")]
58#[cfg(any(
59    feature = "rustls-aws",
60    feature = "rustls-ring",
61    feature = "native-tls"
62))]
63use rand::{RngExt, distr::Alphanumeric, rng};
64use secrecy::SecretString;
65use thiserror::Error;
66#[cfg(any(
67    feature = "rustls-aws",
68    feature = "rustls-ring",
69    feature = "native-tls"
70))]
71use url::Url;
72
73#[cfg(feature = "scram")]
74use crate::rfc7677::auth_scram_sha_256::*;
75use crate::{
76    coroutine::*,
77    message::*,
78    rfc3207::starttls::*,
79    rfc5321::{
80        SmtpDomain, SmtpEhloDomain, SmtpForwardPath, SmtpGreeting, SmtpParameter, SmtpReversePath,
81        data::*, ehlo::*, greeting::*, helo::*, mail::*, noop::*, quit::*, raw::*, rcpt::*,
82        rset::*,
83    },
84    rfc7628::auth_oauthbearer::*,
85    sasl::{auth_anonymous::*, auth_login::*, auth_plain::*, auth_xoauth2::*},
86};
87
88/// Errors returned by [`SmtpClientStd`].
89#[derive(Debug, Error)]
90pub enum SmtpClientStdError {
91    /// The greeting coroutine failed.
92    #[error(transparent)]
93    SmtpGreeting(#[from] SmtpGreetingGetError),
94    /// The EHLO coroutine failed.
95    #[error(transparent)]
96    Ehlo(#[from] SmtpEhloError),
97    /// The HELO coroutine failed.
98    #[error(transparent)]
99    Helo(#[from] SmtpHeloError),
100    /// The STARTTLS coroutine failed.
101    #[error(transparent)]
102    StartTls(#[from] SmtpStartTlsError),
103    /// The AUTH ANONYMOUS coroutine failed.
104    #[error(transparent)]
105    AuthAnonymous(#[from] SmtpAuthAnonymousError),
106    /// The AUTH LOGIN coroutine failed.
107    #[error(transparent)]
108    AuthLogin(#[from] SmtpAuthLoginError),
109    /// The AUTH PLAIN coroutine failed.
110    #[error(transparent)]
111    AuthPlain(#[from] SmtpAuthPlainError),
112    /// The AUTH OAUTHBEARER coroutine failed.
113    #[error(transparent)]
114    AuthOAuthBearer(#[from] SmtpAuthOauthbearerError),
115    /// The AUTH XOAUTH2 coroutine failed.
116    #[error(transparent)]
117    AuthXOAuth2(#[from] SmtpAuthXoauth2Error),
118    /// The AUTH SCRAM-SHA-256 coroutine failed.
119    #[cfg(feature = "scram")]
120    #[error(transparent)]
121    AuthScramSha256(#[from] SmtpAuthScramSha256Error),
122    /// SCRAM-SHA-256 was requested but its cargo feature is off.
123    #[cfg(any(
124        feature = "rustls-aws",
125        feature = "rustls-ring",
126        feature = "native-tls"
127    ))]
128    #[cfg(not(feature = "scram"))]
129    #[error("SCRAM-SHA-256 SASL mechanism requires the `scram` cargo feature")]
130    ScramSha256NotEnabled,
131    /// The MAIL FROM coroutine failed.
132    #[error(transparent)]
133    Mail(#[from] SmtpMailError),
134    /// The RCPT TO coroutine failed.
135    #[error(transparent)]
136    Rcpt(#[from] SmtpRcptError),
137    /// The DATA coroutine failed.
138    #[error(transparent)]
139    Data(#[from] SmtpDataError),
140    /// The NOOP coroutine failed.
141    #[error(transparent)]
142    Noop(#[from] SmtpNoopError),
143    /// The raw passthrough coroutine failed.
144    #[error(transparent)]
145    Raw(#[from] SmtpRawError),
146    /// The RSET coroutine failed.
147    #[error(transparent)]
148    Rset(#[from] SmtpRsetError),
149    /// The QUIT coroutine failed.
150    #[error(transparent)]
151    Quit(#[from] SmtpQuitError),
152    /// The composite message send coroutine failed.
153    #[error(transparent)]
154    MessageSend(#[from] SmtpMessageSendError),
155    /// Reading from or writing to the stream failed.
156    #[error(transparent)]
157    Io(#[from] io::Error),
158    /// Opening the TCP connection or negotiating TLS failed.
159    #[cfg(any(
160        feature = "rustls-aws",
161        feature = "rustls-ring",
162        feature = "native-tls"
163    ))]
164    #[error(transparent)]
165    Tls(#[from] anyhow::Error),
166    /// The connection URL carries no host part.
167    #[cfg(any(
168        feature = "rustls-aws",
169        feature = "rustls-ring",
170        feature = "native-tls"
171    ))]
172    #[error("SMTP URL `{0}` has no host")]
173    UrlMissingHost(String),
174    /// The connection URL scheme is neither smtp nor smtps.
175    #[cfg(any(
176        feature = "rustls-aws",
177        feature = "rustls-ring",
178        feature = "native-tls"
179    ))]
180    #[error("SMTP URL `{0}` has unsupported scheme `{1}` (expected `smtp` or `smtps`)")]
181    UrlUnsupportedScheme(String, String),
182    /// STARTTLS was requested on an already-TLS connection.
183    #[cfg(any(
184        feature = "rustls-aws",
185        feature = "rustls-ring",
186        feature = "native-tls"
187    ))]
188    #[error("STARTTLS requested on an `smtps://` URL: TLS is already active")]
189    StartTlsOverTls,
190}
191
192const READ_BUFFER_SIZE: usize = 16 * 1024;
193
194/// Std-blocking SMTP client wrapping a single boxed stream.
195pub struct SmtpClientStd {
196    /// The wrapped stream every coroutine is pumped against.
197    pub stream: Box<dyn SmtpStream>,
198}
199
200impl SmtpClientStd {
201    /// Builds a client around `stream`. The caller is responsible for opening
202    /// the connection (TCP, TLS handshake if needed, STARTTLS upgrade if
203    /// needed).
204    pub fn new<S: Read + Write + Send + 'static>(stream: S) -> Self {
205        Self {
206            stream: Box::new(stream),
207        }
208    }
209
210    /// Default ALPN protocol identifier offered during the TLS handshake for
211    /// SMTP submission connections (RFC 7595 registers the `smtp` token).
212    /// Exposed so config-based callers can use it as a serde default and so
213    /// wizard/discovery code shares a single source of truth.
214    pub fn default_alpn() -> Vec<String> {
215        vec![String::from("smtp")]
216    }
217
218    /// Default SMTP port for `scheme`: 465 for `smtps`, 25 otherwise.
219    pub fn default_port(scheme: &str) -> u16 {
220        if scheme.eq_ignore_ascii_case("smtps") {
221            465
222        } else {
223            25
224        }
225    }
226
227    /// Replaces the underlying stream; useful after a caller-managed TLS
228    /// upgrade or reconnection.
229    pub fn set_stream<S: Read + Write + Send + 'static>(&mut self, stream: S) {
230        self.stream = Box::new(stream);
231    }
232
233    /// Pumps any standard-shape coroutine (`Yield = SmtpYield`, `Return =
234    /// Result<Output, Error>`) against the wrapped stream until it
235    /// terminates. Every coroutine in this crate (including [`SmtpStartTls`])
236    /// fits this signature.
237    pub fn run<C, T, E>(&mut self, mut coroutine: C) -> Result<T, SmtpClientStdError>
238    where
239        C: SmtpCoroutine<Yield = SmtpYield, Return = Result<T, E>>,
240        SmtpClientStdError: From<E>,
241    {
242        let mut buf = [0u8; READ_BUFFER_SIZE];
243        let mut arg: Option<&[u8]> = None;
244
245        loop {
246            match coroutine.resume(arg.take()) {
247                SmtpCoroutineState::Complete(Ok(out)) => return Ok(out),
248                SmtpCoroutineState::Complete(Err(err)) => return Err(err.into()),
249                SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {
250                    let n = self.stream.read(&mut buf)?;
251                    arg = Some(&buf[..n]);
252                }
253                SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => {
254                    self.stream.write_all(&bytes)?;
255                    arg = None;
256                }
257            }
258        }
259    }
260
261    // NOTE: Session lifecycle methods below.
262
263    /// Runs [`SmtpGreetingGet`]: reads the initial server greeting.  Call this
264    /// once after [`new`] / [`connect`].
265    ///
266    /// [`new`]: SmtpClientStd::new
267    /// [`connect`]: SmtpClientStd::connect
268    pub fn greeting(&mut self) -> Result<SmtpGreeting<'static>, SmtpClientStdError> {
269        self.run(SmtpGreetingGet::new())
270    }
271
272    /// Runs [`SmtpEhlo`] (`EHLO <domain>`, RFC 5321 §4.1.1.1). Returns the raw
273    /// capability lines reported by the server.
274    pub fn ehlo(
275        &mut self,
276        domain: SmtpEhloDomain<'_>,
277    ) -> Result<Vec<Cow<'static, str>>, SmtpClientStdError> {
278        self.run(SmtpEhlo::new(domain))
279    }
280
281    /// Runs [`SmtpHelo`] (`HELO <domain>`, RFC 5321 §4.1.1.1). Use [`ehlo`] on
282    /// any modern server; fall back to HELO only if the server rejects EHLO
283    /// with 500/502.
284    ///
285    /// [`ehlo`]: SmtpClientStd::ehlo
286    pub fn helo(&mut self, domain: SmtpDomain<'_>) -> Result<(), SmtpClientStdError> {
287        self.run(SmtpHelo::new(domain))
288    }
289
290    /// Runs [`SmtpStartTls`] (`STARTTLS`, RFC 3207). On success the caller must
291    /// upgrade the underlying socket to TLS (via [`StreamStd::upgrade_tls`]),
292    /// then build a new client around the upgraded stream and re-issue
293    /// [`ehlo`]. The returned bytes are anything the coroutine pre-read past
294    /// the `220` reply (normally empty; any pre-handshake bytes are a classic
295    /// STARTTLS-injection signal).
296    ///
297    /// [`StreamStd::upgrade_tls`]: pimalaya_stream::std::stream::StreamStd::upgrade_tls
298    /// [`ehlo`]: SmtpClientStd::ehlo
299    pub fn starttls(&mut self) -> Result<Vec<u8>, SmtpClientStdError> {
300        self.run(SmtpStartTls::new())
301    }
302
303    /// Runs [`SmtpQuit`] (`QUIT`, RFC 5321 §4.1.1.10).
304    pub fn quit(&mut self) -> Result<(), SmtpClientStdError> {
305        self.run(SmtpQuit::new())
306    }
307
308    // NOTE: Authentication methods below.
309
310    /// Runs [`SmtpAuthAnonymous`] (`AUTH ANONYMOUS`, RFC 4505). The optional
311    /// `trace` token is sent in cleartext for server-side logging; do not put
312    /// credentials in it.
313    pub fn auth_anonymous(
314        &mut self,
315        trace: Option<&str>,
316        domain: SmtpEhloDomain<'_>,
317    ) -> Result<(), SmtpClientStdError> {
318        self.run(SmtpAuthAnonymous::new(
319            trace,
320            domain,
321            SmtpAuthAnonymousOptions::default(),
322        ))
323    }
324
325    /// Runs [`SmtpAuthLogin`] (`AUTH LOGIN`, legacy SASL mechanism). Prefer
326    /// [`auth_plain`] or [`auth_scram_sha256`] when the server supports them.
327    ///
328    /// [`auth_plain`]: SmtpClientStd::auth_plain
329    /// [`auth_scram_sha256`]: SmtpClientStd::auth_scram_sha256
330    pub fn auth_login(
331        &mut self,
332        login: &str,
333        password: &SecretString,
334        domain: SmtpEhloDomain<'_>,
335    ) -> Result<(), SmtpClientStdError> {
336        self.run(SmtpAuthLogin::new(
337            login,
338            password,
339            domain,
340            SmtpAuthLoginOptions::default(),
341        ))
342    }
343
344    /// Runs [`SmtpAuthPlain`] (`AUTH PLAIN`, RFC 4616).
345    pub fn auth_plain(
346        &mut self,
347        login: &str,
348        password: &SecretString,
349        domain: SmtpEhloDomain<'_>,
350    ) -> Result<(), SmtpClientStdError> {
351        self.run(SmtpAuthPlain::new(
352            login,
353            password,
354            domain,
355            SmtpAuthPlainOptions::default(),
356        ))
357    }
358
359    /// Runs [`SmtpAuthOauthbearer`] (`AUTH OAUTHBEARER`, RFC 7628). The `token`
360    /// is an OAuth 2.0 bearer access token: the connection **must** be
361    /// TLS-protected before calling this method.
362    pub fn auth_oauthbearer(
363        &mut self,
364        token: &SecretString,
365        username: Option<&str>,
366        domain: SmtpEhloDomain<'_>,
367    ) -> Result<(), SmtpClientStdError> {
368        self.run(SmtpAuthOauthbearer::new(
369            token,
370            username,
371            domain,
372            SmtpAuthOauthbearerOptions::default(),
373        ))
374    }
375
376    /// Runs [`SmtpAuthXoauth2`] (`AUTH XOAUTH2`, Google's pre-standard OAuth
377    /// 2.0 SASL mechanism). The `token` is an OAuth 2.0 bearer access token:
378    /// the connection **must** be TLS-protected before calling this method.
379    /// Prefer [`auth_oauthbearer`] on servers that support both.
380    ///
381    /// [`auth_oauthbearer`]: SmtpClientStd::auth_oauthbearer
382    pub fn auth_xoauth2(
383        &mut self,
384        username: &str,
385        token: &SecretString,
386        domain: SmtpEhloDomain<'_>,
387    ) -> Result<(), SmtpClientStdError> {
388        self.run(SmtpAuthXoauth2::new(
389            username,
390            token,
391            domain,
392            SmtpAuthXoauth2Options::default(),
393        ))
394    }
395
396    /// Runs [`SmtpAuthScramSha256`] (`AUTH SCRAM-SHA-256`, RFC 7677). `nonce`
397    /// must be printable ASCII (no commas); the standard recommends at least
398    /// 18 bytes of cryptographic randomness.
399    #[cfg(feature = "scram")]
400    pub fn auth_scram_sha256(
401        &mut self,
402        username: &str,
403        password: &SecretString,
404        nonce: &[u8],
405        domain: SmtpEhloDomain<'_>,
406    ) -> Result<(), SmtpClientStdError> {
407        self.run(SmtpAuthScramSha256::new(
408            username,
409            password,
410            nonce,
411            domain,
412            SmtpAuthScramSha256Options::default(),
413        ))
414    }
415
416    // NOTE: Mail transaction methods below.
417
418    /// Runs [`SmtpMail`] (`MAIL FROM:<reverse-path>`, RFC 5321
419    /// §4.1.1.2). Pass an empty `parameters` vector for the bare
420    /// form, non-empty entries for ESMTP parameters (e.g. `SIZE=`,
421    /// `BODY=`, DSN).
422    pub fn mail(
423        &mut self,
424        reverse_path: SmtpReversePath<'_>,
425        parameters: Vec<SmtpParameter<'_>>,
426    ) -> Result<(), SmtpClientStdError> {
427        self.run(SmtpMail::new(reverse_path, parameters))
428    }
429
430    /// Runs [`SmtpRcpt`] (`RCPT TO:<forward-path>`, RFC 5321
431    /// §4.1.1.3). Pass an empty `parameters` vector for the bare
432    /// form, non-empty entries for ESMTP parameters (e.g. DSN
433    /// `NOTIFY=`, `ORCPT=`).
434    pub fn rcpt(
435        &mut self,
436        forward_path: SmtpForwardPath<'_>,
437        parameters: Vec<SmtpParameter<'_>>,
438    ) -> Result<(), SmtpClientStdError> {
439        self.run(SmtpRcpt::new(forward_path, parameters))
440    }
441
442    /// Runs [`SmtpData`] (`DATA` + body terminator, RFC 5321
443    /// §4.1.1.4). The coroutine handles dot-stuffing automatically.
444    pub fn data(&mut self, message: Vec<u8>) -> Result<(), SmtpClientStdError> {
445        self.run(SmtpData::new(message))
446    }
447
448    /// Runs [`SmtpRset`] (`RSET`, RFC 5321 §4.1.1.5). Aborts the
449    /// current mail transaction.
450    pub fn rset(&mut self) -> Result<(), SmtpClientStdError> {
451        self.run(SmtpRset::new())
452    }
453
454    /// Runs [`SmtpNoop`] (`NOOP`, RFC 5321 §4.1.1.9).
455    pub fn noop(&mut self) -> Result<(), SmtpClientStdError> {
456        self.run(SmtpNoop::new())
457    }
458
459    /// Runs [`SmtpRaw`]: sends an arbitrary command line (without the
460    /// trailing CRLF) and returns the server reply verbatim. Reserved
461    /// for simple request/reply commands; do not use for DATA or
462    /// STARTTLS, which switch the stream into a different mode.
463    pub fn raw(
464        &mut self,
465        command: impl Into<Cow<'static, str>>,
466    ) -> Result<String, SmtpClientStdError> {
467        self.run(SmtpRaw::new(command))
468    }
469
470    // NOTE: High-level helpers below.
471
472    /// Runs [`SmtpMessageSend`]: a complete `MAIL FROM` / `RCPT TO`
473    /// (one per recipient) / `DATA` exchange in one call.
474    pub fn send<'a>(
475        &mut self,
476        reverse_path: SmtpReversePath<'_>,
477        forward_paths: impl IntoIterator<Item = SmtpForwardPath<'a>>,
478        message: Vec<u8>,
479    ) -> Result<(), SmtpClientStdError> {
480        self.run(SmtpMessageSend::new(reverse_path, forward_paths, message))
481    }
482}
483
484impl fmt::Debug for SmtpClientStd {
485    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
486        f.debug_struct("SmtpClientStd").finish_non_exhaustive()
487    }
488}
489
490#[cfg(any(
491    feature = "rustls-aws",
492    feature = "rustls-ring",
493    feature = "native-tls"
494))]
495impl SmtpClientStd {
496    /// Connects to `url`, reads the greeting, sends an initial EHLO,
497    /// optionally performs the STARTTLS upgrade (then re-sends EHLO),
498    /// and finally runs the chosen SASL mechanism.
499    ///
500    /// - `smtp://`  goes through plain TCP (port defaults to 25).
501    /// - `smtps://` goes through implicit TLS (port defaults to 465).
502    /// - `tls` carries the rustls/native-tls knobs *and* the ALPN list
503    ///   (see [`Self::default_alpn`] for the SMTP-conformant `["smtp"]`).
504    ///   Set `tls.rustls.alpn` to an empty vec to skip ALPN.
505    /// - `starttls = true` (only valid on `smtp://`) performs the SMTP
506    ///   `STARTTLS` upgrade and runs a fresh EHLO over TLS.
507    /// - `domain` is the client identifier sent in EHLO (typically
508    ///   the sending host's name or an address literal).
509    /// - `sasl` is the optional SASL mechanism. Accepts anything that
510    ///   converts into a [`Sasl`], so callers can pass the
511    ///   per-mechanism struct directly (e.g. `Some(SaslPlain { .. })`)
512    ///   without wrapping it in a [`Sasl`] variant. Supported
513    ///   mechanisms: [`SaslAnonymous`] (RFC 4505), [`SaslLogin`]
514    ///   (legacy two-prompt LOGIN), [`SaslPlain`] (RFC 4616),
515    ///   [`SaslOauthbearer`] (RFC 7628), [`SaslXoauth2`] (Google),
516    ///   and [`SaslScramSha256`] (RFC 7677, behind the `scram` cargo
517    ///   feature). Pass [`None`] to skip authentication.
518    ///
519    /// Returns a fully authenticated client ready to issue further
520    /// commands.
521    pub fn connect(
522        url: &Url,
523        tls: &Tls,
524        starttls: bool,
525        domain: SmtpEhloDomain<'_>,
526        sasl: Option<impl Into<Sasl>>,
527    ) -> Result<Self, SmtpClientStdError> {
528        let (stream, is_tls) = match url.scheme() {
529            scheme if scheme.eq_ignore_ascii_case("smtp") => {
530                let host = tcp_host(url)?;
531                (
532                    StreamStd::connect_tcp(host, url.port().unwrap_or(Self::default_port(scheme)))?,
533                    false,
534                )
535            }
536            scheme if scheme.eq_ignore_ascii_case("smtps") => {
537                let host = tcp_host(url)?;
538                (
539                    StreamStd::connect_tls(
540                        host,
541                        url.port().unwrap_or(Self::default_port(scheme)),
542                        tls,
543                    )?,
544                    true,
545                )
546            }
547            // NOTE: a `unix://` URL reaches a local socket proxy such as
548            // sirup: no host and no TLS. SMTP has no PREAUTH greeting, so
549            // authentication is skipped only when no SASL config is passed.
550            scheme if scheme.eq_ignore_ascii_case("unix") => {
551                (StreamStd::connect_unix(url.path())?, false)
552            }
553            scheme => {
554                let url = url.to_string();
555                let scheme = scheme.to_string();
556                return Err(SmtpClientStdError::UrlUnsupportedScheme(url, scheme));
557            }
558        };
559
560        if starttls && is_tls {
561            return Err(SmtpClientStdError::StartTlsOverTls);
562        }
563
564        let domain = domain.into_static();
565
566        // NOTE: STARTTLS needs the concrete StreamStd to call
567        // upgrade_tls after the SMTP-layer handshake; once boxed
568        // there is no way back to the concrete type. Run greeting +
569        // the initial EHLO (and optionally STARTTLS) inline against
570        // the raw stream, upgrade if requested, then build the boxed
571        // client.
572        let stream = {
573            let mut stream = stream;
574            run_smtp_inline(&mut stream, SmtpGreetingGet::new())?;
575            run_smtp_inline(&mut stream, SmtpEhlo::new(domain.clone()))?;
576            if starttls {
577                run_smtp_inline(&mut stream, SmtpStartTls::new())?;
578                stream.upgrade_tls(tls)?
579            } else {
580                stream
581            }
582        };
583
584        let mut client = Self::new(stream);
585
586        if starttls {
587            client.ehlo(domain.clone())?;
588        }
589
590        if let Some(sasl) = sasl.map(Into::into) {
591            match sasl {
592                Sasl::Anonymous(SaslAnonymous { message }) => {
593                    client.auth_anonymous(message.as_deref(), domain.clone())?;
594                }
595                Sasl::Login(SaslLogin { username, password }) => {
596                    client.auth_login(&username, &password, domain.clone())?;
597                }
598                Sasl::Plain(SaslPlain {
599                    authzid: _,
600                    authcid,
601                    passwd,
602                }) => {
603                    client.auth_plain(&authcid, &passwd, domain.clone())?;
604                }
605                Sasl::Oauthbearer(SaslOauthbearer {
606                    username,
607                    host: _,
608                    port: _,
609                    token,
610                }) => {
611                    client.auth_oauthbearer(&token, Some(&username), domain.clone())?;
612                }
613                Sasl::Xoauth2(SaslXoauth2 { username, token }) => {
614                    client.auth_xoauth2(&username, &token, domain.clone())?;
615                }
616                #[cfg(feature = "scram")]
617                Sasl::ScramSha256(SaslScramSha256 { username, password }) => {
618                    let nonce = rng()
619                        .sample_iter(Alphanumeric)
620                        .take(24)
621                        .collect::<Vec<u8>>();
622                    client.auth_scram_sha256(&username, &password, &nonce, domain.clone())?;
623                }
624                #[cfg(not(feature = "scram"))]
625                Sasl::ScramSha256(_) => {
626                    return Err(SmtpClientStdError::ScramSha256NotEnabled);
627                }
628            }
629        }
630
631        Ok(client)
632    }
633}
634
635/// Extracts the host from a TCP-bound SMTP URL (`smtp`/`smtps`), erroring
636/// when it carries none. The `unix` scheme does not go through here.
637fn tcp_host(url: &Url) -> Result<&str, SmtpClientStdError> {
638    url.host_str()
639        .ok_or_else(|| SmtpClientStdError::UrlMissingHost(url.to_string()))
640}
641
642/// Pumps any standard-shape SMTP coroutine inline against a concrete
643/// [`StreamStd`]. Used by [`SmtpClientStd::connect`] to run greeting +
644/// the pre-STARTTLS EHLO before boxing the stream; the boxed
645/// [`SmtpClientStd::stream`] hides the concrete type that
646/// [`StreamStd::upgrade_tls`] needs.
647#[cfg(any(
648    feature = "rustls-aws",
649    feature = "rustls-ring",
650    feature = "native-tls"
651))]
652fn run_smtp_inline<C, T, E>(
653    stream: &mut StreamStd,
654    mut coroutine: C,
655) -> Result<T, SmtpClientStdError>
656where
657    C: SmtpCoroutine<Yield = SmtpYield, Return = Result<T, E>>,
658    SmtpClientStdError: From<E>,
659{
660    let mut buf = [0u8; READ_BUFFER_SIZE];
661    let mut arg: Option<&[u8]> = None;
662
663    loop {
664        match coroutine.resume(arg.take()) {
665            SmtpCoroutineState::Complete(Ok(out)) => return Ok(out),
666            SmtpCoroutineState::Complete(Err(err)) => return Err(err.into()),
667            SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {
668                let n = stream.read(&mut buf)?;
669                arg = Some(&buf[..n]);
670            }
671            SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => {
672                stream.write_all(&bytes)?;
673            }
674        }
675    }
676}
677
678/// Marker for everything the client can run against; auto-implemented for any
679/// blocking `Read + Write + Send + 'static` impl. The `Send` supertrait flows
680/// the auto-trait through the `Box<dyn SmtpStream>` type erasure so
681/// `SmtpClientStd` can travel between threads.  [`as_any_mut`] lets specialized
682/// callers (e.g. byte-level proxies that need [`StreamStd::set_read_timeout`])
683/// downcast the boxed stream back to its concrete type.
684///
685/// [`as_any_mut`]: SmtpStream::as_any_mut
686/// [`StreamStd::set_read_timeout`]: pimalaya_stream::std::stream::StreamStd::set_read_timeout
687pub trait SmtpStream: Read + Write + Send + Any {
688    /// Downcasts the boxed stream back to its concrete type.
689    fn as_any_mut(&mut self) -> &mut dyn Any;
690}
691
692impl<T: Read + Write + Send + Any> SmtpStream for T {
693    fn as_any_mut(&mut self) -> &mut dyn Any {
694        self
695    }
696}