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    /// Replaces the underlying stream; useful after a caller-managed TLS
219    /// upgrade or reconnection.
220    pub fn set_stream<S: Read + Write + Send + 'static>(&mut self, stream: S) {
221        self.stream = Box::new(stream);
222    }
223
224    /// Pumps any standard-shape coroutine (`Yield = SmtpYield`, `Return =
225    /// Result<Output, Error>`) against the wrapped stream until it
226    /// terminates. Every coroutine in this crate (including [`SmtpStartTls`])
227    /// fits this signature.
228    pub fn run<C, T, E>(&mut self, mut coroutine: C) -> Result<T, SmtpClientStdError>
229    where
230        C: SmtpCoroutine<Yield = SmtpYield, Return = Result<T, E>>,
231        SmtpClientStdError: From<E>,
232    {
233        let mut buf = [0u8; READ_BUFFER_SIZE];
234        let mut arg: Option<&[u8]> = None;
235
236        loop {
237            match coroutine.resume(arg.take()) {
238                SmtpCoroutineState::Complete(Ok(out)) => return Ok(out),
239                SmtpCoroutineState::Complete(Err(err)) => return Err(err.into()),
240                SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {
241                    let n = self.stream.read(&mut buf)?;
242                    arg = Some(&buf[..n]);
243                }
244                SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => {
245                    self.stream.write_all(&bytes)?;
246                    arg = None;
247                }
248            }
249        }
250    }
251
252    // NOTE: Session lifecycle methods below.
253
254    /// Runs [`SmtpGreetingGet`]: reads the initial server greeting.  Call this
255    /// once after [`new`] / [`connect`].
256    ///
257    /// [`new`]: SmtpClientStd::new
258    /// [`connect`]: SmtpClientStd::connect
259    pub fn greeting(&mut self) -> Result<SmtpGreeting<'static>, SmtpClientStdError> {
260        self.run(SmtpGreetingGet::new())
261    }
262
263    /// Runs [`SmtpEhlo`] (`EHLO <domain>`, RFC 5321 §4.1.1.1). Returns the raw
264    /// capability lines reported by the server.
265    pub fn ehlo(
266        &mut self,
267        domain: SmtpEhloDomain<'_>,
268    ) -> Result<Vec<Cow<'static, str>>, SmtpClientStdError> {
269        self.run(SmtpEhlo::new(domain))
270    }
271
272    /// Runs [`SmtpHelo`] (`HELO <domain>`, RFC 5321 §4.1.1.1). Use [`ehlo`] on
273    /// any modern server; fall back to HELO only if the server rejects EHLO
274    /// with 500/502.
275    ///
276    /// [`ehlo`]: SmtpClientStd::ehlo
277    pub fn helo(&mut self, domain: SmtpDomain<'_>) -> Result<(), SmtpClientStdError> {
278        self.run(SmtpHelo::new(domain))
279    }
280
281    /// Runs [`SmtpStartTls`] (`STARTTLS`, RFC 3207). On success the caller must
282    /// upgrade the underlying socket to TLS (via [`StreamStd::upgrade_tls`]),
283    /// then build a new client around the upgraded stream and re-issue
284    /// [`ehlo`]. The returned bytes are anything the coroutine pre-read past
285    /// the `220` reply (normally empty; any pre-handshake bytes are a classic
286    /// STARTTLS-injection signal).
287    ///
288    /// [`StreamStd::upgrade_tls`]: pimalaya_stream::std::stream::StreamStd::upgrade_tls
289    /// [`ehlo`]: SmtpClientStd::ehlo
290    pub fn starttls(&mut self) -> Result<Vec<u8>, SmtpClientStdError> {
291        self.run(SmtpStartTls::new())
292    }
293
294    /// Runs [`SmtpQuit`] (`QUIT`, RFC 5321 §4.1.1.10).
295    pub fn quit(&mut self) -> Result<(), SmtpClientStdError> {
296        self.run(SmtpQuit::new())
297    }
298
299    // NOTE: Authentication methods below.
300
301    /// Runs [`SmtpAuthAnonymous`] (`AUTH ANONYMOUS`, RFC 4505). The optional
302    /// `trace` token is sent in cleartext for server-side logging; do not put
303    /// credentials in it.
304    pub fn auth_anonymous(
305        &mut self,
306        trace: Option<&str>,
307        domain: SmtpEhloDomain<'_>,
308    ) -> Result<(), SmtpClientStdError> {
309        self.run(SmtpAuthAnonymous::new(
310            trace,
311            domain,
312            SmtpAuthAnonymousOptions::default(),
313        ))
314    }
315
316    /// Runs [`SmtpAuthLogin`] (`AUTH LOGIN`, legacy SASL mechanism). Prefer
317    /// [`auth_plain`] or [`auth_scram_sha256`] when the server supports them.
318    ///
319    /// [`auth_plain`]: SmtpClientStd::auth_plain
320    /// [`auth_scram_sha256`]: SmtpClientStd::auth_scram_sha256
321    pub fn auth_login(
322        &mut self,
323        login: &str,
324        password: &SecretString,
325        domain: SmtpEhloDomain<'_>,
326    ) -> Result<(), SmtpClientStdError> {
327        self.run(SmtpAuthLogin::new(
328            login,
329            password,
330            domain,
331            SmtpAuthLoginOptions::default(),
332        ))
333    }
334
335    /// Runs [`SmtpAuthPlain`] (`AUTH PLAIN`, RFC 4616).
336    pub fn auth_plain(
337        &mut self,
338        login: &str,
339        password: &SecretString,
340        domain: SmtpEhloDomain<'_>,
341    ) -> Result<(), SmtpClientStdError> {
342        self.run(SmtpAuthPlain::new(
343            login,
344            password,
345            domain,
346            SmtpAuthPlainOptions::default(),
347        ))
348    }
349
350    /// Runs [`SmtpAuthOauthbearer`] (`AUTH OAUTHBEARER`, RFC 7628). The `token`
351    /// is an OAuth 2.0 bearer access token: the connection **must** be
352    /// TLS-protected before calling this method.
353    pub fn auth_oauthbearer(
354        &mut self,
355        token: &SecretString,
356        username: Option<&str>,
357        domain: SmtpEhloDomain<'_>,
358    ) -> Result<(), SmtpClientStdError> {
359        self.run(SmtpAuthOauthbearer::new(
360            token,
361            username,
362            domain,
363            SmtpAuthOauthbearerOptions::default(),
364        ))
365    }
366
367    /// Runs [`SmtpAuthXoauth2`] (`AUTH XOAUTH2`, Google's pre-standard OAuth
368    /// 2.0 SASL mechanism). The `token` is an OAuth 2.0 bearer access token:
369    /// the connection **must** be TLS-protected before calling this method.
370    /// Prefer [`auth_oauthbearer`] on servers that support both.
371    ///
372    /// [`auth_oauthbearer`]: SmtpClientStd::auth_oauthbearer
373    pub fn auth_xoauth2(
374        &mut self,
375        username: &str,
376        token: &SecretString,
377        domain: SmtpEhloDomain<'_>,
378    ) -> Result<(), SmtpClientStdError> {
379        self.run(SmtpAuthXoauth2::new(
380            username,
381            token,
382            domain,
383            SmtpAuthXoauth2Options::default(),
384        ))
385    }
386
387    /// Runs [`SmtpAuthScramSha256`] (`AUTH SCRAM-SHA-256`, RFC 7677). `nonce`
388    /// must be printable ASCII (no commas); the standard recommends at least
389    /// 18 bytes of cryptographic randomness.
390    #[cfg(feature = "scram")]
391    pub fn auth_scram_sha256(
392        &mut self,
393        username: &str,
394        password: &SecretString,
395        nonce: &[u8],
396        domain: SmtpEhloDomain<'_>,
397    ) -> Result<(), SmtpClientStdError> {
398        self.run(SmtpAuthScramSha256::new(
399            username,
400            password,
401            nonce,
402            domain,
403            SmtpAuthScramSha256Options::default(),
404        ))
405    }
406
407    // NOTE: Mail transaction methods below.
408
409    /// Runs [`SmtpMail`] (`MAIL FROM:<reverse-path>`, RFC 5321
410    /// §4.1.1.2). Pass an empty `parameters` vector for the bare
411    /// form, non-empty entries for ESMTP parameters (e.g. `SIZE=`,
412    /// `BODY=`, DSN).
413    pub fn mail(
414        &mut self,
415        reverse_path: SmtpReversePath<'_>,
416        parameters: Vec<SmtpParameter<'_>>,
417    ) -> Result<(), SmtpClientStdError> {
418        self.run(SmtpMail::new(reverse_path, parameters))
419    }
420
421    /// Runs [`SmtpRcpt`] (`RCPT TO:<forward-path>`, RFC 5321
422    /// §4.1.1.3). Pass an empty `parameters` vector for the bare
423    /// form, non-empty entries for ESMTP parameters (e.g. DSN
424    /// `NOTIFY=`, `ORCPT=`).
425    pub fn rcpt(
426        &mut self,
427        forward_path: SmtpForwardPath<'_>,
428        parameters: Vec<SmtpParameter<'_>>,
429    ) -> Result<(), SmtpClientStdError> {
430        self.run(SmtpRcpt::new(forward_path, parameters))
431    }
432
433    /// Runs [`SmtpData`] (`DATA` + body terminator, RFC 5321
434    /// §4.1.1.4). The coroutine handles dot-stuffing automatically.
435    pub fn data(&mut self, message: Vec<u8>) -> Result<(), SmtpClientStdError> {
436        self.run(SmtpData::new(message))
437    }
438
439    /// Runs [`SmtpRset`] (`RSET`, RFC 5321 §4.1.1.5). Aborts the
440    /// current mail transaction.
441    pub fn rset(&mut self) -> Result<(), SmtpClientStdError> {
442        self.run(SmtpRset::new())
443    }
444
445    /// Runs [`SmtpNoop`] (`NOOP`, RFC 5321 §4.1.1.9).
446    pub fn noop(&mut self) -> Result<(), SmtpClientStdError> {
447        self.run(SmtpNoop::new())
448    }
449
450    /// Runs [`SmtpRaw`]: sends an arbitrary command line (without the
451    /// trailing CRLF) and returns the server reply verbatim. Reserved
452    /// for simple request/reply commands; do not use for DATA or
453    /// STARTTLS, which switch the stream into a different mode.
454    pub fn raw(
455        &mut self,
456        command: impl Into<Cow<'static, str>>,
457    ) -> Result<String, SmtpClientStdError> {
458        self.run(SmtpRaw::new(command))
459    }
460
461    // NOTE: High-level helpers below.
462
463    /// Runs [`SmtpMessageSend`]: a complete `MAIL FROM` / `RCPT TO`
464    /// (one per recipient) / `DATA` exchange in one call.
465    pub fn send<'a>(
466        &mut self,
467        reverse_path: SmtpReversePath<'_>,
468        forward_paths: impl IntoIterator<Item = SmtpForwardPath<'a>>,
469        message: Vec<u8>,
470    ) -> Result<(), SmtpClientStdError> {
471        self.run(SmtpMessageSend::new(reverse_path, forward_paths, message))
472    }
473}
474
475impl fmt::Debug for SmtpClientStd {
476    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
477        f.debug_struct("SmtpClientStd").finish_non_exhaustive()
478    }
479}
480
481#[cfg(any(
482    feature = "rustls-aws",
483    feature = "rustls-ring",
484    feature = "native-tls"
485))]
486impl SmtpClientStd {
487    /// Connects to `url`, reads the greeting, sends an initial EHLO,
488    /// optionally performs the STARTTLS upgrade (then re-sends EHLO),
489    /// and finally runs the chosen SASL mechanism.
490    ///
491    /// - `smtp://`  goes through plain TCP (port defaults to 25).
492    /// - `smtps://` goes through implicit TLS (port defaults to 465).
493    /// - `tls` carries the rustls/native-tls knobs *and* the ALPN list
494    ///   (see [`Self::default_alpn`] for the SMTP-conformant `["smtp"]`).
495    ///   Set `tls.rustls.alpn` to an empty vec to skip ALPN.
496    /// - `starttls = true` (only valid on `smtp://`) performs the SMTP
497    ///   `STARTTLS` upgrade and runs a fresh EHLO over TLS.
498    /// - `domain` is the client identifier sent in EHLO (typically
499    ///   the sending host's name or an address literal).
500    /// - `sasl` is the optional SASL mechanism. Accepts anything that
501    ///   converts into a [`Sasl`], so callers can pass the
502    ///   per-mechanism struct directly (e.g. `Some(SaslPlain { .. })`)
503    ///   without wrapping it in a [`Sasl`] variant. Supported
504    ///   mechanisms: [`SaslAnonymous`] (RFC 4505), [`SaslLogin`]
505    ///   (legacy two-prompt LOGIN), [`SaslPlain`] (RFC 4616),
506    ///   [`SaslOauthbearer`] (RFC 7628), [`SaslXoauth2`] (Google),
507    ///   and [`SaslScramSha256`] (RFC 7677, behind the `scram` cargo
508    ///   feature). Pass [`None`] to skip authentication.
509    ///
510    /// Returns a fully authenticated client ready to issue further
511    /// commands.
512    pub fn connect(
513        url: &Url,
514        tls: &Tls,
515        starttls: bool,
516        domain: SmtpEhloDomain<'_>,
517        sasl: Option<impl Into<Sasl>>,
518    ) -> Result<Self, SmtpClientStdError> {
519        let (stream, is_tls) = match url.scheme() {
520            scheme if scheme.eq_ignore_ascii_case("smtp") => {
521                let host = tcp_host(url)?;
522                (
523                    StreamStd::connect_tcp(host, url.port().unwrap_or(25))?,
524                    false,
525                )
526            }
527            scheme if scheme.eq_ignore_ascii_case("smtps") => {
528                let host = tcp_host(url)?;
529                (
530                    StreamStd::connect_tls(host, url.port().unwrap_or(465), tls)?,
531                    true,
532                )
533            }
534            // NOTE: a `unix://` URL reaches a local socket proxy such as
535            // sirup: no host and no TLS. SMTP has no PREAUTH greeting, so
536            // authentication is skipped only when no SASL config is passed.
537            scheme if scheme.eq_ignore_ascii_case("unix") => {
538                (StreamStd::connect_unix(url.path())?, false)
539            }
540            scheme => {
541                let url = url.to_string();
542                let scheme = scheme.to_string();
543                return Err(SmtpClientStdError::UrlUnsupportedScheme(url, scheme));
544            }
545        };
546
547        if starttls && is_tls {
548            return Err(SmtpClientStdError::StartTlsOverTls);
549        }
550
551        let domain = domain.into_static();
552
553        // NOTE: STARTTLS needs the concrete StreamStd to call
554        // upgrade_tls after the SMTP-layer handshake; once boxed
555        // there is no way back to the concrete type. Run greeting +
556        // the initial EHLO (and optionally STARTTLS) inline against
557        // the raw stream, upgrade if requested, then build the boxed
558        // client.
559        let stream = {
560            let mut stream = stream;
561            run_smtp_inline(&mut stream, SmtpGreetingGet::new())?;
562            run_smtp_inline(&mut stream, SmtpEhlo::new(domain.clone()))?;
563            if starttls {
564                run_smtp_inline(&mut stream, SmtpStartTls::new())?;
565                stream.upgrade_tls(tls)?
566            } else {
567                stream
568            }
569        };
570
571        let mut client = Self::new(stream);
572
573        if starttls {
574            client.ehlo(domain.clone())?;
575        }
576
577        if let Some(sasl) = sasl.map(Into::into) {
578            match sasl {
579                Sasl::Anonymous(SaslAnonymous { message }) => {
580                    client.auth_anonymous(message.as_deref(), domain.clone())?;
581                }
582                Sasl::Login(SaslLogin { username, password }) => {
583                    client.auth_login(&username, &password, domain.clone())?;
584                }
585                Sasl::Plain(SaslPlain {
586                    authzid: _,
587                    authcid,
588                    passwd,
589                }) => {
590                    client.auth_plain(&authcid, &passwd, domain.clone())?;
591                }
592                Sasl::Oauthbearer(SaslOauthbearer {
593                    username,
594                    host: _,
595                    port: _,
596                    token,
597                }) => {
598                    client.auth_oauthbearer(&token, Some(&username), domain.clone())?;
599                }
600                Sasl::Xoauth2(SaslXoauth2 { username, token }) => {
601                    client.auth_xoauth2(&username, &token, domain.clone())?;
602                }
603                #[cfg(feature = "scram")]
604                Sasl::ScramSha256(SaslScramSha256 { username, password }) => {
605                    let nonce = rng()
606                        .sample_iter(Alphanumeric)
607                        .take(24)
608                        .collect::<Vec<u8>>();
609                    client.auth_scram_sha256(&username, &password, &nonce, domain.clone())?;
610                }
611                #[cfg(not(feature = "scram"))]
612                Sasl::ScramSha256(_) => {
613                    return Err(SmtpClientStdError::ScramSha256NotEnabled);
614                }
615            }
616        }
617
618        Ok(client)
619    }
620}
621
622/// Extracts the host from a TCP-bound SMTP URL (`smtp`/`smtps`), erroring
623/// when it carries none. The `unix` scheme does not go through here.
624fn tcp_host(url: &Url) -> Result<&str, SmtpClientStdError> {
625    url.host_str()
626        .ok_or_else(|| SmtpClientStdError::UrlMissingHost(url.to_string()))
627}
628
629/// Pumps any standard-shape SMTP coroutine inline against a concrete
630/// [`StreamStd`]. Used by [`SmtpClientStd::connect`] to run greeting +
631/// the pre-STARTTLS EHLO before boxing the stream; the boxed
632/// [`SmtpClientStd::stream`] hides the concrete type that
633/// [`StreamStd::upgrade_tls`] needs.
634#[cfg(any(
635    feature = "rustls-aws",
636    feature = "rustls-ring",
637    feature = "native-tls"
638))]
639fn run_smtp_inline<C, T, E>(
640    stream: &mut StreamStd,
641    mut coroutine: C,
642) -> Result<T, SmtpClientStdError>
643where
644    C: SmtpCoroutine<Yield = SmtpYield, Return = Result<T, E>>,
645    SmtpClientStdError: From<E>,
646{
647    let mut buf = [0u8; READ_BUFFER_SIZE];
648    let mut arg: Option<&[u8]> = None;
649
650    loop {
651        match coroutine.resume(arg.take()) {
652            SmtpCoroutineState::Complete(Ok(out)) => return Ok(out),
653            SmtpCoroutineState::Complete(Err(err)) => return Err(err.into()),
654            SmtpCoroutineState::Yielded(SmtpYield::WantsRead) => {
655                let n = stream.read(&mut buf)?;
656                arg = Some(&buf[..n]);
657            }
658            SmtpCoroutineState::Yielded(SmtpYield::WantsWrite(bytes)) => {
659                stream.write_all(&bytes)?;
660            }
661        }
662    }
663}
664
665/// Marker for everything the client can run against; auto-implemented for any
666/// blocking `Read + Write + Send + 'static` impl. The `Send` supertrait flows
667/// the auto-trait through the `Box<dyn SmtpStream>` type erasure so
668/// `SmtpClientStd` can travel between threads.  [`as_any_mut`] lets specialized
669/// callers (e.g. byte-level proxies that need [`StreamStd::set_read_timeout`])
670/// downcast the boxed stream back to its concrete type.
671///
672/// [`as_any_mut`]: SmtpStream::as_any_mut
673/// [`StreamStd::set_read_timeout`]: pimalaya_stream::std::stream::StreamStd::set_read_timeout
674pub trait SmtpStream: Read + Write + Send + Any {
675    /// Downcasts the boxed stream back to its concrete type.
676    fn as_any_mut(&mut self) -> &mut dyn Any;
677}
678
679impl<T: Read + Write + Send + Any> SmtpStream for T {
680    fn as_any_mut(&mut self) -> &mut dyn Any {
681        self
682    }
683}