Skip to main content

wasm_smtp_tokio/
lib.rs

1//! Tokio + rustls transport adapter for [`wasm-smtp`].
2//!
3//! `wasm-smtp` is runtime-agnostic: it drives the SMTP state machine
4//! and parses replies, but knows nothing about sockets or TLS. This
5//! crate provides the concrete `Transport` implementation that most
6//! tokio-based servers — axum, actix, warp, hyper, plain tokio — need
7//! to connect to a real SMTP submission endpoint.
8//!
9//! # Cargo features
10//!
11//! Two pairs of mutually-exclusive features select the TLS stack:
12//!
13//! | Trust source     | Pick one                                |
14//! |------------------|-----------------------------------------|
15//! | `native-roots`   | OS trust store via `rustls-native-certs` (default) |
16//! | `webpki-roots`   | Bundled Mozilla root set                |
17//!
18//! | Crypto provider  | Pick one                                |
19//! |------------------|-----------------------------------------|
20//! | `aws-lc-rs`      | BoringSSL-derived, default, FIPS paths  |
21//! | `ring`           | Traditional rustls provider, faster to compile |
22//!
23//! Picking both members of a pair, or neither, is a configuration
24//! error: this crate fails to compile in either case (see the
25//! `compile_error!` block below) so the misconfiguration is caught at
26//! `cargo build` time rather than in production.
27//!
28//! # Quick start (implicit TLS, port 465)
29//!
30//! ```no_run
31//! use wasm_smtp::SmtpClient;
32//! use wasm_smtp_tokio::TokioTlsTransport;
33//!
34//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
35//! let transport = TokioTlsTransport::connect_implicit_tls(
36//!     "smtp.example.com",
37//!     465,
38//!     "smtp.example.com", // SNI / certificate hostname
39//! ).await?;
40//!
41//! let mut client = SmtpClient::connect(transport, "client.example.com").await?;
42//! client.login("user@example.com", "secret").await?;
43//! client.send_mail(
44//!     "user@example.com",
45//!     &["recipient@example.org"],
46//!     "Subject: hi\r\n\r\nhello\r\n",
47//! ).await?;
48//! client.quit().await?;
49//! # Ok(())
50//! # }
51//! ```
52//!
53//! # STARTTLS (port 587)
54//!
55//! For STARTTLS-on-587 endpoints, connect plaintext first and let
56//! `wasm-smtp` drive the STARTTLS upgrade:
57//!
58//! ```no_run
59//! use wasm_smtp::SmtpClient;
60//! use wasm_smtp_tokio::TokioPlainTransport;
61//!
62//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
63//! let transport = TokioPlainTransport::connect(
64//!     "smtp.example.com",
65//!     587,
66//!     "smtp.example.com",
67//! ).await?;
68//!
69//! // `connect_starttls` runs the EHLO + STARTTLS dance and asks the
70//! // transport to upgrade to TLS in place.
71//! let mut client = SmtpClient::connect_starttls(transport, "client.example.com").await?;
72//! client.login("user@example.com", "secret").await?;
73//! # Ok(())
74//! # }
75//! ```
76//!
77//! # Trust anchors
78//!
79//! Two cargo features control where the trust roots come from:
80//!
81//! - `native-roots` (default): use the system trust store via
82//!   [`rustls-native-certs`]. Best for desktop / server deployments
83//!   that already manage CA trust through the OS.
84//! - `webpki-roots`: use the bundled Mozilla root set via
85//!   [`webpki-roots`]. Best for minimal containers, distroless images,
86//!   or any environment without a system CA store.
87//!
88//! Pick exactly one. They are mutually exclusive at the API level —
89//! the connect helpers will fail to compile if neither is enabled.
90//!
91//! # Custom configuration
92//!
93//! For more control (custom root stores, ALPN, alternate SNI), use
94//! [`TokioTlsTransport::connect_with`] together with [`ConnectOptions`]:
95//!
96//! ```no_run
97//! use wasm_smtp_tokio::{TokioTlsTransport, ConnectOptions};
98//!
99//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
100//! let opts = ConnectOptions::new()
101//!     .with_server_name("alt-name.example.com");
102//!
103//! let transport = TokioTlsTransport::connect_with(
104//!     "smtp.example.com",
105//!     465,
106//!     opts,
107//! ).await?;
108//! # Ok(())
109//! # }
110//! ```
111//!
112//! # Security
113//!
114//! Certificate validation is **on by default and cannot be disabled
115//! through the public API**. There is intentionally no
116//! `dangerous_configuration`-style escape hatch on this crate. To
117//! talk to a server with a self-signed certificate (a development
118//! mail catcher, for instance), construct a custom root store
119//! containing the test CA and pass it through
120//! [`ConnectOptions::with_root_store`].
121//!
122//! [`wasm-smtp`]: https://docs.rs/wasm-smtp
123//! [`rustls-native-certs`]: https://docs.rs/rustls-native-certs
124//! [`webpki-roots`]: https://docs.rs/webpki-roots
125
126// ---- compile-time feature validation -------------------------------------
127//
128// `cargo` does not support mutually-exclusive features natively. We use
129// `compile_error!` to surface misconfigurations as a build error rather
130// than letting them slip through and produce a runtime panic from rustls's
131// `CryptoProvider::install_default()`.
132
133#[cfg(all(feature = "native-roots", feature = "webpki-roots"))]
134compile_error!(
135    "wasm-smtp-tokio: the `native-roots` and `webpki-roots` features are \
136     mutually exclusive. Pick exactly one. To use webpki-roots, set \
137     `default-features = false, features = [\"webpki-roots\", \"aws-lc-rs\"]` \
138     (or substitute `ring` for `aws-lc-rs`)."
139);
140
141#[cfg(not(any(feature = "native-roots", feature = "webpki-roots")))]
142compile_error!(
143    "wasm-smtp-tokio: a trust-anchor source is required. Enable exactly \
144     one of `native-roots` (default) or `webpki-roots`."
145);
146
147#[cfg(all(feature = "aws-lc-rs", feature = "ring"))]
148compile_error!(
149    "wasm-smtp-tokio: the `aws-lc-rs` and `ring` features are mutually \
150     exclusive. Pick exactly one. The default is `aws-lc-rs`; set \
151     `default-features = false` and add `ring` (plus a trust-anchor \
152     feature) to switch."
153);
154
155#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))]
156compile_error!(
157    "wasm-smtp-tokio: a rustls crypto provider is required. Enable \
158     exactly one of `aws-lc-rs` (default) or `ring`."
159);
160
161mod transport;
162
163#[cfg(test)]
164mod tests;
165
166pub use transport::{ConnectOptions, TokioPlainTransport, TokioTlsTransport};