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