1use 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#[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
170pub fn default_alpn() -> Vec<String> {
175 vec![String::from("smtp")]
176}
177
178pub struct SmtpClientStd {
180 pub stream: Box<dyn SmtpStream>,
181}
182
183impl SmtpClientStd {
184 pub fn new<S: Read + Write + Send + 'static>(stream: S) -> Self {
188 Self {
189 stream: Box::new(stream),
190 }
191 }
192
193 pub fn set_stream<S: Read + Write + Send + 'static>(&mut self, stream: S) {
196 self.stream = Box::new(stream);
197 }
198
199 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 pub fn greeting(&mut self) -> Result<Greeting<'static>, SmtpClientStdError> {
235 self.run(SmtpGreetingGet::new())
236 }
237
238 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 pub fn helo(&mut self, domain: Domain<'_>) -> Result<(), SmtpClientStdError> {
253 self.run(SmtpHelo::new(domain))
254 }
255
256 pub fn starttls(&mut self) -> Result<Vec<u8>, SmtpClientStdError> {
266 self.run(SmtpStartTls::new())
267 }
268
269 pub fn quit(&mut self) -> Result<(), SmtpClientStdError> {
271 self.run(SmtpQuit::new())
272 }
273
274 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 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 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 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 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 #[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 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 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 pub fn data(&mut self, message: Vec<u8>) -> Result<(), SmtpClientStdError> {
418 self.run(SmtpData::new(message))
419 }
420
421 pub fn rset(&mut self) -> Result<(), SmtpClientStdError> {
424 self.run(SmtpRset::new())
425 }
426
427 pub fn noop(&mut self) -> Result<(), SmtpClientStdError> {
429 self.run(SmtpNoop::new())
430 }
431
432 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 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 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#[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
624pub 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}