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}