Compare commits

..

52 Commits

Author SHA1 Message Date
Paolo Barbolini
716d7baac2 regression 2025-06-02 11:40:45 +02:00
Paolo Barbolini
659b0b50b1 fix 2025-06-02 11:14:11 +02:00
Paolo Barbolini
f332439000 clippy 2025-06-02 11:09:21 +02:00
Paolo Barbolini
335cdea3f9 Merge branch 'master' into better-tls-parameters-builder 2025-06-02 11:05:59 +02:00
Paolo Barbolini
d3d8e24824 feat: add rustls-platform-verifier support (#1081) 2025-06-02 10:43:17 +02:00
Paolo Barbolini
c4df9730aa refactor(smtp/pool): remove duplicate abort_concurrent implementation (#1092) 2025-05-24 16:37:54 +02:00
Paolo Barbolini
bfed19e6ad refactor(stub): always use std Mutex (#1091) 2025-05-24 14:34:09 +00:00
David Campbell
629967ac98 docs: use Mailbox::new rather than string parsing (#1090) 2025-05-24 16:21:15 +02:00
Paolo Barbolini
06e381ec9c Prepare v0.11.16 (#1089) 2025-05-12 11:16:14 +02:00
Paolo Barbolini
d9ce9a6e47 chore: deprecate ungated TLS types _when_ no TLS backend is enabled (#1084) 2025-05-11 09:36:47 +02:00
Paolo Barbolini
e892b55b6b build(deps): upgrade webpki-roots to v1 (#1088) 2025-05-06 12:37:39 +00:00
Paolo Barbolini
7642b2130e simpler stuff 2025-05-02 08:54:34 +02:00
Paolo Barbolini
5a3f189e50 happy clippy 2025-05-02 08:51:24 +02:00
Paolo Barbolini
31b8f297ec enum weirdness 2025-05-02 08:43:50 +02:00
Paolo Barbolini
3a6ab6f398 async-std again 2025-05-02 08:33:34 +02:00
Paolo Barbolini
7b8fc5a678 async-std 2025-05-02 08:30:37 +02:00
Paolo Barbolini
2b36935b1f done 2025-05-02 08:29:20 +02:00
Paolo Barbolini
d31490a2a9 Make this all internal only for now 2025-05-02 07:20:20 +02:00
Paolo Barbolini
b7482f0232 examples 2025-05-02 05:47:53 +02:00
Paolo Barbolini
abc8cdf789 examples 2025-05-02 05:35:54 +02:00
Paolo Barbolini
81b233def4 warnings 2025-05-02 05:30:30 +02:00
Paolo Barbolini
e644a6c2d3 #[allow(missing_copy_implementations)] 2025-05-02 05:27:40 +02:00
Paolo Barbolini
0385ca3b19 deprecations 2025-05-02 05:26:18 +02:00
Paolo Barbolini
d114f9caf3 fix 2025-05-02 05:23:25 +02:00
Paolo Barbolini
785307b091 fix async-std 2025-05-02 05:22:03 +02:00
Paolo Barbolini
f16cbeec51 fixme 2025-05-02 05:19:39 +02:00
Paolo Barbolini
5cbe9ba283 Have the old builder use the new one underneath 2025-05-02 05:17:53 +02:00
Paolo Barbolini
2f4e36ac61 wip 2025-05-01 21:23:31 +02:00
Paolo Barbolini
610b72e93b broken doc build 2025-05-01 20:37:51 +02:00
Paolo Barbolini
69b7c5500a wip 2025-05-01 20:29:43 +02:00
Paolo Barbolini
63c5fcccfc Start moving types over 2025-05-01 20:22:23 +02:00
Paolo Barbolini
b583aff36c Move things 2025-05-01 19:42:30 +02:00
Paolo Barbolini
512c5e3ce8 wip 2025-05-01 19:02:13 +02:00
Paolo Barbolini
771d212198 build: gate web-time behind cfg(target_arch = "wasm32") (#1086) 2025-05-01 18:32:26 +02:00
Paolo Barbolini
83ba93944d docs: add missing doc(cfg(...)) attributes (#1085) 2025-05-01 18:16:40 +02:00
Paolo Barbolini
de3ab006e2 fix: feature gate internal TransportBuilder::tls to avoid recursive call site (#1083) 2025-05-01 15:09:07 +02:00
Paolo Barbolini
9504b7f45c refactor: cleanup internal TlsParameters and (Async)NetworkStream config (#1082) 2025-05-01 14:00:56 +02:00
dependabot[bot]
c91b356a96 build(deps): bump tokio from 1.44.1 to 1.44.2 (#1080)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.44.1 to 1.44.2.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.44.1...tokio-1.44.2)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.44.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-08 06:57:33 +02:00
dependabot[bot]
118c1ad47f build(deps): bump openssl from 0.10.71 to 0.10.72 (#1079)
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.71 to 0.10.72.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.71...openssl-v0.10.72)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.72
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-06 11:49:28 +02:00
Paolo Barbolini
8bf4d3a9c1 style: fix clippy::io_other_error (#1078) 2025-04-03 10:28:06 +00:00
Paolo Barbolini
1fcff673ba fix: remove E: Clone bound from AsyncFileTransport Clone impl (#1075) 2025-04-03 08:05:39 +00:00
Paolo Barbolini
8c70c0cfb4 build(deps): upgrade semver compatible dependencies (#1076) 2025-04-03 07:54:06 +00:00
Paolo Barbolini
63d8d30088 fix: let cannot be used for global variables (#1077) 2025-04-03 09:50:06 +02:00
Paolo Barbolini
6c0be84817 Prepare v0.11.15 (#1070) 2025-03-10 17:26:17 +01:00
Paolo Barbolini
6059cb04d6 build(deps): upgrade email-encoding to v0.4 (#1069) 2025-03-09 07:22:20 +00:00
Paolo Barbolini
fdf0346556 style: fix rustdoc::broken_intra_doc_links (#1068) 2025-03-09 07:55:15 +01:00
Paolo Barbolini
0f9455715c build(deps): upgrade semver compatible locked dependencies (#1067) 2025-03-08 11:52:38 +00:00
Popax21
0b3a1ed278 feat: add controlled shutdown methods (#1045) 2025-03-08 12:43:05 +01:00
Paolo Barbolini
76bf68268f build(deps): bump minimum supported serde to v1.0.110 (#1064) 2025-03-04 13:06:17 +01:00
Paolo Barbolini
99a86c0fac build(deps): bump minimum supported rustls to v0.23.18 (#1063) 2025-03-04 12:55:43 +01:00
Paolo Barbolini
f0de9ef02c style: deny unreachable_pub lint (#1058) 2025-02-23 10:17:17 +01:00
Paolo Barbolini
b4ddcbdcfc build: bump MSRV to 1.74 (#1060) 2025-02-23 10:16:57 +01:00
32 changed files with 1905 additions and 809 deletions

View File

@@ -75,8 +75,8 @@ jobs:
rust: stable
- name: beta
rust: beta
- name: '1.71'
rust: '1.71'
- name: '1.74'
rust: '1.74'
steps:
- name: Checkout

View File

@@ -1,3 +1,67 @@
<a name="v0.11.16"></a>
### v0.11.16 (2025-05-12)
#### Features
* Always implement `Clone` for `AsyncFileTransport` ([#1075])
#### Changes
* `Tls`, `CertificateStore`, `TlsParameters`, `TlsParametersBuilder`, `Certificate` and `Identity`
are now marked as deprecated when no TLS backend is enabled. They will be properly feature gated
in lettre v0.12 ([#1084])
#### Misc
* Gate `web-time` behind `cfg(target_arch = "wasm32")]` ([#1086])
* Add missing `#[doc(cfg(...))]` attributes ([#1086])
* Upgrade `webpki-roots` to v1 ([#1088])
* Cleanup internal `TlsParameters` and `(Async)NetworkStream` structures ([#1082])
* Feature gate internal `TransportBuilder::tls` to avoid recursive call site warnings ([#1083])
* Fix workaround for embedding cargo script in rustdoc output ([#1077])
* Fix `clippy::io_other_error` warnings ([#1078])
* Upgrade semver compatible dependencies ([#1076], [#1079], [#1080])
[#1075]: https://github.com/lettre/lettre/pull/1075
[#1076]: https://github.com/lettre/lettre/pull/1076
[#1077]: https://github.com/lettre/lettre/pull/1077
[#1078]: https://github.com/lettre/lettre/pull/1078
[#1079]: https://github.com/lettre/lettre/pull/1079
[#1080]: https://github.com/lettre/lettre/pull/1080
[#1082]: https://github.com/lettre/lettre/pull/1082
[#1083]: https://github.com/lettre/lettre/pull/1083
[#1084]: https://github.com/lettre/lettre/pull/1084
[#1086]: https://github.com/lettre/lettre/pull/1086
[#1088]: https://github.com/lettre/lettre/pull/1088
<a name="v0.11.15"></a>
### v0.11.15 (2025-03-10)
#### Upgrade notes
* MSRV is now 1.74 ([#1060])
#### Features
* Add controlled shutdown methods ([#1045], [#1068])
#### Misc
* Deny `unreachable_pub` lint ([#1058])
* Bump minimum supported `rustls` ([#1063])
* Bump minimum supported `serde` ([#1064])
* Upgrade semver compatible dependencies ([#1067])
* Upgrade `email-encoding` to v0.4 ([#1069])
[#1045]: https://github.com/lettre/lettre/pull/1045
[#1058]: https://github.com/lettre/lettre/pull/1058
[#1060]: https://github.com/lettre/lettre/pull/1060
[#1063]: https://github.com/lettre/lettre/pull/1063
[#1064]: https://github.com/lettre/lettre/pull/1064
[#1067]: https://github.com/lettre/lettre/pull/1067
[#1068]: https://github.com/lettre/lettre/pull/1068
[#1069]: https://github.com/lettre/lettre/pull/1069
<a name="v0.11.14"></a>
### v0.11.14 (2025-02-23)

776
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package]
name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.11.14"
version = "0.11.16"
description = "Email client"
readme = "README.md"
homepage = "https://lettre.rs"
@@ -11,7 +11,7 @@ authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo
categories = ["email", "network-programming"]
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
edition = "2021"
rust-version = "1.71"
rust-version = "1.74"
[badges]
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
@@ -32,11 +32,11 @@ mime = { version = "0.3.4", optional = true }
fastrand = { version = "2.0", optional = true }
quoted_printable = { version = "0.5", optional = true }
base64 = { version = "0.22", optional = true }
email-encoding = { version = "0.3", optional = true }
email-encoding = { version = "0.4", optional = true }
# file transport
uuid = { version = "1", features = ["v4"], optional = true }
serde = { version = "1", features = ["derive"], optional = true }
serde = { version = "1.0.110", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
# smtp-transport
@@ -48,9 +48,10 @@ percent-encoding = { version = "2.3", optional = true }
## tls
native-tls = { version = "0.2.9", optional = true } # feature
rustls = { version = "0.23.5", default-features = false, features = ["logging", "std", "tls12"], optional = true }
rustls = { version = "0.23.18", default-features = false, features = ["logging", "std", "tls12"], optional = true }
rustls-platform-verifier = { version = "0.6.0", optional = true }
rustls-native-certs = { version = "0.8", optional = true }
webpki-roots = { version = "0.26", optional = true }
webpki-roots = { version = "1.0.0", optional = true }
boring = { version = "4", optional = true }
# async
@@ -73,6 +74,7 @@ sha2 = { version = "0.10", features = ["oid"], optional = true }
rsa = { version = "0.9", optional = true }
ed25519-dalek = { version = "2", optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
## web-time for wasm support
web-time = { version = "1.1.0", optional = true }

View File

@@ -28,8 +28,8 @@
</div>
<div align="center">
<a href="https://deps.rs/crate/lettre/0.11.14">
<img src="https://deps.rs/crate/lettre/0.11.14/status.svg"
<a href="https://deps.rs/crate/lettre/0.11.16">
<img src="https://deps.rs/crate/lettre/0.11.16/status.svg"
alt="dependency status" />
</a>
</div>
@@ -53,12 +53,12 @@ Lettre does not provide (for now):
## Supported Rust Versions
Lettre supports all Rust versions released in the last 6 months. At the time of writing
the minimum supported Rust version is 1.71, but this could change at any time either from
the minimum supported Rust version is 1.74, but this could change at any time either from
one of our dependencies bumping their MSRV or by a new patch release of lettre.
## Example
This library requires Rust 1.71 or newer.
This library requires Rust 1.74 or newer.
To use this library, add the following to your `Cargo.toml`:
```toml
@@ -73,9 +73,9 @@ use lettre::{Message, SmtpTransport, Transport};
fn main() {
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.from(Mailbox::new("NoBody".to_owned(), "nobody@domain.tld".parse().unwrap()))
.reply_to(Mailbox::new("Yuin".to_owned(), "yuin@domain.tld".parse().unwrap()))
.to(Mailbox::new("Hei".to_owned(), "hei@domain.tld".parse().unwrap()))
.subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!"))

View File

@@ -53,7 +53,7 @@ mod serde_forward_path {
}
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Address>, D::Error>
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Address>, D::Error>
where
D: serde::Deserializer<'de>,
{
@@ -163,6 +163,7 @@ impl Envelope {
}
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
impl TryFrom<&Headers> for Envelope {
type Error = Error;

View File

@@ -45,6 +45,7 @@ use crate::transport::smtp::Error;
#[async_trait]
pub trait Executor: Debug + Send + Sync + 'static + private::Sealed {
#[cfg(feature = "smtp-transport")]
#[allow(private_bounds)]
type Handle: SpawnHandle;
#[cfg(feature = "smtp-transport")]
type Sleep: Future<Output = ()> + Send + 'static;
@@ -82,8 +83,8 @@ pub trait Executor: Debug + Send + Sync + 'static + private::Sealed {
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
#[async_trait]
pub trait SpawnHandle: Debug + Send + Sync + 'static + private::Sealed {
async fn shutdown(self);
pub(crate) trait SpawnHandle: Debug + Send + Sync + 'static + private::Sealed {
async fn shutdown(&self);
}
/// Async [`Executor`] using `tokio` `1.x`
@@ -177,7 +178,7 @@ impl Executor for Tokio1Executor {
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
#[async_trait]
impl SpawnHandle for tokio1_crate::task::JoinHandle<()> {
async fn shutdown(self) {
async fn shutdown(&self) {
self.abort();
}
}
@@ -201,7 +202,7 @@ pub struct AsyncStd1Executor;
#[cfg(feature = "async-std1")]
impl Executor for AsyncStd1Executor {
#[cfg(feature = "smtp-transport")]
type Handle = async_std::task::JoinHandle<()>;
type Handle = futures_util::future::AbortHandle;
#[cfg(feature = "smtp-transport")]
type Sleep = BoxFuture<'static, ()>;
@@ -211,7 +212,9 @@ impl Executor for AsyncStd1Executor {
F: Future<Output = ()> + Send + 'static,
F::Output: Send + 'static,
{
async_std::task::spawn(fut)
let (handle, registration) = futures_util::future::AbortHandle::new_pair();
async_std::task::spawn(futures_util::future::Abortable::new(fut, registration));
handle
}
#[cfg(feature = "smtp-transport")]
@@ -272,9 +275,9 @@ impl Executor for AsyncStd1Executor {
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
#[async_trait]
impl SpawnHandle for async_std::task::JoinHandle<()> {
async fn shutdown(self) {
self.cancel().await;
impl SpawnHandle for futures_util::future::AbortHandle {
async fn shutdown(&self) {
self.abort();
}
}
@@ -291,5 +294,5 @@ mod private {
impl Sealed for tokio1_crate::task::JoinHandle<()> {}
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
impl Sealed for async_std::task::JoinHandle<()> {}
impl Sealed for futures_util::future::AbortHandle {}
}

View File

@@ -6,7 +6,7 @@
//! * Secure defaults
//! * Async support
//!
//! Lettre requires Rust 1.71 or newer.
//! Lettre requires Rust 1.74 or newer.
//!
//! ## Features
//!
@@ -93,17 +93,20 @@
//! When the `rustls` feature is enabled, one of the following verification backends
//! MUST also be enabled.
//!
//! * **rustls-native-certs**: verify TLS certificates using the platform's native certificate store (see [`rustls-native-certs`])
//! * **rustls-platform-verifier**: verify TLS certificate using the OS's native certificate store (see [`rustls-platform-verifier`])
//! * **rustls-native-certs**: verify TLS certificates using the platform's native certificate store (see [`rustls-native-certs`]) - when in doubt use `rustls-platform-verifier`
//! * **webpki-roots**: verify TLS certificates against Mozilla's root certificates (see [`webpki-roots`])
//!
//! For the `rustls-native-certs` backend to work correctly, the following packages
//! will need to be installed in order for the build stage and the compiled program
//! to run properly.
//! The following packages will need to be installed in order for the build
//! stage and the compiled program to run properly.
//!
//! | Distro | Build-time packages | Runtime packages |
//! | ------------ | -------------------------- | ---------------------------- |
//! | Debian | none | `ca-certificates` |
//! | Alpine Linux | none | `ca-certificates` |
//! | Verification backend | Distro | Build-time packages | Runtime packages |
//! | --------------------- | ------------ | -------------------------- | ---------------------------- |
//! | `rustls-platform-verifier` | Debian | none | `ca-certificates` |
//! | `rustls-platform-verifier` | Alpine Linux | none | `ca-certificates` |
//! | `rustls-native-certs` | Debian | none | `ca-certificates` |
//! | `rustls-native-certs` | Alpine Linux | none | `ca-certificates` |
//! | `webpki-roots` | any | none | none |
//!
//! ### Sendmail transport
//!
@@ -151,6 +154,7 @@
//! [AWS-LC]: https://github.com/aws/aws-lc
//! [`aws-lc-rs`]: https://crates.io/crates/aws-lc-rs
//! [`ring`]: https://crates.io/crates/ring
//! [`rustls-platform-verifier`]: https://crates.io/crates/rustls-platform-verifier
//! [`rustls-native-certs`]: https://crates.io/crates/rustls-native-certs
//! [`webpki-roots`]: https://crates.io/crates/webpki-roots
//! [Tokio 1.x]: https://docs.rs/tokio/1
@@ -158,11 +162,12 @@
//! [mime 0.3]: https://docs.rs/mime/0.3
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.14")]
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.16")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)]
#![deny(
unreachable_pub,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
@@ -207,12 +212,13 @@ mod compiletime_checks {
#[cfg(all(
feature = "rustls",
not(feature = "rustls-platform-verifier"),
not(feature = "rustls-native-certs"),
not(feature = "webpki-roots")
))]
compile_error!(
"feature `rustls` also requires either the `rustls-native-certs` or the `webpki-roots` feature to
be enabled"
"feature `rustls` also requires either the `rustls-platform-verifier`, the `rustls-native-certs`
or the `webpki-roots` feature to be enabled"
);
#[cfg(all(feature = "native-tls", feature = "boring-tls"))]

View File

@@ -41,14 +41,14 @@ fn quoted_pair() -> impl Parser<char, char, Error = Cheap<char>> {
// FWS = ([*WSP CRLF] 1*WSP) / ; Folding white space
// obs-FWS
pub fn fws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
pub(super) fn fws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
rfc2234::wsp()
.or_not()
.then_ignore(rfc2234::wsp().ignored().repeated())
}
// CFWS = *([FWS] comment) (([FWS] comment) / FWS)
pub fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
pub(super) fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
// TODO: comment are not currently supported, so for now a cfws is
// the same as a fws.
fws()
@@ -106,12 +106,12 @@ pub(super) fn atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
}
// dot-atom = [CFWS] dot-atom-text [CFWS]
pub fn dot_atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
pub(super) fn dot_atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
cfws().chain(dot_atom_text())
}
// dot-atom-text = 1*atext *("." 1*atext)
pub fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
pub(super) fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
atext().repeated().at_least(1).chain(
just('.')
.chain(atext().repeated().at_least(1))
@@ -204,7 +204,7 @@ pub(crate) fn mailbox_list(
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1
// addr-spec = local-part "@" domain
pub fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<char>> {
pub(super) fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<char>> {
local_part()
.collect()
.then_ignore(just('@'))
@@ -212,12 +212,12 @@ pub fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<char>> {
}
// local-part = dot-atom / quoted-string / obs-local-part
pub fn local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
pub(super) fn local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
choice((dot_atom(), quoted_string(), obs_local_part()))
}
// domain = dot-atom / domain-literal / obs-domain
pub fn domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
pub(super) fn domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
// NOTE: omitting domain-literal since it may never be used
choice((dot_atom(), obs_domain()))
}
@@ -240,11 +240,11 @@ fn obs_phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.4
// obs-local-part = word *("." word)
pub fn obs_local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
pub(super) fn obs_local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
word().chain(just('.').chain(word()).repeated().flatten())
}
// obs-domain = atom *("." atom)
pub fn obs_domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
pub(super) fn obs_domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
atom().chain(just('.').chain(atom()).repeated().flatten())
}

View File

@@ -1,6 +1,6 @@
use std::time::SystemTime;
#[cfg(feature = "web")]
#[cfg(all(feature = "web", target_arch = "wasm32"))]
pub(crate) fn now() -> SystemTime {
fn to_std_systemtime(time: web_time::SystemTime) -> std::time::SystemTime {
let duration = time
@@ -18,7 +18,7 @@ pub(crate) fn now() -> SystemTime {
to_std_systemtime(web_time::SystemTime::now())
}
#[cfg(not(feature = "web"))]
#[cfg(not(all(feature = "web", target_arch = "wasm32")))]
pub(crate) fn now() -> SystemTime {
// FIXME: change to #[expect(clippy::disallowed_methods, reason = "the `web` feature is disabled")]
#[allow(clippy::disallowed_methods)]

View File

@@ -34,6 +34,7 @@ impl Error {
/// Returns true if the error is an envelope serialization or deserialization error
#[cfg(feature = "file-transport-envelope")]
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
pub fn is_envelope(&self) -> bool {
matches!(self.inner.kind, Kind::Envelope)
}

View File

@@ -173,7 +173,7 @@ pub struct FileTransport {
}
/// Asynchronously writes the content and the envelope information to a file
#[derive(Debug, Clone)]
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
@@ -199,6 +199,7 @@ impl FileTransport {
/// Writes the email content in eml format and the envelope
/// in json format.
#[cfg(feature = "file-transport-envelope")]
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
pub fn with_envelope<P: AsRef<Path>>(path: P) -> FileTransport {
FileTransport {
path: PathBuf::from(path.as_ref()),
@@ -211,6 +212,7 @@ impl FileTransport {
///
/// Reads the envelope and the raw message content.
#[cfg(feature = "file-transport-envelope")]
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
pub fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
use std::fs;
@@ -249,6 +251,7 @@ where
/// Writes the email content in eml format and the envelope
/// in json format.
#[cfg(feature = "file-transport-envelope")]
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
pub fn with_envelope<P: AsRef<Path>>(path: P) -> Self {
Self {
inner: FileTransport::with_envelope(path),
@@ -260,6 +263,7 @@ where
///
/// Reads the envelope and the raw message content.
#[cfg(feature = "file-transport-envelope")]
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
pub async fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
let eml_file = self.inner.path.join(format!("{email_id}.eml"));
let eml = E::fs_read(&eml_file).await.map_err(error::io)?;
@@ -272,6 +276,16 @@ where
}
}
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
impl<E: Executor> Clone for AsyncFileTransport<E> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
marker_: PhantomData,
}
}
}
impl Transport for FileTransport {
type Ok = Id;
type Error = Error;

View File

@@ -140,6 +140,10 @@ pub trait Transport {
}
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
/// Shuts down the transport. Future calls to [`Self::send`] and
/// [`Self::send_raw`] might fail.
fn shutdown(&self) {}
}
/// Async Transport method for emails
@@ -166,4 +170,8 @@ pub trait AsyncTransport {
}
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
/// Shuts down the transport. Future calls to [`Self::send`] and
/// [`Self::send_raw`] might fail.
async fn shutdown(&self) {}
}

View File

@@ -79,6 +79,11 @@ impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
Ok(result)
}
async fn shutdown(&self) {
#[cfg(feature = "pool")]
self.inner.shutdown().await;
}
}
#[cfg(feature = "async-std1")]
@@ -97,6 +102,11 @@ impl AsyncTransport for AsyncSmtpTransport<AsyncStd1Executor> {
Ok(result)
}
async fn shutdown(&self) {
#[cfg(feature = "pool")]
self.inner.shutdown().await;
}
}
impl<E> AsyncSmtpTransport<E>
@@ -224,7 +234,7 @@ where
/// a proper URL encoder, like the following cargo script:
///
/// ```rust
/// # let _ = r#"
/// # const TOML: &str = r#"
/// #!/usr/bin/env cargo
///
/// //! ```cargo
@@ -460,7 +470,7 @@ impl AsyncSmtpTransportBuilder {
}
/// Build client
pub struct AsyncSmtpClient<E> {
pub(super) struct AsyncSmtpClient<E> {
info: SmtpInfo,
marker_: PhantomData<E>,
}
@@ -472,7 +482,7 @@ where
/// Creates a new connection directly usable to send emails
///
/// Handles encryption and authentication
pub async fn connection(&self) -> Result<AsyncSmtpConnection, Error> {
pub(super) async fn connection(&self) -> Result<AsyncSmtpConnection, Error> {
let mut conn = E::connect(
&self.info.server,
self.info.port,

View File

@@ -54,6 +54,7 @@ impl AsyncSmtpConnection {
///
/// Sends EHLO and parses server information
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub async fn connect_with_transport(
stream: Box<dyn AsyncTokioStream>,
hello_name: &ClientId,
@@ -94,6 +95,7 @@ impl AsyncSmtpConnection {
/// # }
/// ```
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub async fn connect_tokio1<T: tokio1_crate::net::ToSocketAddrs>(
server: T,
timeout: Option<Duration>,
@@ -112,6 +114,7 @@ impl AsyncSmtpConnection {
///
/// Sends EHLO and parses server information
#[cfg(feature = "async-std1")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
pub async fn connect_asyncstd1<T: async_std::net::ToSocketAddrs>(
server: T,
timeout: Option<Duration>,
@@ -376,6 +379,10 @@ impl AsyncSmtpConnection {
/// The X509 certificate of the server (DER encoded)
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate()
}
@@ -392,12 +399,14 @@ impl AsyncSmtpConnection {
/// as the TLSA records match the leaf or issuer certificates.
/// It cannot be called on non Boring TLS streams.
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn tls_verify_result(&self) -> Result<(), Error> {
self.stream.get_ref().tls_verify_result()
}
/// All the X509 certificates of the chain (DER encoded)
#[cfg(any(feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "boring-tls"))))]
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
self.stream.get_ref().certificate_chain()
}

View File

@@ -9,13 +9,11 @@ use std::{
#[cfg(feature = "async-std1")]
use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs};
use futures_io::{
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind,
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError,
Result as IoResult,
};
#[cfg(feature = "async-std1-rustls")]
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
#[cfg(any(feature = "tokio1-rustls", feature = "async-std1-rustls"))]
use rustls::pki_types::ServerName;
use futures_rustls::client::TlsStream as AsyncStd1RustlsStream;
#[cfg(feature = "tokio1-boring-tls")]
use tokio1_boring::SslStream as Tokio1SslStream;
#[cfg(feature = "tokio1")]
@@ -28,7 +26,7 @@ use tokio1_crate::net::{
#[cfg(feature = "tokio1-native-tls")]
use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream;
#[cfg(feature = "tokio1-rustls")]
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
use tokio1_rustls::client::TlsStream as Tokio1RustlsStream;
#[cfg(any(
feature = "tokio1-native-tls",
@@ -79,7 +77,7 @@ enum InnerAsyncNetworkStream {
Tokio1NativeTls(Tokio1TlsStream<Box<dyn AsyncTokioStream>>),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-rustls")]
Tokio1RustlsTls(Tokio1RustlsTlsStream<Box<dyn AsyncTokioStream>>),
Tokio1Rustls(Tokio1RustlsStream<Box<dyn AsyncTokioStream>>),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-boring-tls")]
Tokio1BoringTls(Tokio1SslStream<Box<dyn AsyncTokioStream>>),
@@ -88,7 +86,7 @@ enum InnerAsyncNetworkStream {
AsyncStd1Tcp(AsyncStd1TcpStream),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "async-std1-rustls")]
AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>),
AsyncStd1Rustls(AsyncStd1RustlsStream<AsyncStd1TcpStream>),
/// Can't be built
None,
}
@@ -113,17 +111,16 @@ impl AsyncNetworkStream {
s.get_ref().get_ref().get_ref().peer_addr()
}
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => s.get_ref().0.peer_addr(),
InnerAsyncNetworkStream::Tokio1Rustls(s) => s.get_ref().0.peer_addr(),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(s) => s.get_ref().peer_addr(),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => s.peer_addr(),
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => s.get_ref().0.peer_addr(),
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => s.get_ref().0.peer_addr(),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Err(IoError::new(
ErrorKind::Other,
Err(IoError::other(
"InnerAsyncNetworkStream::None must never be built",
))
}
@@ -131,11 +128,13 @@ impl AsyncNetworkStream {
}
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub fn use_existing_tokio1(stream: Box<dyn AsyncTokioStream>) -> AsyncNetworkStream {
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(stream))
}
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>(
server: T,
timeout: Option<Duration>,
@@ -202,6 +201,7 @@ impl AsyncNetworkStream {
}
#[cfg(feature = "async-std1")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
pub async fn connect_asyncstd1<T: AsyncStd1ToSocketAddrs>(
server: T,
timeout: Option<Duration>,
@@ -317,11 +317,9 @@ impl AsyncNetworkStream {
tcp_stream: Box<dyn AsyncTokioStream>,
tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = tls_parameters.domain().to_owned();
match tls_parameters.connector {
match tls_parameters.inner {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
InnerTlsParameters::NativeTls(inner) => {
#[cfg(not(feature = "tokio1-native-tls"))]
panic!("built without the tokio1-native-tls feature");
@@ -329,16 +327,16 @@ impl AsyncNetworkStream {
return {
use tokio1_native_tls_crate::TlsConnector;
let connector = TlsConnector::from(connector);
let connector = TlsConnector::from(inner.connector);
let stream = connector
.connect(&domain, tcp_stream)
.connect(&inner.server_name, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream))
};
}
#[cfg(feature = "rustls")]
InnerTlsParameters::RustlsTls(config) => {
InnerTlsParameters::Rustls(inner) => {
#[cfg(not(feature = "tokio1-rustls"))]
panic!("built without the tokio1-rustls feature");
@@ -346,28 +344,25 @@ impl AsyncNetworkStream {
return {
use tokio1_rustls::TlsConnector;
let domain = ServerName::try_from(domain.as_str())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connector = TlsConnector::from(config);
let connector = TlsConnector::from(inner.connector);
let stream = connector
.connect(domain.to_owned(), tcp_stream)
.connect(inner.server_name.inner(), tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
Ok(InnerAsyncNetworkStream::Tokio1Rustls(stream))
};
}
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(connector) => {
InnerTlsParameters::BoringTls(inner) => {
#[cfg(not(feature = "tokio1-boring-tls"))]
panic!("built without the tokio1-boring-tls feature");
#[cfg(feature = "tokio1-boring-tls")]
return {
let mut config = connector.configure().map_err(error::connection)?;
config.set_verify_hostname(tls_parameters.accept_invalid_hostnames);
let mut config = inner.connector.configure().map_err(error::connection)?;
config.set_verify_hostname(inner.extra_info.accept_invalid_hostnames);
let stream = tokio1_boring::connect(config, &domain, tcp_stream)
let stream = tokio1_boring::connect(config, &inner.server_name, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1BoringTls(stream))
@@ -380,17 +375,15 @@ impl AsyncNetworkStream {
#[cfg(feature = "async-std1-rustls")]
async fn upgrade_asyncstd1_tls(
tcp_stream: AsyncStd1TcpStream,
mut tls_parameters: TlsParameters,
tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = mem::take(&mut tls_parameters.domain);
match tls_parameters.connector {
match tls_parameters.inner {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
InnerTlsParameters::NativeTls(_) => {
panic!("native-tls isn't supported with async-std yet. See https://github.com/lettre/lettre/pull/531#issuecomment-757893531");
}
#[cfg(feature = "rustls")]
InnerTlsParameters::RustlsTls(config) => {
InnerTlsParameters::Rustls(inner) => {
#[cfg(not(feature = "async-std1-rustls"))]
panic!("built without the async-std1-rustls feature");
@@ -398,19 +391,16 @@ impl AsyncNetworkStream {
return {
use futures_rustls::TlsConnector;
let domain = ServerName::try_from(domain.as_str())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connector = TlsConnector::from(config);
let connector = TlsConnector::from(inner.connector);
let stream = connector
.connect(domain.to_owned(), tcp_stream)
.connect(inner.server_name.inner(), tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
Ok(InnerAsyncNetworkStream::AsyncStd1Rustls(stream))
};
}
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(connector) => {
InnerTlsParameters::BoringTls(_inner) => {
panic!("boring-tls isn't supported with async-std yet.");
}
}
@@ -423,18 +413,19 @@ impl AsyncNetworkStream {
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(_) => true,
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(_) => true,
InnerAsyncNetworkStream::Tokio1Rustls(_) => true,
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(_) => true,
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false,
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => true,
InnerAsyncNetworkStream::AsyncStd1Rustls(_) => true,
InnerAsyncNetworkStream::None => false,
}
}
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn tls_verify_result(&self) -> Result<(), Error> {
match &self.inner {
#[cfg(feature = "tokio1")]
@@ -444,7 +435,7 @@ impl AsyncNetworkStream {
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(_) => panic!("Unsupported"),
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(_) => panic!("Unsupported"),
InnerAsyncNetworkStream::Tokio1Rustls(_) => panic!("Unsupported"),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => {
stream.ssl().verify_result().map_err(error::tls)
@@ -454,10 +445,11 @@ impl AsyncNetworkStream {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => panic!("Unsupported"),
InnerAsyncNetworkStream::AsyncStd1Rustls(_) => panic!("Unsupported"),
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
match &self.inner {
#[cfg(feature = "tokio1")]
@@ -467,7 +459,7 @@ impl AsyncNetworkStream {
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(_) => panic!("Unsupported"),
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(stream) => Ok(stream
InnerAsyncNetworkStream::Tokio1Rustls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
@@ -488,7 +480,7 @@ impl AsyncNetworkStream {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream
InnerAsyncNetworkStream::AsyncStd1Rustls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
@@ -515,7 +507,7 @@ impl AsyncNetworkStream {
.to_der()
.map_err(error::tls)?),
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(stream) => Ok(stream
InnerAsyncNetworkStream::Tokio1Rustls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
@@ -535,7 +527,7 @@ impl AsyncNetworkStream {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream
InnerAsyncNetworkStream::AsyncStd1Rustls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
@@ -575,7 +567,7 @@ impl FuturesAsyncRead for AsyncNetworkStream {
}
}
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => {
InnerAsyncNetworkStream::Tokio1Rustls(s) => {
let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
@@ -595,7 +587,7 @@ impl FuturesAsyncRead for AsyncNetworkStream {
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_read(cx, buf),
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_read(cx, buf),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(0))
@@ -617,13 +609,13 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_write(cx, buf),
InnerAsyncNetworkStream::Tokio1Rustls(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_write(cx, buf),
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_write(cx, buf),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(0))
@@ -638,13 +630,13 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_flush(cx),
InnerAsyncNetworkStream::Tokio1Rustls(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_flush(cx),
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_flush(cx),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(()))
@@ -659,13 +651,13 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_shutdown(cx),
InnerAsyncNetworkStream::Tokio1Rustls(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_close(cx),
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_close(cx),
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_close(cx),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(()))

View File

@@ -300,6 +300,10 @@ impl SmtpConnection {
/// The X509 certificate of the server (DER encoded)
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate()
}
@@ -316,12 +320,14 @@ impl SmtpConnection {
/// as the TLSA records match the leaf or issuer certificates.
/// It cannot be called on non Boring TLS streams.
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn tls_verify_result(&self) -> Result<(), Error> {
self.stream.get_ref().tls_verify_result()
}
/// All the X509 certificates of the chain (DER encoded)
#[cfg(any(feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "boring-tls"))))]
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
self.stream.get_ref().certificate_chain()
}

View File

@@ -34,12 +34,14 @@ pub use self::async_net::AsyncNetworkStream;
pub use self::async_net::AsyncTokioStream;
use self::net::NetworkStream;
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
pub(super) use self::tls::InnerTlsParameters;
pub(super) use self::tls::current::InnerTlsParameters;
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
pub use self::tls::TlsVersion;
pub use self::tls::current::TlsVersion;
pub use self::{
connection::SmtpConnection,
tls::{Certificate, CertificateStore, Identity, Tls, TlsParameters, TlsParametersBuilder},
tls::current::{
Certificate, CertificateStore, Identity, Tls, TlsParameters, TlsParametersBuilder,
},
};
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
@@ -58,7 +60,7 @@ struct ClientCodec {
impl ClientCodec {
/// Creates a new client codec
pub fn new() -> Self {
pub(crate) fn new() -> Self {
Self {
status: CodecStatus::StartOfNewLine,
}

View File

@@ -12,7 +12,7 @@ use boring::ssl::SslStream;
#[cfg(feature = "native-tls")]
use native_tls::TlsStream;
#[cfg(feature = "rustls")]
use rustls::{pki_types::ServerName, ClientConnection, StreamOwned};
use rustls::{ClientConnection, StreamOwned};
use socket2::{Domain, Protocol, Type};
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
@@ -37,7 +37,7 @@ enum InnerNetworkStream {
NativeTls(TlsStream<TcpStream>),
/// Encrypted TCP stream
#[cfg(feature = "rustls")]
RustlsTls(StreamOwned<ClientConnection, TcpStream>),
Rustls(StreamOwned<ClientConnection, TcpStream>),
#[cfg(feature = "boring-tls")]
BoringTls(SslStream<TcpStream>),
/// Can't be built
@@ -60,7 +60,7 @@ impl NetworkStream {
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(s) => s.get_ref().peer_addr(),
#[cfg(feature = "rustls")]
InnerNetworkStream::RustlsTls(s) => s.get_ref().peer_addr(),
InnerNetworkStream::Rustls(s) => s.get_ref().peer_addr(),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.get_ref().peer_addr(),
InnerNetworkStream::None => {
@@ -80,7 +80,7 @@ impl NetworkStream {
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(s) => s.get_ref().shutdown(how),
#[cfg(feature = "rustls")]
InnerNetworkStream::RustlsTls(s) => s.get_ref().shutdown(how),
InnerNetworkStream::Rustls(s) => s.get_ref().shutdown(how),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.get_ref().shutdown(how),
InnerNetworkStream::None => {
@@ -172,30 +172,33 @@ impl NetworkStream {
tcp_stream: TcpStream,
tls_parameters: &TlsParameters,
) -> Result<InnerNetworkStream, Error> {
Ok(match &tls_parameters.connector {
Ok(match &tls_parameters.inner {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
let stream = connector
.connect(tls_parameters.domain(), tcp_stream)
InnerTlsParameters::NativeTls(inner) => {
let stream = inner
.connector
.connect(&inner.server_name, tcp_stream)
.map_err(error::connection)?;
InnerNetworkStream::NativeTls(stream)
}
#[cfg(feature = "rustls")]
InnerTlsParameters::RustlsTls(connector) => {
let domain = ServerName::try_from(tls_parameters.domain())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connection = ClientConnection::new(Arc::clone(connector), domain.to_owned())
.map_err(error::connection)?;
InnerTlsParameters::Rustls(inner) => {
let connection = ClientConnection::new(
Arc::clone(&inner.connector),
inner.server_name.inner_ref().clone(),
)
.map_err(error::connection)?;
let stream = StreamOwned::new(connection, tcp_stream);
InnerNetworkStream::RustlsTls(stream)
InnerNetworkStream::Rustls(stream)
}
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(connector) => {
let stream = connector
InnerTlsParameters::BoringTls(inner) => {
let stream = inner
.connector
.configure()
.map_err(error::connection)?
.verify_hostname(tls_parameters.accept_invalid_hostnames)
.connect(tls_parameters.domain(), tcp_stream)
.verify_hostname(inner.extra_info.accept_invalid_hostnames)
.connect(&inner.server_name, tcp_stream)
.map_err(error::connection)?;
InnerNetworkStream::BoringTls(stream)
}
@@ -208,7 +211,7 @@ impl NetworkStream {
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(_) => true,
#[cfg(feature = "rustls")]
InnerNetworkStream::RustlsTls(_) => true,
InnerNetworkStream::Rustls(_) => true,
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(_) => true,
InnerNetworkStream::None => {
@@ -219,13 +222,14 @@ impl NetworkStream {
}
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn tls_verify_result(&self) -> Result<(), Error> {
match &self.inner {
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(_) => panic!("Unsupported"),
#[cfg(feature = "rustls")]
InnerNetworkStream::RustlsTls(_) => panic!("Unsupported"),
InnerNetworkStream::Rustls(_) => panic!("Unsupported"),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => {
stream.ssl().verify_result().map_err(error::tls)
@@ -235,13 +239,14 @@ impl NetworkStream {
}
#[cfg(any(feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "boring-tls"))))]
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
match &self.inner {
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(_) => panic!("Unsupported"),
#[cfg(feature = "rustls")]
InnerNetworkStream::RustlsTls(stream) => Ok(stream
InnerNetworkStream::Rustls(stream) => Ok(stream
.conn
.peer_certificates()
.unwrap()
@@ -261,6 +266,10 @@ impl NetworkStream {
}
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
match &self.inner {
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
@@ -272,7 +281,7 @@ impl NetworkStream {
.to_der()
.map_err(error::tls)?),
#[cfg(feature = "rustls")]
InnerNetworkStream::RustlsTls(stream) => Ok(stream
InnerNetworkStream::Rustls(stream) => Ok(stream
.conn
.peer_certificates()
.unwrap()
@@ -296,7 +305,7 @@ impl NetworkStream {
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_read_timeout(duration),
#[cfg(feature = "rustls")]
InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_read_timeout(duration),
InnerNetworkStream::Rustls(stream) => stream.get_ref().set_read_timeout(duration),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_read_timeout(duration),
InnerNetworkStream::None => {
@@ -314,7 +323,7 @@ impl NetworkStream {
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_write_timeout(duration),
#[cfg(feature = "rustls")]
InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_write_timeout(duration),
InnerNetworkStream::Rustls(stream) => stream.get_ref().set_write_timeout(duration),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_write_timeout(duration),
InnerNetworkStream::None => {
@@ -332,7 +341,7 @@ impl Read for NetworkStream {
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(s) => s.read(buf),
#[cfg(feature = "rustls")]
InnerNetworkStream::RustlsTls(s) => s.read(buf),
InnerNetworkStream::Rustls(s) => s.read(buf),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.read(buf),
InnerNetworkStream::None => {
@@ -350,7 +359,7 @@ impl Write for NetworkStream {
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(s) => s.write(buf),
#[cfg(feature = "rustls")]
InnerNetworkStream::RustlsTls(s) => s.write(buf),
InnerNetworkStream::Rustls(s) => s.write(buf),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.write(buf),
InnerNetworkStream::None => {
@@ -366,7 +375,7 @@ impl Write for NetworkStream {
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(s) => s.flush(),
#[cfg(feature = "rustls")]
InnerNetworkStream::RustlsTls(s) => s.flush(),
InnerNetworkStream::Rustls(s) => s.flush(),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.flush(),
InnerNetworkStream::None => {

View File

@@ -0,0 +1,111 @@
use std::fmt::{self, Debug};
use boring::{
ssl::{SslConnector, SslMethod, SslVerifyMode, SslVersion},
x509::store::X509StoreBuilder,
};
use crate::transport::smtp::error::{self, Error};
pub(super) fn build_connector(
builder: super::TlsParametersBuilder<super::BoringTls>,
) -> Result<(Box<str>, SslConnector), Error> {
let mut tls_builder = SslConnector::builder(SslMethod::tls_client()).map_err(error::tls)?;
if builder.accept_invalid_certs {
tls_builder.set_verify(SslVerifyMode::NONE);
} else {
match builder.cert_store {
CertificateStore::System => {}
CertificateStore::None => {
// Replace the default store with an empty store.
tls_builder.set_cert_store(X509StoreBuilder::new().map_err(error::tls)?.build());
}
}
let cert_store = tls_builder.cert_store_mut();
for cert in builder.root_certs {
cert_store.add_cert(cert.0).map_err(error::tls)?;
}
}
if let Some(identity) = builder.identity {
tls_builder
.set_certificate(identity.chain.as_ref())
.map_err(error::tls)?;
tls_builder
.set_private_key(identity.key.as_ref())
.map_err(error::tls)?;
}
let min_tls_version = match builder.min_tls_version {
MinTlsVersion::Tlsv10 => SslVersion::TLS1,
MinTlsVersion::Tlsv11 => SslVersion::TLS1_1,
MinTlsVersion::Tlsv12 => SslVersion::TLS1_2,
MinTlsVersion::Tlsv13 => SslVersion::TLS1_3,
};
tls_builder
.set_min_proto_version(Some(min_tls_version))
.map_err(error::tls)?;
Ok((builder.server_name.into_boxed_str(), tls_builder.build()))
}
#[derive(Debug, Clone, Default)]
#[allow(missing_copy_implementations)]
#[non_exhaustive]
pub(super) enum CertificateStore {
#[default]
System,
None,
}
#[derive(Clone)]
pub(super) struct Certificate(pub(super) boring::x509::X509);
impl Certificate {
pub(super) fn from_pem(pem: &[u8]) -> Result<Self, Error> {
Ok(Self(boring::x509::X509::from_pem(pem).map_err(error::tls)?))
}
pub(super) fn from_der(der: &[u8]) -> Result<Self, Error> {
Ok(Self(boring::x509::X509::from_der(der).map_err(error::tls)?))
}
}
impl Debug for Certificate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Certificate").finish_non_exhaustive()
}
}
#[derive(Clone)]
pub(super) struct Identity {
pub(super) chain: boring::x509::X509,
pub(super) key: boring::pkey::PKey<boring::pkey::Private>,
}
impl Identity {
pub(super) fn from_pem(pem: &[u8], key: &[u8]) -> Result<Self, Error> {
let chain = boring::x509::X509::from_pem(pem).map_err(error::tls)?;
let key = boring::pkey::PKey::private_key_from_pem(key).map_err(error::tls)?;
Ok(Self { chain, key })
}
}
impl Debug for Identity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Identity").finish_non_exhaustive()
}
}
#[derive(Debug, Copy, Clone, Default)]
#[non_exhaustive]
pub(super) enum MinTlsVersion {
Tlsv10,
Tlsv11,
#[default]
Tlsv12,
Tlsv13,
}

View File

@@ -1,26 +1,9 @@
use std::fmt::{self, Debug};
#[cfg(feature = "rustls")]
use std::sync::Arc;
#[cfg(feature = "boring-tls")]
use boring::{
pkey::PKey,
ssl::{SslConnector, SslVersion},
x509::store::X509StoreBuilder,
};
#[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls")]
use rustls::{
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider},
pki_types::{self, pem::PemObject, CertificateDer, PrivateKeyDer, ServerName, UnixTime},
server::ParsedCertificate,
ClientConfig, DigitallySignedStruct, Error as TlsError, RootCertStore, SignatureScheme,
};
use super::TlsBackend;
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
use crate::transport::smtp::{error, Error};
use crate::transport::smtp::error;
use crate::transport::smtp::Error;
/// TLS protocol versions.
#[derive(Debug, Copy, Clone)]
@@ -65,6 +48,16 @@ pub enum TlsVersion {
/// connecting to a local server.
#[derive(Clone)]
#[allow(missing_copy_implementations)]
#[cfg_attr(
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
deprecated(
note = "starting from lettre v0.12 `Tls` won't be available when none of the TLS backends are enabled"
)
)]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub enum Tls {
/// Insecure (plaintext) connection only.
///
@@ -138,14 +131,25 @@ impl Debug for Tls {
/// Source for the base set of root certificates to trust.
#[allow(missing_copy_implementations)]
#[derive(Clone, Debug, Default)]
#[cfg_attr(
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
deprecated(
note = "starting from lettre v0.12 `CertificateStore` won't be available when none of the TLS backends are enabled"
)
)]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub enum CertificateStore {
/// Use the default for the TLS backend.
///
/// For native-tls, this will use the system certificate store on Windows, the keychain on
/// macOS, and OpenSSL directories on Linux (usually `/etc/ssl`).
///
/// For rustls, this will also use the system store if the `rustls-native-certs` feature is
/// enabled, or will fall back to `webpki-roots`.
/// For rustls, this will use the system certificate verifier if the `rustls-platform-verifier`
/// feature is enabled. If the `rustls-native-certs` feature is enabled, system certificate
/// store will be used. Otherwise, it will fall back to `webpki-roots`.
///
/// The boring-tls backend uses the same logic as OpenSSL on all platforms.
#[default]
@@ -161,16 +165,32 @@ pub enum CertificateStore {
/// Parameters to use for secure clients
#[derive(Clone)]
#[cfg_attr(
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
deprecated(
note = "starting from lettre v0.12 `TlsParameters` won't be available when none of the TLS backends are enabled"
)
)]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub struct TlsParameters {
pub(crate) connector: InnerTlsParameters,
/// The domain name which is expected in the TLS certificate from the server
pub(super) domain: String,
#[cfg(feature = "boring-tls")]
pub(super) accept_invalid_hostnames: bool,
pub(in crate::transport::smtp) inner: InnerTlsParameters,
}
/// Builder for `TlsParameters`
#[derive(Debug, Clone)]
#[cfg_attr(
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
deprecated(
note = "starting from lettre v0.12 `TlsParametersBuilder` won't be available when none of the TLS backends are enabled"
)
)]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub struct TlsParametersBuilder {
domain: String,
cert_store: CertificateStore,
@@ -221,6 +241,8 @@ impl TlsParametersBuilder {
/// Controls whether certificates with an invalid hostname are accepted
///
/// This option is silently disabled when using `rustls-platform-verifier`.
///
/// Defaults to `false`.
///
/// # Warning
@@ -244,6 +266,10 @@ impl TlsParametersBuilder {
///
/// Defaults to [`Tlsv12`][TlsVersion::Tlsv12].
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn set_min_tls_version(mut self, min_tls_version: TlsVersion) -> Self {
self.min_tls_version = min_tls_version;
self
@@ -290,30 +316,20 @@ impl TlsParametersBuilder {
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
pub fn build_native(self) -> Result<TlsParameters, Error> {
let mut tls_builder = TlsConnector::builder();
match self.cert_store {
CertificateStore::Default => {}
CertificateStore::None => {
tls_builder.disable_built_in_roots(true);
}
let cert_store = match self.cert_store {
CertificateStore::Default => super::native_tls::CertificateStore::System,
CertificateStore::None => super::native_tls::CertificateStore::None,
#[allow(unreachable_patterns)]
other => {
return Err(error::tls(format!(
"{other:?} is not supported in native tls"
)))
}
}
for cert in self.root_certs {
tls_builder.add_root_certificate(cert.native_tls);
}
tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames);
tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs);
};
let min_tls_version = match self.min_tls_version {
TlsVersion::Tlsv10 => Protocol::Tlsv10,
TlsVersion::Tlsv11 => Protocol::Tlsv11,
TlsVersion::Tlsv12 => Protocol::Tlsv12,
TlsVersion::Tlsv10 => super::native_tls::MinTlsVersion::Tlsv10,
TlsVersion::Tlsv11 => super::native_tls::MinTlsVersion::Tlsv11,
TlsVersion::Tlsv12 => super::native_tls::MinTlsVersion::Tlsv12,
TlsVersion::Tlsv13 => {
return Err(error::tls(
"min tls version Tlsv13 not supported in native tls",
@@ -321,182 +337,114 @@ impl TlsParametersBuilder {
}
};
tls_builder.min_protocol_version(Some(min_tls_version));
let mut builder = super::TlsParametersBuilder::<super::NativeTls>::new(self.domain)
.certificate_store(cert_store)
.dangerous_accept_invalid_certs(self.accept_invalid_certs)
.dangerous_accept_invalid_hostnames(self.accept_invalid_hostnames)
.min_tls_version(min_tls_version);
for cert in self.root_certs {
builder = builder.add_root_certificate(cert.native_tls);
}
if let Some(identity) = self.identity {
tls_builder.identity(identity.native_tls);
builder = builder.identify_with(identity.native_tls);
}
let connector = tls_builder.build().map_err(error::tls)?;
Ok(TlsParameters {
connector: InnerTlsParameters::NativeTls(connector),
domain: self.domain,
#[cfg(feature = "boring-tls")]
accept_invalid_hostnames: self.accept_invalid_hostnames,
})
builder
.build()
.map(super::NativeTls::__build_current_tls_parameters)
}
/// Creates a new `TlsParameters` using boring-tls with the provided configuration
///
/// Warning: this uses the certificate store passed via `certificate_store`
/// instead of the one configured in [`TlsParametersBuilder::certificate_store`].
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn build_boring(self) -> Result<TlsParameters, Error> {
use boring::ssl::{SslMethod, SslVerifyMode};
let mut tls_builder = SslConnector::builder(SslMethod::tls_client()).map_err(error::tls)?;
if self.accept_invalid_certs {
tls_builder.set_verify(SslVerifyMode::NONE);
} else {
match self.cert_store {
CertificateStore::Default => {}
CertificateStore::None => {
// Replace the default store with an empty store.
tls_builder
.set_cert_store(X509StoreBuilder::new().map_err(error::tls)?.build());
}
#[allow(unreachable_patterns)]
other => {
return Err(error::tls(format!(
"{other:?} is not supported in boring tls"
)))
}
let cert_store = match self.cert_store {
CertificateStore::Default => super::boring_tls::CertificateStore::System,
CertificateStore::None => super::boring_tls::CertificateStore::None,
#[allow(unreachable_patterns)]
other => {
return Err(error::tls(format!(
"{other:?} is not supported in native tls"
)))
}
let cert_store = tls_builder.cert_store_mut();
for cert in self.root_certs {
cert_store.add_cert(cert.boring_tls).map_err(error::tls)?;
}
}
if let Some(identity) = self.identity {
tls_builder
.set_certificate(identity.boring_tls.0.as_ref())
.map_err(error::tls)?;
tls_builder
.set_private_key(identity.boring_tls.1.as_ref())
.map_err(error::tls)?;
}
};
let min_tls_version = match self.min_tls_version {
TlsVersion::Tlsv10 => SslVersion::TLS1,
TlsVersion::Tlsv11 => SslVersion::TLS1_1,
TlsVersion::Tlsv12 => SslVersion::TLS1_2,
TlsVersion::Tlsv13 => SslVersion::TLS1_3,
TlsVersion::Tlsv10 => super::boring_tls::MinTlsVersion::Tlsv10,
TlsVersion::Tlsv11 => super::boring_tls::MinTlsVersion::Tlsv11,
TlsVersion::Tlsv12 => super::boring_tls::MinTlsVersion::Tlsv12,
TlsVersion::Tlsv13 => super::boring_tls::MinTlsVersion::Tlsv13,
};
tls_builder
.set_min_proto_version(Some(min_tls_version))
.map_err(error::tls)?;
let connector = tls_builder.build();
Ok(TlsParameters {
connector: InnerTlsParameters::BoringTls(connector),
domain: self.domain,
accept_invalid_hostnames: self.accept_invalid_hostnames,
})
let mut builder = super::TlsParametersBuilder::<super::BoringTls>::new(self.domain)
.certificate_store(cert_store)
.dangerous_accept_invalid_certs(self.accept_invalid_certs)
.dangerous_accept_invalid_hostnames(self.accept_invalid_hostnames)
.min_tls_version(min_tls_version);
for cert in self.root_certs {
builder = builder.add_root_certificate(cert.boring_tls);
}
if let Some(identity) = self.identity {
builder = builder.identify_with(identity.boring_tls);
}
builder
.build()
.map(super::BoringTls::__build_current_tls_parameters)
}
/// Creates a new `TlsParameters` using rustls with the provided configuration
#[cfg(feature = "rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
let just_version3 = &[&rustls::version::TLS13];
let supported_versions = match self.min_tls_version {
let cert_store = match self.cert_store {
CertificateStore::Default => super::rustls::CertificateStore::default(),
#[cfg(feature = "webpki-roots")]
CertificateStore::WebpkiRoots => super::rustls::CertificateStore::WebpkiRoots,
CertificateStore::None => super::rustls::CertificateStore::None,
};
let min_tls_version = match self.min_tls_version {
TlsVersion::Tlsv10 => {
return Err(error::tls("min tls version Tlsv10 not supported in rustls"))
}
TlsVersion::Tlsv11 => {
return Err(error::tls("min tls version Tlsv11 not supported in rustls"))
}
TlsVersion::Tlsv12 => rustls::ALL_VERSIONS,
TlsVersion::Tlsv13 => just_version3,
TlsVersion::Tlsv12 => super::rustls::MinTlsVersion::Tlsv12,
TlsVersion::Tlsv13 => super::rustls::MinTlsVersion::Tlsv13,
};
let crypto_provider = crate::rustls_crypto::crypto_provider();
let tls = ClientConfig::builder_with_provider(Arc::clone(&crypto_provider))
.with_protocol_versions(supported_versions)
.map_err(error::tls)?;
// Build TLS config
let mut root_cert_store = RootCertStore::empty();
#[cfg(feature = "rustls-native-certs")]
fn load_native_roots(store: &mut RootCertStore) {
let rustls_native_certs::CertificateResult { certs, errors, .. } =
rustls_native_certs::load_native_certs();
let errors_len = errors.len();
let (added, ignored) = store.add_parsable_certificates(certs);
#[cfg(feature = "tracing")]
tracing::debug!(
"loaded platform certs with {errors_len} failing to load, {added} valid and {ignored} ignored (invalid) certs"
);
#[cfg(not(feature = "tracing"))]
let _ = (errors_len, added, ignored);
}
#[cfg(all(feature = "rustls", feature = "webpki-roots"))]
fn load_webpki_roots(store: &mut RootCertStore) {
store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
}
match self.cert_store {
CertificateStore::Default => {
#[cfg(feature = "rustls-native-certs")]
load_native_roots(&mut root_cert_store);
#[cfg(all(not(feature = "rustls-native-certs"), feature = "webpki-roots"))]
load_webpki_roots(&mut root_cert_store);
}
#[cfg(all(feature = "rustls", feature = "webpki-roots"))]
CertificateStore::WebpkiRoots => {
load_webpki_roots(&mut root_cert_store);
}
CertificateStore::None => {}
}
let mut builder = super::TlsParametersBuilder::<super::Rustls>::new(self.domain)
.certificate_store(cert_store)
.dangerous_accept_invalid_certs(self.accept_invalid_certs)
.dangerous_accept_invalid_hostnames(self.accept_invalid_hostnames)
.min_tls_version(min_tls_version);
for cert in self.root_certs {
for rustls_cert in cert.rustls {
root_cert_store.add(rustls_cert).map_err(error::tls)?;
for cert in cert.rustls {
builder = builder.add_root_certificate(cert);
}
}
if let Some(identity) = self.identity {
builder = builder.identify_with(identity.rustls_tls);
}
let tls = if self.accept_invalid_certs || self.accept_invalid_hostnames {
let verifier = InvalidCertsVerifier {
ignore_invalid_hostnames: self.accept_invalid_hostnames,
ignore_invalid_certs: self.accept_invalid_certs,
roots: root_cert_store,
crypto_provider,
};
tls.dangerous()
.with_custom_certificate_verifier(Arc::new(verifier))
} else {
tls.with_root_certificates(root_cert_store)
};
let tls = if let Some(identity) = self.identity {
let (client_certificates, private_key) = identity.rustls_tls;
tls.with_client_auth_cert(client_certificates, private_key)
.map_err(error::tls)?
} else {
tls.with_no_client_auth()
};
Ok(TlsParameters {
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
domain: self.domain,
#[cfg(feature = "boring-tls")]
accept_invalid_hostnames: self.accept_invalid_hostnames,
})
builder
.build()
.map(super::Rustls::__build_current_tls_parameters)
}
}
#[derive(Clone)]
#[allow(clippy::enum_variant_names)]
pub enum InnerTlsParameters {
pub(in crate::transport::smtp) enum InnerTlsParameters {
#[cfg(feature = "native-tls")]
NativeTls(TlsConnector),
NativeTls(super::TlsParameters<super::NativeTls>),
#[cfg(feature = "rustls")]
RustlsTls(Arc<ClientConfig>),
Rustls(super::TlsParameters<super::Rustls>),
#[cfg(feature = "boring-tls")]
BoringTls(SslConnector),
BoringTls(super::TlsParameters<super::BoringTls>),
}
impl TlsParameters {
@@ -508,7 +456,7 @@ impl TlsParameters {
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn new(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build()
Self::new_with::<super::DefaultTlsBackend>(domain)
}
/// Creates a new `TlsParameters` builder
@@ -520,82 +468,86 @@ impl TlsParameters {
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
pub fn new_native(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build_native()
Self::new_with::<super::NativeTls>(domain)
}
/// Creates a new `TlsParameters` using rustls
#[cfg(feature = "rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
pub fn new_rustls(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build_rustls()
Self::new_with::<super::Rustls>(domain)
}
/// Creates a new `TlsParameters` using boring
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn new_boring(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build_boring()
Self::new_with::<super::BoringTls>(domain)
}
fn new_with<B: TlsBackend>(domain: String) -> Result<Self, Error> {
super::TlsParametersBuilder::<B>::new(domain)
.build()
.map(B::__build_current_tls_parameters)
}
pub fn domain(&self) -> &str {
&self.domain
match self.inner {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(ref inner) => &inner.server_name,
#[cfg(feature = "rustls")]
InnerTlsParameters::Rustls(ref inner) => inner.server_name.as_ref(),
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(ref inner) => &inner.server_name,
}
}
}
/// A certificate that can be used with [`TlsParametersBuilder::add_root_certificate`]
#[derive(Clone)]
#[allow(missing_copy_implementations)]
#[cfg_attr(
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
deprecated(
note = "starting from lettre v0.12 `Certificate` won't be available when none of the TLS backends are enabled"
)
)]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub struct Certificate {
#[cfg(feature = "native-tls")]
native_tls: native_tls::Certificate,
native_tls: super::native_tls::Certificate,
#[cfg(feature = "rustls")]
rustls: Vec<CertificateDer<'static>>,
rustls: Vec<super::rustls::Certificate>,
#[cfg(feature = "boring-tls")]
boring_tls: boring::x509::X509,
boring_tls: super::boring_tls::Certificate,
}
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
impl Certificate {
/// Create a `Certificate` from a DER encoded certificate
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
#[cfg(feature = "native-tls")]
let native_tls_cert = native_tls::Certificate::from_der(&der).map_err(error::tls)?;
#[cfg(feature = "boring-tls")]
let boring_tls_cert = boring::x509::X509::from_der(&der).map_err(error::tls)?;
Ok(Self {
#[cfg(feature = "native-tls")]
native_tls: native_tls_cert,
#[cfg(feature = "rustls")]
rustls: vec![der.into()],
native_tls: super::native_tls::Certificate::from_der(&der)?,
#[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert,
boring_tls: super::boring_tls::Certificate::from_der(&der)?,
#[cfg(feature = "rustls")]
rustls: vec![super::rustls::Certificate::from_der(der)],
})
}
/// Create a `Certificate` from a PEM encoded certificate
pub fn from_pem(pem: &[u8]) -> Result<Self, Error> {
#[cfg(feature = "native-tls")]
let native_tls_cert = native_tls::Certificate::from_pem(pem).map_err(error::tls)?;
#[cfg(feature = "boring-tls")]
let boring_tls_cert = boring::x509::X509::from_pem(pem).map_err(error::tls)?;
#[cfg(feature = "rustls")]
let rustls_cert = {
CertificateDer::pem_slice_iter(pem)
.collect::<Result<Vec<_>, pki_types::pem::Error>>()
.map_err(|_| error::tls("invalid certificates"))?
};
Ok(Self {
#[cfg(feature = "native-tls")]
native_tls: native_tls_cert,
native_tls: super::native_tls::Certificate::from_pem(pem)?,
#[cfg(feature = "rustls")]
rustls: rustls_cert,
rustls: super::rustls::Certificate::from_pem_bundle(pem)?,
#[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert,
boring_tls: super::boring_tls::Certificate::from_pem(pem)?,
})
}
}
@@ -607,14 +559,25 @@ impl Debug for Certificate {
}
/// An identity that can be used with [`TlsParametersBuilder::identify_with`]
#[derive(Clone)]
#[allow(missing_copy_implementations)]
#[cfg_attr(
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
deprecated(
note = "starting from lettre v0.12 `Identity` won't be available when none of the TLS backends are enabled"
)
)]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub struct Identity {
#[cfg(feature = "native-tls")]
native_tls: native_tls::Identity,
native_tls: super::native_tls::Identity,
#[cfg(feature = "rustls")]
rustls_tls: (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>),
rustls_tls: super::rustls::Identity,
#[cfg(feature = "boring-tls")]
boring_tls: (boring::x509::X509, PKey<boring::pkey::Private>),
boring_tls: super::boring_tls::Identity,
}
impl Debug for Identity {
@@ -623,132 +586,16 @@ impl Debug for Identity {
}
}
impl Clone for Identity {
fn clone(&self) -> Self {
Identity {
#[cfg(feature = "native-tls")]
native_tls: self.native_tls.clone(),
#[cfg(feature = "rustls")]
rustls_tls: (self.rustls_tls.0.clone(), self.rustls_tls.1.clone_key()),
#[cfg(feature = "boring-tls")]
boring_tls: (self.boring_tls.0.clone(), self.boring_tls.1.clone()),
}
}
}
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
impl Identity {
pub fn from_pem(pem: &[u8], key: &[u8]) -> Result<Self, Error> {
Ok(Self {
#[cfg(feature = "native-tls")]
native_tls: Identity::from_pem_native_tls(pem, key)?,
native_tls: super::native_tls::Identity::from_pem(pem, key)?,
#[cfg(feature = "rustls")]
rustls_tls: Identity::from_pem_rustls_tls(pem, key)?,
rustls_tls: super::rustls::Identity::from_pem(pem, key)?,
#[cfg(feature = "boring-tls")]
boring_tls: Identity::from_pem_boring_tls(pem, key)?,
boring_tls: super::boring_tls::Identity::from_pem(pem, key)?,
})
}
#[cfg(feature = "native-tls")]
fn from_pem_native_tls(pem: &[u8], key: &[u8]) -> Result<native_tls::Identity, Error> {
native_tls::Identity::from_pkcs8(pem, key).map_err(error::tls)
}
#[cfg(feature = "rustls")]
fn from_pem_rustls_tls(
pem: &[u8],
key: &[u8],
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), Error> {
let key = match PrivateKeyDer::from_pem_slice(key) {
Ok(key) => key,
Err(pki_types::pem::Error::NoItemsFound) => {
return Err(error::tls("no private key found"))
}
Err(err) => return Err(error::tls(err)),
};
Ok((vec![pem.to_owned().into()], key))
}
#[cfg(feature = "boring-tls")]
fn from_pem_boring_tls(
pem: &[u8],
key: &[u8],
) -> Result<(boring::x509::X509, PKey<boring::pkey::Private>), Error> {
let cert = boring::x509::X509::from_pem(pem).map_err(error::tls)?;
let key = boring::pkey::PKey::private_key_from_pem(key).map_err(error::tls)?;
Ok((cert, key))
}
}
#[cfg(feature = "rustls")]
#[derive(Debug)]
struct InvalidCertsVerifier {
ignore_invalid_hostnames: bool,
ignore_invalid_certs: bool,
roots: RootCertStore,
crypto_provider: Arc<CryptoProvider>,
}
#[cfg(feature = "rustls")]
impl ServerCertVerifier for InvalidCertsVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>],
server_name: &ServerName<'_>,
_ocsp_response: &[u8],
now: UnixTime,
) -> Result<ServerCertVerified, TlsError> {
let cert = ParsedCertificate::try_from(end_entity)?;
if !self.ignore_invalid_certs {
rustls::client::verify_server_cert_signed_by_trust_anchor(
&cert,
&self.roots,
intermediates,
now,
self.crypto_provider.signature_verification_algorithms.all,
)?;
}
if !self.ignore_invalid_hostnames {
rustls::client::verify_server_name(&cert, server_name)?;
}
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, TlsError> {
verify_tls12_signature(
message,
cert,
dss,
&self.crypto_provider.signature_verification_algorithms,
)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, TlsError> {
verify_tls13_signature(
message,
cert,
dss,
&self.crypto_provider.signature_verification_algorithms,
)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.crypto_provider
.signature_verification_algorithms
.supported_schemes()
}
}

View File

@@ -0,0 +1,253 @@
use crate::transport::smtp::Error;
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub(super) mod boring_tls;
pub(super) mod current;
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
pub(super) mod native_tls;
#[cfg(feature = "rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
pub(super) mod rustls;
#[derive(Debug)]
#[allow(private_bounds)]
pub(in crate::transport::smtp) struct TlsParameters<B: TlsBackend> {
pub(in crate::transport::smtp) server_name: B::ServerName,
pub(in crate::transport::smtp) connector: B::Connector,
pub(in crate::transport::smtp) extra_info: B::ExtraInfo,
}
impl<B: TlsBackend> Clone for TlsParameters<B> {
fn clone(&self) -> Self {
Self {
server_name: self.server_name.clone(),
connector: self.connector.clone(),
extra_info: self.extra_info.clone(),
}
}
}
#[derive(Debug)]
struct TlsParametersBuilder<B: TlsBackend> {
server_name: String,
cert_store: B::CertificateStore,
root_certs: Vec<B::Certificate>,
identity: Option<B::Identity>,
accept_invalid_certs: bool,
accept_invalid_hostnames: bool,
min_tls_version: B::MinTlsVersion,
}
impl<B: TlsBackend> TlsParametersBuilder<B> {
fn new(server_name: String) -> Self {
Self {
server_name,
cert_store: Default::default(),
root_certs: Vec::new(),
identity: None,
accept_invalid_certs: false,
accept_invalid_hostnames: false,
min_tls_version: Default::default(),
}
}
fn certificate_store(mut self, cert_store: B::CertificateStore) -> Self {
self.cert_store = cert_store;
self
}
fn add_root_certificate(mut self, cert: B::Certificate) -> Self {
self.root_certs.push(cert);
self
}
fn identify_with(mut self, identity: B::Identity) -> Self {
self.identity = Some(identity);
self
}
fn min_tls_version(mut self, min_tls_version: B::MinTlsVersion) -> Self {
self.min_tls_version = min_tls_version;
self
}
fn dangerous_accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
self.accept_invalid_certs = accept_invalid_certs;
self
}
fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
self.accept_invalid_hostnames = accept_invalid_hostnames;
self
}
fn build(self) -> Result<TlsParameters<B>, Error> {
B::__build_connector(self)
}
}
#[allow(private_bounds)]
trait TlsBackend: private::SealedTlsBackend {
type CertificateStore: Default;
type Certificate;
type Identity;
type MinTlsVersion: Default;
#[doc(hidden)]
fn __build_connector(builder: TlsParametersBuilder<Self>)
-> Result<TlsParameters<Self>, Error>;
#[doc(hidden)]
fn __build_current_tls_parameters(inner: TlsParameters<Self>) -> self::current::TlsParameters;
}
#[cfg(feature = "native-tls")]
type DefaultTlsBackend = NativeTls;
#[cfg(all(feature = "rustls", not(feature = "native-tls")))]
type DefaultTlsBackend = Rustls;
#[cfg(all(
feature = "boring-tls",
not(feature = "native-tls"),
not(feature = "rustls")
))]
type DefaultTlsBackend = BoringTls;
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
#[derive(Debug)]
#[allow(missing_copy_implementations)]
#[non_exhaustive]
pub(in crate::transport::smtp) struct NativeTls;
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
impl TlsBackend for NativeTls {
type CertificateStore = self::native_tls::CertificateStore;
type Certificate = self::native_tls::Certificate;
type Identity = self::native_tls::Identity;
type MinTlsVersion = self::native_tls::MinTlsVersion;
fn __build_connector(
builder: TlsParametersBuilder<Self>,
) -> Result<TlsParameters<Self>, Error> {
self::native_tls::build_connector(builder).map(|(server_name, connector)| TlsParameters {
server_name,
connector,
extra_info: (),
})
}
fn __build_current_tls_parameters(inner: TlsParameters<Self>) -> self::current::TlsParameters {
self::current::TlsParameters {
inner: self::current::InnerTlsParameters::NativeTls(inner),
}
}
}
#[cfg(feature = "rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
#[derive(Debug)]
#[allow(missing_copy_implementations)]
#[non_exhaustive]
pub(in crate::transport::smtp) struct Rustls;
#[cfg(feature = "rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
impl TlsBackend for Rustls {
type CertificateStore = self::rustls::CertificateStore;
type Certificate = self::rustls::Certificate;
type Identity = self::rustls::Identity;
type MinTlsVersion = self::rustls::MinTlsVersion;
fn __build_connector(
builder: TlsParametersBuilder<Self>,
) -> Result<TlsParameters<Self>, Error> {
self::rustls::build_connector(builder).map(|(server_name, connector)| TlsParameters {
server_name,
connector,
extra_info: (),
})
}
fn __build_current_tls_parameters(inner: TlsParameters<Self>) -> self::current::TlsParameters {
self::current::TlsParameters {
inner: self::current::InnerTlsParameters::Rustls(inner),
}
}
}
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
#[derive(Debug)]
#[allow(missing_copy_implementations)]
#[non_exhaustive]
pub(in crate::transport::smtp) struct BoringTls;
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
impl TlsBackend for BoringTls {
type CertificateStore = self::boring_tls::CertificateStore;
type Certificate = self::boring_tls::Certificate;
type Identity = self::boring_tls::Identity;
type MinTlsVersion = self::boring_tls::MinTlsVersion;
fn __build_connector(
builder: TlsParametersBuilder<Self>,
) -> Result<TlsParameters<Self>, Error> {
let accept_invalid_hostnames = builder.accept_invalid_hostnames;
self::boring_tls::build_connector(builder).map(|(server_name, connector)| TlsParameters {
server_name,
connector,
extra_info: BoringTlsExtraInfo {
accept_invalid_hostnames,
},
})
}
fn __build_current_tls_parameters(inner: TlsParameters<Self>) -> self::current::TlsParameters {
self::current::TlsParameters {
inner: self::current::InnerTlsParameters::BoringTls(inner),
}
}
}
#[cfg(feature = "boring-tls")]
#[derive(Debug, Clone)]
pub(in crate::transport::smtp) struct BoringTlsExtraInfo {
pub(super) accept_invalid_hostnames: bool,
}
mod private {
pub(in crate::transport::smtp) trait SealedTlsBackend:
Sized
{
type ServerName: Clone + AsRef<str>;
type Connector: Clone;
type ExtraInfo: Clone;
}
#[cfg(feature = "native-tls")]
impl SealedTlsBackend for super::NativeTls {
type ServerName = Box<str>;
type Connector = native_tls::TlsConnector;
type ExtraInfo = ();
}
#[cfg(feature = "rustls")]
impl SealedTlsBackend for super::Rustls {
type ServerName = super::rustls::ServerName;
type Connector = std::sync::Arc<rustls::client::ClientConfig>;
type ExtraInfo = ();
}
#[cfg(feature = "boring-tls")]
impl SealedTlsBackend for super::BoringTls {
type ServerName = Box<str>;
type Connector = boring::ssl::SslConnector;
type ExtraInfo = super::BoringTlsExtraInfo;
}
}

View File

@@ -0,0 +1,95 @@
use std::fmt::{self, Debug};
use native_tls::TlsConnector;
use crate::transport::smtp::error::{self, Error};
pub(super) fn build_connector(
builder: super::TlsParametersBuilder<super::NativeTls>,
) -> Result<(Box<str>, TlsConnector), Error> {
let mut tls_builder = TlsConnector::builder();
match builder.cert_store {
CertificateStore::System => {}
CertificateStore::None => {
tls_builder.disable_built_in_roots(true);
}
}
for cert in builder.root_certs {
tls_builder.add_root_certificate(cert.0);
}
tls_builder.danger_accept_invalid_hostnames(builder.accept_invalid_hostnames);
tls_builder.danger_accept_invalid_certs(builder.accept_invalid_certs);
let min_tls_version = match builder.min_tls_version {
MinTlsVersion::Tlsv10 => native_tls::Protocol::Tlsv10,
MinTlsVersion::Tlsv11 => native_tls::Protocol::Tlsv11,
MinTlsVersion::Tlsv12 => native_tls::Protocol::Tlsv12,
};
tls_builder.min_protocol_version(Some(min_tls_version));
if let Some(identity) = builder.identity {
tls_builder.identity(identity.0);
}
let connector = tls_builder.build().map_err(error::tls)?;
Ok((builder.server_name.into_boxed_str(), connector))
}
#[derive(Debug, Clone, Default)]
#[allow(missing_copy_implementations)]
#[non_exhaustive]
pub(super) enum CertificateStore {
#[default]
System,
None,
}
#[derive(Clone)]
pub(super) struct Certificate(pub(super) native_tls::Certificate);
impl Certificate {
pub(super) fn from_pem(pem: &[u8]) -> Result<Self, Error> {
Ok(Self(
native_tls::Certificate::from_pem(pem).map_err(error::tls)?,
))
}
pub(super) fn from_der(der: &[u8]) -> Result<Self, Error> {
Ok(Self(
native_tls::Certificate::from_der(der).map_err(error::tls)?,
))
}
}
impl Debug for Certificate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Certificate").finish_non_exhaustive()
}
}
#[derive(Clone)]
pub(super) struct Identity(pub(super) native_tls::Identity);
impl Identity {
pub(super) fn from_pem(pem: &[u8], key: &[u8]) -> Result<Self, Error> {
Ok(Self(
native_tls::Identity::from_pkcs8(pem, key).map_err(error::tls)?,
))
}
}
impl Debug for Identity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Identity").finish_non_exhaustive()
}
}
#[derive(Debug, Copy, Clone, Default)]
#[non_exhaustive]
pub(super) enum MinTlsVersion {
Tlsv10,
Tlsv11,
#[default]
Tlsv12,
}

View File

@@ -0,0 +1,329 @@
use std::{
fmt::{self, Debug},
sync::Arc,
};
use rustls::{
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider},
pki_types::{self, UnixTime},
server::ParsedCertificate,
ClientConfig, DigitallySignedStruct, RootCertStore, SignatureScheme,
};
use crate::transport::smtp::error::{self, Error};
pub(super) fn build_connector(
builder: super::TlsParametersBuilder<super::Rustls>,
) -> Result<(ServerName, Arc<ClientConfig>), Error> {
let just_version3 = &[&rustls::version::TLS13];
let supported_versions = match builder.min_tls_version {
MinTlsVersion::Tlsv12 => rustls::ALL_VERSIONS,
MinTlsVersion::Tlsv13 => just_version3,
};
let crypto_provider = crate::rustls_crypto::crypto_provider();
let tls = ClientConfig::builder_with_provider(Arc::clone(&crypto_provider))
.with_protocol_versions(supported_versions)
.map_err(error::tls)?;
// Build TLS config
let mut root_cert_store = RootCertStore::empty();
#[cfg(feature = "rustls-platform-verifier")]
let mut extra_roots = Vec::new();
match builder.cert_store {
#[cfg(feature = "rustls-platform-verifier")]
CertificateStore::PlatformVerifier => {
extra_roots = builder
.root_certs
.iter()
.map(|cert| cert.0.clone())
.collect();
}
#[cfg(feature = "rustls-native-certs")]
CertificateStore::NativeCerts => {
let rustls_native_certs::CertificateResult { certs, errors, .. } =
rustls_native_certs::load_native_certs();
let errors_len = errors.len();
let (added, ignored) = root_cert_store.add_parsable_certificates(certs);
#[cfg(feature = "tracing")]
tracing::debug!(
"loaded platform certs with {errors_len} failing to load, {added} valid and {ignored} ignored (invalid) certs"
);
#[cfg(not(feature = "tracing"))]
let _ = (errors_len, added, ignored);
}
#[cfg(feature = "webpki-roots")]
CertificateStore::WebpkiRoots => {
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
}
CertificateStore::None => {}
}
for cert in builder.root_certs {
root_cert_store.add(cert.0).map_err(error::tls)?;
}
let tls = match (
builder.cert_store,
builder.accept_invalid_certs,
builder.accept_invalid_hostnames,
) {
#[cfg(feature = "rustls-platform-verifier")]
(CertificateStore::PlatformVerifier, false, _) => {
tls.dangerous().with_custom_certificate_verifier(Arc::new(
rustls_platform_verifier::Verifier::new_with_extra_roots(
extra_roots,
crypto_provider,
)
.map_err(error::tls)?,
))
}
(_, true, _) | (_, _, true) => {
let verifier = InvalidCertsVerifier {
ignore_invalid_hostnames: builder.accept_invalid_hostnames,
ignore_invalid_certs: builder.accept_invalid_certs,
roots: root_cert_store,
crypto_provider,
};
tls.dangerous()
.with_custom_certificate_verifier(Arc::new(verifier))
}
_ => tls.with_root_certificates(root_cert_store),
};
let tls = if let Some(identity) = builder.identity {
tls.with_client_auth_cert(identity.chain, identity.key)
.map_err(error::tls)?
} else {
tls.with_no_client_auth()
};
let server_name = ServerName::try_from(builder.server_name)?;
Ok((server_name, Arc::new(tls)))
}
#[derive(Clone)]
pub(in crate::transport::smtp) struct ServerName {
val: pki_types::ServerName<'static>,
str_val: Box<str>,
}
impl ServerName {
#[allow(dead_code)]
pub(in crate::transport::smtp) fn inner(self) -> pki_types::ServerName<'static> {
self.val
}
pub(in crate::transport::smtp) fn inner_ref(&self) -> &pki_types::ServerName<'static> {
&self.val
}
fn try_from(value: String) -> Result<Self, crate::transport::smtp::Error> {
let val: pki_types::ServerName<'_> = value
.as_str()
.try_into()
.map_err(crate::transport::smtp::error::tls)?;
Ok(Self {
val: val.to_owned(),
str_val: value.into_boxed_str(),
})
}
}
impl AsRef<str> for ServerName {
fn as_ref(&self) -> &str {
&self.str_val
}
}
#[derive(Debug, Clone, Default)]
#[allow(dead_code, missing_copy_implementations)]
#[non_exhaustive]
pub(super) enum CertificateStore {
#[cfg(feature = "rustls-platform-verifier")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-platform-verifier")))]
#[cfg_attr(feature = "rustls-platform-verifier", default)]
PlatformVerifier,
#[cfg(feature = "rustls-native-certs")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-native-certs")))]
#[cfg_attr(
all(
not(feature = "rustls-platform-verifier"),
feature = "rustls-native-certs",
),
default
)]
NativeCerts,
#[cfg(feature = "webpki-roots")]
#[cfg_attr(docsrs, doc(cfg(feature = "webpki-roots")))]
#[cfg_attr(
all(
not(feature = "rustls-platform-verifier"),
not(feature = "rustls-native-certs"),
feature = "webpki-roots",
),
default
)]
WebpkiRoots,
#[cfg_attr(
all(
not(feature = "webpki-roots"),
not(feature = "rustls-platform-verifier"),
not(feature = "rustls-native-certs")
),
default
)]
None,
}
#[derive(Clone)]
pub(super) struct Certificate(pub(super) pki_types::CertificateDer<'static>);
impl Certificate {
#[allow(dead_code)]
pub(super) fn from_pem(pem: &[u8]) -> Result<Self, Error> {
use rustls::pki_types::pem::PemObject as _;
Ok(Self(
pki_types::CertificateDer::from_pem_slice(pem)
.map_err(|_| error::tls("invalid certificate"))?,
))
}
pub(super) fn from_pem_bundle(pem: &[u8]) -> Result<Vec<Self>, Error> {
use rustls::pki_types::pem::PemObject as _;
pki_types::CertificateDer::pem_slice_iter(pem)
.map(|cert| Ok(Self(cert?)))
.collect::<Result<Vec<_>, pki_types::pem::Error>>()
.map_err(|_| error::tls("invalid certificate"))
}
pub(super) fn from_der(der: Vec<u8>) -> Self {
Self(der.into())
}
}
impl Debug for Certificate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Certificate").finish_non_exhaustive()
}
}
pub(super) struct Identity {
pub(super) chain: Vec<pki_types::CertificateDer<'static>>,
pub(super) key: pki_types::PrivateKeyDer<'static>,
}
impl Identity {
pub(super) fn from_pem(pem: &[u8], key: &[u8]) -> Result<Self, Error> {
use rustls::pki_types::pem::PemObject as _;
let key = match pki_types::PrivateKeyDer::from_pem_slice(key) {
Ok(key) => key,
Err(pki_types::pem::Error::NoItemsFound) => {
return Err(error::tls("no private key found"))
}
Err(err) => return Err(error::tls(err)),
};
Ok(Self {
chain: vec![pem.to_owned().into()],
key,
})
}
}
impl Clone for Identity {
fn clone(&self) -> Self {
Self {
chain: self.chain.clone(),
key: self.key.clone_key(),
}
}
}
impl Debug for Identity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Identity").finish_non_exhaustive()
}
}
#[derive(Debug, Copy, Clone, Default)]
#[non_exhaustive]
pub(super) enum MinTlsVersion {
#[default]
Tlsv12,
Tlsv13,
}
#[derive(Debug)]
struct InvalidCertsVerifier {
ignore_invalid_hostnames: bool,
ignore_invalid_certs: bool,
roots: RootCertStore,
crypto_provider: Arc<CryptoProvider>,
}
impl ServerCertVerifier for InvalidCertsVerifier {
fn verify_server_cert(
&self,
end_entity: &pki_types::CertificateDer<'_>,
intermediates: &[pki_types::CertificateDer<'_>],
server_name: &pki_types::ServerName<'_>,
_ocsp_response: &[u8],
now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
let cert = ParsedCertificate::try_from(end_entity)?;
if !self.ignore_invalid_certs {
rustls::client::verify_server_cert_signed_by_trust_anchor(
&cert,
&self.roots,
intermediates,
now,
self.crypto_provider.signature_verification_algorithms.all,
)?;
}
if !self.ignore_invalid_hostnames {
rustls::client::verify_server_name(&cert, server_name)?;
}
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &pki_types::CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
verify_tls12_signature(
message,
cert,
dss,
&self.crypto_provider.signature_verification_algorithms,
)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &pki_types::CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
verify_tls13_signature(
message,
cert,
dss,
&self.crypto_provider.signature_verification_algorithms,
)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.crypto_provider
.signature_verification_algorithms
.supported_schemes()
}
}

View File

@@ -13,6 +13,7 @@ use super::{
pub(crate) trait TransportBuilder {
fn new<T: Into<String>>(server: T) -> Self;
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
fn tls(self, tls: super::Tls) -> Self;
fn port(self, port: u16) -> Self;
fn credentials(self, credentials: Credentials) -> Self;
@@ -24,6 +25,7 @@ impl TransportBuilder for SmtpTransportBuilder {
Self::new(server)
}
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
fn tls(self, tls: super::Tls) -> Self {
self.tls(tls)
}
@@ -47,6 +49,7 @@ impl TransportBuilder for AsyncSmtpTransportBuilder {
Self::new(server)
}
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
fn tls(self, tls: super::Tls) -> Self {
self.tls(tls)
}

View File

@@ -77,6 +77,11 @@ impl Error {
matches!(self.inner.kind, Kind::Tls)
}
/// Returns true if the error is because the transport was shut down
pub fn is_transport_shutdown(&self) -> bool {
matches!(self.inner.kind, Kind::TransportShutdown)
}
/// Returns the status code, if the error was generated from a response.
pub fn status(&self) -> Option<Code> {
match self.inner.kind {
@@ -111,6 +116,8 @@ pub(crate) enum Kind {
)]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
Tls,
/// Transport shutdown error
TransportShutdown,
}
impl fmt::Debug for Error {
@@ -136,6 +143,7 @@ impl fmt::Display for Error {
Kind::Connection => f.write_str("Connection error")?,
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
Kind::Tls => f.write_str("tls error")?,
Kind::TransportShutdown => f.write_str("transport has been shut down")?,
Kind::Transient(code) => {
write!(f, "transient error ({code})")?;
}
@@ -189,3 +197,7 @@ pub(crate) fn connection<E: Into<BoxError>>(e: E) -> Error {
pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Tls, Some(e))
}
pub(crate) fn transport_shutdown() -> Error {
Error::new::<BoxError>(Kind::TransportShutdown, None)
}

View File

@@ -1,6 +1,5 @@
use std::{
fmt::{self, Debug},
mem,
ops::{Deref, DerefMut},
sync::{Arc, OnceLock},
time::{Duration, Instant},
@@ -15,11 +14,15 @@ use super::{
super::{client::AsyncSmtpConnection, Error},
PoolConfig,
};
use crate::{executor::SpawnHandle, transport::smtp::async_transport::AsyncSmtpClient, Executor};
use crate::{
executor::SpawnHandle,
transport::smtp::{async_transport::AsyncSmtpClient, error},
Executor,
};
pub struct Pool<E: Executor> {
pub(crate) struct Pool<E: Executor> {
config: PoolConfig,
connections: Mutex<Vec<ParkedConnection>>,
connections: Mutex<Option<Vec<ParkedConnection>>>,
client: AsyncSmtpClient<E>,
handle: OnceLock<E::Handle>,
}
@@ -29,16 +32,16 @@ struct ParkedConnection {
since: Instant,
}
pub struct PooledConnection<E: Executor> {
pub(crate) struct PooledConnection<E: Executor> {
conn: Option<AsyncSmtpConnection>,
pool: Arc<Pool<E>>,
}
impl<E: Executor> Pool<E> {
pub fn new(config: PoolConfig, client: AsyncSmtpClient<E>) -> Arc<Self> {
pub(crate) fn new(config: PoolConfig, client: AsyncSmtpClient<E>) -> Arc<Self> {
let pool = Arc::new(Self {
config,
connections: Mutex::new(Vec::new()),
connections: Mutex::new(Some(Vec::new())),
client,
handle: OnceLock::new(),
});
@@ -60,6 +63,10 @@ impl<E: Executor> Pool<E> {
#[allow(clippy::needless_collect)]
let (count, dropped) = {
let mut connections = pool.connections.lock().await;
let Some(connections) = connections.as_mut() else {
// The transport was shut down
return;
};
let to_drop = connections
.iter()
@@ -92,6 +99,11 @@ impl<E: Executor> Pool<E> {
};
let mut connections = pool.connections.lock().await;
let Some(connections) = connections.as_mut() else {
// The transport was shut down
return;
};
connections.push(ParkedConnection::park(conn));
#[cfg(feature = "tracing")]
@@ -134,10 +146,25 @@ impl<E: Executor> Pool<E> {
pool
}
pub async fn connection(self: &Arc<Self>) -> Result<PooledConnection<E>, Error> {
pub(crate) async fn shutdown(&self) {
let connections = { self.connections.lock().await.take() };
if let Some(connections) = connections {
abort_concurrent(connections.into_iter().map(ParkedConnection::unpark)).await;
}
if let Some(handle) = self.handle.get() {
handle.shutdown().await;
}
}
pub(crate) async fn connection(self: &Arc<Self>) -> Result<PooledConnection<E>, Error> {
loop {
let conn = {
let mut connections = self.connections.lock().await;
let Some(connections) = connections.as_mut() else {
// The transport was shut down
return Err(error::transport_shutdown());
};
connections.pop()
};
@@ -181,13 +208,20 @@ impl<E: Executor> Pool<E> {
#[cfg(feature = "tracing")]
tracing::debug!("recycling connection");
let mut connections = self.connections.lock().await;
if connections.len() >= self.config.max_size as usize {
drop(connections);
conn.abort().await;
let mut connections_guard = self.connections.lock().await;
if let Some(connections) = connections_guard.as_mut() {
if connections.len() >= self.config.max_size as usize {
drop(connections_guard);
conn.abort().await;
} else {
let conn = ParkedConnection::park(conn);
connections.push(conn);
}
} else {
let conn = ParkedConnection::park(conn);
connections.push(conn);
// The pool has already been shut down
drop(connections_guard);
conn.abort().await;
}
}
}
@@ -200,7 +234,13 @@ impl<E: Executor> Debug for Pool<E> {
.field(
"connections",
&match self.connections.try_lock() {
Some(connections) => format!("{} connections", connections.len()),
Some(connections) => {
if let Some(connections) = connections.as_ref() {
format!("{} connections", connections.len())
} else {
"SHUT DOWN".to_owned()
}
}
None => "LOCKED".to_owned(),
},
@@ -222,14 +262,16 @@ impl<E: Executor> Drop for Pool<E> {
#[cfg(feature = "tracing")]
tracing::debug!("dropping Pool");
let connections = mem::take(self.connections.get_mut());
let connections = self.connections.get_mut().take();
let handle = self.handle.take();
E::spawn(async move {
if let Some(handle) = handle {
handle.shutdown().await;
}
abort_concurrent(connections.into_iter().map(ParkedConnection::unpark)).await;
if let Some(connections) = connections {
abort_concurrent(connections.into_iter().map(ParkedConnection::unpark)).await;
}
});
}
}

View File

@@ -1,8 +1,8 @@
use std::time::Duration;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub mod async_impl;
pub mod sync_impl;
pub(super) mod async_impl;
pub(super) mod sync_impl;
/// Configuration for a connection pool
#[derive(Debug, Clone)]

View File

@@ -1,8 +1,7 @@
use std::{
fmt::{self, Debug},
mem,
ops::{Deref, DerefMut},
sync::{Arc, Mutex, TryLockError},
sync::{mpsc, Arc, Mutex, TryLockError},
thread,
time::{Duration, Instant},
};
@@ -11,11 +10,12 @@ use super::{
super::{client::SmtpConnection, Error},
PoolConfig,
};
use crate::transport::smtp::transport::SmtpClient;
use crate::transport::smtp::{error, transport::SmtpClient};
pub struct Pool {
pub(crate) struct Pool {
config: PoolConfig,
connections: Mutex<Vec<ParkedConnection>>,
connections: Mutex<Option<Vec<ParkedConnection>>>,
thread_terminator: mpsc::SyncSender<()>,
client: SmtpClient,
}
@@ -24,16 +24,19 @@ struct ParkedConnection {
since: Instant,
}
pub struct PooledConnection {
pub(crate) struct PooledConnection {
conn: Option<SmtpConnection>,
pool: Arc<Pool>,
}
impl Pool {
pub fn new(config: PoolConfig, client: SmtpClient) -> Arc<Self> {
pub(crate) fn new(config: PoolConfig, client: SmtpClient) -> Arc<Self> {
let (thread_tx, thread_rx) = mpsc::sync_channel(1);
let pool = Arc::new(Self {
config,
connections: Mutex::new(Vec::new()),
connections: Mutex::new(Some(Vec::new())),
thread_terminator: thread_tx,
client,
});
@@ -54,6 +57,10 @@ impl Pool {
#[allow(clippy::needless_collect)]
let (count, dropped) = {
let mut connections = pool.connections.lock().unwrap();
let Some(connections) = connections.as_mut() else {
// The transport was shut down
return;
};
let to_drop = connections
.iter()
@@ -86,6 +93,11 @@ impl Pool {
};
let mut connections = pool.connections.lock().unwrap();
let Some(connections) = connections.as_mut() else {
// The transport was shut down
return;
};
connections.push(ParkedConnection::park(conn));
#[cfg(feature = "tracing")]
@@ -110,7 +122,14 @@ impl Pool {
}
drop(pool);
thread::sleep(idle_timeout);
match thread_rx.recv_timeout(idle_timeout) {
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => {
// The transport was shut down
return;
}
Err(mpsc::RecvTimeoutError::Timeout) => {}
}
}
})
.expect("couldn't spawn the Pool thread");
@@ -119,10 +138,25 @@ impl Pool {
pool
}
pub fn connection(self: &Arc<Self>) -> Result<PooledConnection, Error> {
pub(crate) fn shutdown(&self) {
let connections = { self.connections.lock().unwrap().take() };
if let Some(connections) = connections {
for conn in connections {
conn.unpark().abort();
}
}
_ = self.thread_terminator.try_send(());
}
pub(crate) fn connection(self: &Arc<Self>) -> Result<PooledConnection, Error> {
loop {
let conn = {
let mut connections = self.connections.lock().unwrap();
let Some(connections) = connections.as_mut() else {
// The transport was shut down
return Err(error::transport_shutdown());
};
connections.pop()
};
@@ -166,13 +200,20 @@ impl Pool {
#[cfg(feature = "tracing")]
tracing::debug!("recycling connection");
let mut connections = self.connections.lock().unwrap();
if connections.len() >= self.config.max_size as usize {
drop(connections);
conn.abort();
let mut connections_guard = self.connections.lock().unwrap();
if let Some(connections) = connections_guard.as_mut() {
if connections.len() >= self.config.max_size as usize {
drop(connections_guard);
conn.abort();
} else {
let conn = ParkedConnection::park(conn);
connections.push(conn);
}
} else {
let conn = ParkedConnection::park(conn);
connections.push(conn);
// The pool has already been shut down
drop(connections_guard);
conn.abort();
}
}
}
@@ -185,7 +226,13 @@ impl Debug for Pool {
.field(
"connections",
&match self.connections.try_lock() {
Ok(connections) => format!("{} connections", connections.len()),
Ok(connections) => {
if let Some(connections) = connections.as_ref() {
format!("{} connections", connections.len())
} else {
"SHUT DOWN".to_owned()
}
}
Err(TryLockError::WouldBlock) => "LOCKED".to_owned(),
Err(TryLockError::Poisoned(_)) => "POISONED".to_owned(),
@@ -201,10 +248,11 @@ impl Drop for Pool {
#[cfg(feature = "tracing")]
tracing::debug!("dropping Pool");
let connections = mem::take(&mut *self.connections.get_mut().unwrap());
for conn in connections {
let mut conn = conn.unpark();
conn.abort();
if let Some(connections) = self.connections.get_mut().unwrap().take() {
for conn in connections {
let mut conn = conn.unpark();
conn.abort();
}
}
}
}

View File

@@ -60,6 +60,11 @@ impl Transport for SmtpTransport {
Ok(result)
}
fn shutdown(&self) {
#[cfg(feature = "pool")]
self.inner.shutdown();
}
}
impl Debug for SmtpTransport {
@@ -172,7 +177,7 @@ impl SmtpTransport {
/// a proper URL encoder, like the following cargo script:
///
/// ```rust
/// # let _ = r#"
/// # const TOML: &str = r#"
/// #!/usr/bin/env cargo
///
/// //! ```cargo
@@ -369,7 +374,7 @@ impl SmtpTransportBuilder {
/// Build client
#[derive(Debug, Clone)]
pub struct SmtpClient {
pub(super) struct SmtpClient {
info: SmtpInfo,
}
@@ -377,7 +382,7 @@ impl SmtpClient {
/// Creates a new connection directly usable to send emails
///
/// Handles encryption and authentication
pub fn connection(&self) -> Result<SmtpConnection, Error> {
pub(super) fn connection(&self) -> Result<SmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match &self.info.tls {
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]

View File

@@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
/// Encode a string as xtext
#[derive(Debug)]
pub struct XText<'a>(pub &'a str);
pub(crate) struct XText<'a>(pub(crate) &'a str);
impl Display for XText<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {

View File

@@ -43,13 +43,11 @@
use std::{
error::Error as StdError,
fmt,
sync::{Arc, Mutex as StdMutex},
sync::{Arc, Mutex},
};
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
use async_trait::async_trait;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
use futures_util::lock::Mutex as FuturesMutex;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
use crate::AsyncTransport;
@@ -72,7 +70,7 @@ impl StdError for Error {}
#[derive(Debug, Clone)]
pub struct StubTransport {
response: Result<(), Error>,
message_log: Arc<StdMutex<Vec<(Envelope, String)>>>,
message_log: Arc<Mutex<Vec<(Envelope, String)>>>,
}
/// This transport logs messages and always returns the given response
@@ -81,7 +79,7 @@ pub struct StubTransport {
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
pub struct AsyncStubTransport {
response: Result<(), Error>,
message_log: Arc<FuturesMutex<Vec<(Envelope, String)>>>,
message_log: Arc<Mutex<Vec<(Envelope, String)>>>,
}
impl StubTransport {
@@ -89,7 +87,7 @@ impl StubTransport {
pub fn new(response: Result<(), Error>) -> Self {
Self {
response,
message_log: Arc::new(StdMutex::new(vec![])),
message_log: Arc::new(Mutex::new(vec![])),
}
}
@@ -97,7 +95,7 @@ impl StubTransport {
pub fn new_ok() -> Self {
Self {
response: Ok(()),
message_log: Arc::new(StdMutex::new(vec![])),
message_log: Arc::new(Mutex::new(vec![])),
}
}
@@ -105,7 +103,7 @@ impl StubTransport {
pub fn new_error() -> Self {
Self {
response: Err(Error),
message_log: Arc::new(StdMutex::new(vec![])),
message_log: Arc::new(Mutex::new(vec![])),
}
}
@@ -124,7 +122,7 @@ impl AsyncStubTransport {
pub fn new(response: Result<(), Error>) -> Self {
Self {
response,
message_log: Arc::new(FuturesMutex::new(vec![])),
message_log: Arc::new(Mutex::new(vec![])),
}
}
@@ -132,7 +130,7 @@ impl AsyncStubTransport {
pub fn new_ok() -> Self {
Self {
response: Ok(()),
message_log: Arc::new(FuturesMutex::new(vec![])),
message_log: Arc::new(Mutex::new(vec![])),
}
}
@@ -140,14 +138,14 @@ impl AsyncStubTransport {
pub fn new_error() -> Self {
Self {
response: Err(Error),
message_log: Arc::new(FuturesMutex::new(vec![])),
message_log: Arc::new(Mutex::new(vec![])),
}
}
/// Return all logged messages sent using [`AsyncTransport::send_raw`]
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub async fn messages(&self) -> Vec<(Envelope, String)> {
self.message_log.lock().await.clone()
self.message_log.lock().unwrap().clone()
}
}
@@ -173,7 +171,7 @@ impl AsyncTransport for AsyncStubTransport {
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
self.message_log
.lock()
.await
.unwrap()
.push((envelope.clone(), String::from_utf8_lossy(email).into()));
self.response
}