Compare commits
31 Commits
v0.11.14
...
chumsky-to
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e347cf9ae2 | ||
|
|
4c738085e5 | ||
|
|
b752170bd8 | ||
|
|
b073df7666 | ||
|
|
cf6b767a9c | ||
|
|
d3d8e24824 | ||
|
|
c4df9730aa | ||
|
|
bfed19e6ad | ||
|
|
629967ac98 | ||
|
|
06e381ec9c | ||
|
|
d9ce9a6e47 | ||
|
|
e892b55b6b | ||
|
|
771d212198 | ||
|
|
83ba93944d | ||
|
|
de3ab006e2 | ||
|
|
9504b7f45c | ||
|
|
c91b356a96 | ||
|
|
118c1ad47f | ||
|
|
8bf4d3a9c1 | ||
|
|
1fcff673ba | ||
|
|
8c70c0cfb4 | ||
|
|
63d8d30088 | ||
|
|
6c0be84817 | ||
|
|
6059cb04d6 | ||
|
|
fdf0346556 | ||
|
|
0f9455715c | ||
|
|
0b3a1ed278 | ||
|
|
76bf68268f | ||
|
|
99a86c0fac | ||
|
|
f0de9ef02c | ||
|
|
b4ddcbdcfc |
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
82
CHANGELOG.md
82
CHANGELOG.md
@@ -1,3 +1,85 @@
|
||||
<a name="v0.11.17"></a>
|
||||
### v0.11.17 (2025-06-06)
|
||||
|
||||
#### Features
|
||||
|
||||
* Add support for `rustls-platform-verifier` ([#1081])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Change readme example to use `Mailbox::new` instead of string parsing ([#1090])
|
||||
* Replace futures-util `Mutex` with std `Mutex` in `AsyncStubTransport` ([#1091])
|
||||
* Avoid duplicate `abort_concurrent` implementation ([#1092])
|
||||
|
||||
[#1081]: https://github.com/lettre/lettre/pull/1081
|
||||
[#1090]: https://github.com/lettre/lettre/pull/1090
|
||||
[#1091]: https://github.com/lettre/lettre/pull/1091
|
||||
[#1092]: https://github.com/lettre/lettre/pull/1092
|
||||
|
||||
<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)
|
||||
|
||||
|
||||
821
Cargo.lock
generated
821
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
@@ -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.17"
|
||||
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" }
|
||||
@@ -20,7 +20,7 @@ maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
email_address = { version = "0.2.1", default-features = false }
|
||||
chumsky = "0.9"
|
||||
nom = "8"
|
||||
idna = "1"
|
||||
|
||||
## tracing support
|
||||
@@ -32,25 +32,25 @@ 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
|
||||
nom = { version = "8", optional = true }
|
||||
hostname = { version = "0.4", optional = true } # feature
|
||||
socket2 = { version = "0.5.1", optional = true }
|
||||
socket2 = { version = "0.6", optional = true }
|
||||
url = { version = "2.4", optional = true }
|
||||
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 +73,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 }
|
||||
|
||||
@@ -105,7 +106,7 @@ mime03 = ["dep:mime"]
|
||||
file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
|
||||
file-transport-envelope = ["serde", "dep:serde_json", "file-transport"]
|
||||
sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
|
||||
smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:url", "dep:percent-encoding", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
|
||||
smtp-transport = ["dep:base64", "dep:socket2", "dep:url", "dep:percent-encoding", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
|
||||
|
||||
pool = ["dep:futures-util"]
|
||||
|
||||
|
||||
14
README.md
14
README.md
@@ -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.17">
|
||||
<img src="https://deps.rs/crate/lettre/0.11.17/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!"))
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
30
src/lib.rs
30
src/lib.rs
@@ -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.17")]
|
||||
#![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"))]
|
||||
|
||||
@@ -3,30 +3,34 @@
|
||||
//!
|
||||
//! [RFC2234]: https://datatracker.ietf.org/doc/html/rfc2234
|
||||
|
||||
use chumsky::{error::Cheap, prelude::*};
|
||||
use nom::{
|
||||
branch::alt,
|
||||
character::complete::{char, satisfy},
|
||||
IResult, Parser,
|
||||
};
|
||||
|
||||
// 6.1 Core Rules
|
||||
// https://datatracker.ietf.org/doc/html/rfc2234#section-6.1
|
||||
|
||||
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
|
||||
pub(super) fn alpha() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c: &char| c.is_ascii_alphabetic())
|
||||
pub(super) fn alpha(input: &str) -> IResult<&str, char> {
|
||||
satisfy(|c| c.is_ascii_alphabetic()).parse(input)
|
||||
}
|
||||
|
||||
// DIGIT = %x30-39
|
||||
// ; 0-9
|
||||
pub(super) fn digit() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c: &char| c.is_ascii_digit())
|
||||
pub(super) fn digit(input: &str) -> IResult<&str, char> {
|
||||
satisfy(|c| c.is_ascii_digit()).parse(input)
|
||||
}
|
||||
|
||||
// DQUOTE = %x22
|
||||
// ; " (Double Quote)
|
||||
pub(super) fn dquote() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
just('"')
|
||||
pub(super) fn dquote(input: &str) -> IResult<&str, char> {
|
||||
char('"').parse(input)
|
||||
}
|
||||
|
||||
// WSP = SP / HTAB
|
||||
// ; white space
|
||||
pub(super) fn wsp() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
choice((just(' '), just('\t')))
|
||||
pub(super) fn wsp(input: &str) -> IResult<&str, char> {
|
||||
alt((char(' '), char('\t'))).parse(input)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
//!
|
||||
//! [RFC2822]: https://datatracker.ietf.org/doc/html/rfc2822
|
||||
|
||||
use chumsky::{error::Cheap, prelude::*};
|
||||
use nom::{
|
||||
branch::alt,
|
||||
character::complete::{char, satisfy},
|
||||
combinator::{eof, map, opt},
|
||||
multi::{fold_many0, fold_many1, many0, many1, separated_list0},
|
||||
sequence::{delimited, pair, preceded, terminated},
|
||||
IResult, Parser,
|
||||
};
|
||||
|
||||
use super::{rfc2234, rfc5336};
|
||||
|
||||
@@ -15,8 +22,8 @@ use super::{rfc2234, rfc5336};
|
||||
// %d12 / ; carriage return, line feed,
|
||||
// %d14-31 / ; and white space characters
|
||||
// %d127
|
||||
fn no_ws_ctl() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c| matches!(u32::from(*c), 1..=8 | 11 | 12 | 14..=31 | 127))
|
||||
fn no_ws_ctl(input: &str) -> IResult<&str, char> {
|
||||
satisfy(|c| matches!(u32::from(c), 1..=8 | 11 | 12 | 14..=31 | 127)).parse(input)
|
||||
}
|
||||
|
||||
// text = %d1-9 / ; Characters excluding CR and LF
|
||||
@@ -24,16 +31,16 @@ fn no_ws_ctl() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
// %d12 /
|
||||
// %d14-127 /
|
||||
// obs-text
|
||||
fn text() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c| matches!(u32::from(*c), 1..=9 | 11 | 12 | 14..=127))
|
||||
fn text(input: &str) -> IResult<&str, char> {
|
||||
satisfy(|c| matches!(u32::from(c), 1..=9 | 11 | 12 | 14..=127)).parse(input)
|
||||
}
|
||||
|
||||
// 3.2.2. Quoted characters
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
|
||||
|
||||
// quoted-pair = ("\" text) / obs-qp
|
||||
fn quoted_pair() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
just('\\').ignore_then(text())
|
||||
fn quoted_pair(input: &str) -> IResult<&str, char> {
|
||||
preceded(char('\\'), text).parse(input)
|
||||
}
|
||||
|
||||
// 3.2.3. Folding white space and comments
|
||||
@@ -41,17 +48,19 @@ 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>> {
|
||||
rfc2234::wsp()
|
||||
.or_not()
|
||||
.then_ignore(rfc2234::wsp().ignored().repeated())
|
||||
pub(super) fn fws(input: &str) -> IResult<&str, Option<char>> {
|
||||
map(
|
||||
pair(opt(rfc2234::wsp), many0(rfc2234::wsp)),
|
||||
|(first, _rest)| first,
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
// CFWS = *([FWS] comment) (([FWS] comment) / FWS)
|
||||
pub fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
|
||||
pub(super) fn cfws(input: &str) -> IResult<&str, Option<char>> {
|
||||
// TODO: comment are not currently supported, so for now a cfws is
|
||||
// the same as a fws.
|
||||
fws()
|
||||
fws(input)
|
||||
}
|
||||
|
||||
// 3.2.4. Atom
|
||||
@@ -68,13 +77,13 @@ pub fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
|
||||
// "`" / "{" /
|
||||
// "|" / "}" /
|
||||
// "~"
|
||||
pub(super) fn atext() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
choice((
|
||||
rfc2234::alpha(),
|
||||
rfc2234::digit(),
|
||||
filter(|c| {
|
||||
pub(super) fn atext(input: &str) -> IResult<&str, char> {
|
||||
alt((
|
||||
rfc2234::alpha,
|
||||
rfc2234::digit,
|
||||
satisfy(|c| {
|
||||
matches!(
|
||||
*c,
|
||||
c,
|
||||
'!' | '#'
|
||||
| '$'
|
||||
| '%'
|
||||
@@ -96,29 +105,64 @@ pub(super) fn atext() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
)
|
||||
}),
|
||||
// also allow non ASCII UTF8 chars
|
||||
rfc5336::utf8_non_ascii(),
|
||||
rfc5336::utf8_non_ascii,
|
||||
))
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
// atom = [CFWS] 1*atext [CFWS]
|
||||
pub(super) fn atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
cfws().chain(atext().repeated().at_least(1))
|
||||
pub(super) fn atom(input: &str) -> IResult<&str, String> {
|
||||
map(
|
||||
pair(
|
||||
cfws,
|
||||
fold_many1(atext, String::new, |mut acc, c| {
|
||||
acc.push(c);
|
||||
acc
|
||||
}),
|
||||
),
|
||||
|(cfws, mut chars)| {
|
||||
if let Some(cfws) = cfws {
|
||||
chars.insert(0, cfws);
|
||||
}
|
||||
chars
|
||||
},
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
// dot-atom = [CFWS] dot-atom-text [CFWS]
|
||||
pub fn dot_atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
cfws().chain(dot_atom_text())
|
||||
pub(super) fn dot_atom(input: &str) -> IResult<&str, String> {
|
||||
map(pair(cfws, dot_atom_text), |(_cfws, text)| text).parse(input)
|
||||
}
|
||||
|
||||
// dot-atom-text = 1*atext *("." 1*atext)
|
||||
pub 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))
|
||||
.repeated()
|
||||
.at_least(1)
|
||||
.flatten(),
|
||||
pub(super) fn dot_atom_text(input: &str) -> IResult<&str, String> {
|
||||
map(
|
||||
pair(
|
||||
fold_many1(atext, String::new, |mut acc, c| {
|
||||
acc.push(c);
|
||||
acc
|
||||
}),
|
||||
many0(map(
|
||||
pair(
|
||||
char('.'),
|
||||
fold_many1(atext, String::new, |mut acc, c| {
|
||||
acc.push(c);
|
||||
acc
|
||||
}),
|
||||
),
|
||||
|(dot, chars)| format!("{dot}{chars}"),
|
||||
)),
|
||||
),
|
||||
|(first, rest)| {
|
||||
let mut result = first;
|
||||
for part in rest {
|
||||
result.push_str(&part);
|
||||
}
|
||||
result
|
||||
},
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
// 3.2.5. Quoted strings
|
||||
@@ -129,122 +173,168 @@ pub fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
// %d33 / ; The rest of the US-ASCII
|
||||
// %d35-91 / ; characters not including "\"
|
||||
// %d93-126 ; or the quote character
|
||||
fn qtext() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
choice((
|
||||
filter(|c| matches!(u32::from(*c), 33 | 35..=91 | 93..=126)),
|
||||
no_ws_ctl(),
|
||||
fn qtext(input: &str) -> IResult<&str, char> {
|
||||
alt((
|
||||
satisfy(|c| matches!(u32::from(c), 33 | 35..=91 | 93..=126)),
|
||||
no_ws_ctl,
|
||||
))
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
// qcontent = qtext / quoted-pair
|
||||
pub(super) fn qcontent() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
choice((qtext(), quoted_pair(), rfc5336::utf8_non_ascii()))
|
||||
pub(super) fn qcontent(input: &str) -> IResult<&str, char> {
|
||||
alt((qtext, quoted_pair, rfc5336::utf8_non_ascii)).parse(input)
|
||||
}
|
||||
|
||||
// quoted-string = [CFWS]
|
||||
// DQUOTE *([FWS] qcontent) [FWS] DQUOTE
|
||||
// [CFWS]
|
||||
fn quoted_string() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
rfc2234::dquote()
|
||||
.ignore_then(fws().chain(qcontent()).repeated().flatten())
|
||||
.then_ignore(text::whitespace())
|
||||
.then_ignore(rfc2234::dquote())
|
||||
fn quoted_string(input: &str) -> IResult<&str, String> {
|
||||
map(
|
||||
delimited(
|
||||
rfc2234::dquote,
|
||||
fold_many0(
|
||||
map(pair(fws, qcontent), |(_fws, c)| c),
|
||||
String::new,
|
||||
|mut acc, c| {
|
||||
acc.push(c);
|
||||
acc
|
||||
},
|
||||
),
|
||||
preceded(many0(satisfy(char::is_whitespace)), rfc2234::dquote),
|
||||
),
|
||||
|s| s,
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
// 3.2.6. Miscellaneous tokens
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.6
|
||||
|
||||
// word = atom / quoted-string
|
||||
fn word() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
choice((quoted_string(), atom()))
|
||||
fn word(input: &str) -> IResult<&str, String> {
|
||||
alt((quoted_string, atom)).parse(input)
|
||||
}
|
||||
|
||||
// phrase = 1*word / obs-phrase
|
||||
fn phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
choice((obs_phrase(), word().repeated().at_least(1).flatten()))
|
||||
fn phrase(input: &str) -> IResult<&str, String> {
|
||||
alt((obs_phrase, map(many1(word), |words| words.join(" ")))).parse(input)
|
||||
}
|
||||
|
||||
// 3.4. Address Specification
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
|
||||
|
||||
// mailbox = name-addr / addr-spec
|
||||
pub(crate) fn mailbox() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>>
|
||||
{
|
||||
choice((name_addr(), addr_spec().map(|addr| (None, addr))))
|
||||
.padded()
|
||||
.then_ignore(end())
|
||||
pub(crate) fn mailbox(input: &str) -> IResult<&str, (Option<String>, (String, String))> {
|
||||
terminated(alt((name_addr, map(addr_spec, |addr| (None, addr)))), eof).parse(input)
|
||||
}
|
||||
|
||||
// name-addr = [display-name] angle-addr
|
||||
fn name_addr() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>> {
|
||||
display_name().collect().or_not().then(angle_addr())
|
||||
fn name_addr(input: &str) -> IResult<&str, (Option<String>, (String, String))> {
|
||||
pair(opt(display_name), angle_addr).parse(input)
|
||||
}
|
||||
|
||||
// angle-addr = [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr
|
||||
fn angle_addr() -> impl Parser<char, (String, String), Error = Cheap<char>> {
|
||||
addr_spec()
|
||||
.delimited_by(just('<').ignored(), just('>').ignored())
|
||||
.padded()
|
||||
fn angle_addr(input: &str) -> IResult<&str, (String, String)> {
|
||||
delimited((cfws, char('<')), addr_spec, (char('>'), cfws)).parse(input)
|
||||
}
|
||||
|
||||
// display-name = phrase
|
||||
fn display_name() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
phrase()
|
||||
fn display_name(input: &str) -> IResult<&str, String> {
|
||||
phrase(input)
|
||||
}
|
||||
|
||||
// mailbox-list = (mailbox *("," mailbox)) / obs-mbox-list
|
||||
pub(crate) fn mailbox_list(
|
||||
) -> impl Parser<char, Vec<(Option<String>, (String, String))>, Error = Cheap<char>> {
|
||||
choice((name_addr(), addr_spec().map(|addr| (None, addr))))
|
||||
.separated_by(just(',').padded())
|
||||
.then_ignore(end())
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) fn mailbox_list(input: &str) -> IResult<&str, Vec<(Option<String>, (String, String))>> {
|
||||
terminated(
|
||||
separated_list0(
|
||||
delimited(
|
||||
many0(satisfy(char::is_whitespace)),
|
||||
char(','),
|
||||
many0(satisfy(char::is_whitespace)),
|
||||
),
|
||||
alt((name_addr, map(addr_spec, |addr| (None, addr)))),
|
||||
),
|
||||
eof,
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
// 3.4.1. Addr-spec specification
|
||||
// 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>> {
|
||||
local_part()
|
||||
.collect()
|
||||
.then_ignore(just('@'))
|
||||
.then(domain().collect())
|
||||
pub(super) fn addr_spec(input: &str) -> IResult<&str, (String, String)> {
|
||||
pair(terminated(local_part, char('@')), domain).parse(input)
|
||||
}
|
||||
|
||||
// local-part = dot-atom / quoted-string / obs-local-part
|
||||
pub fn local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
choice((dot_atom(), quoted_string(), obs_local_part()))
|
||||
pub(super) fn local_part(input: &str) -> IResult<&str, String> {
|
||||
alt((dot_atom, quoted_string, obs_local_part)).parse(input)
|
||||
}
|
||||
|
||||
// domain = dot-atom / domain-literal / obs-domain
|
||||
pub fn domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
pub(super) fn domain(input: &str) -> IResult<&str, String> {
|
||||
// NOTE: omitting domain-literal since it may never be used
|
||||
choice((dot_atom(), obs_domain()))
|
||||
alt((dot_atom, obs_domain)).parse(input)
|
||||
}
|
||||
|
||||
// 4.1. Miscellaneous obsolete tokens
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.1
|
||||
|
||||
// obs-phrase = word *(word / "." / CFWS)
|
||||
fn obs_phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
fn obs_phrase(input: &str) -> IResult<&str, String> {
|
||||
// NOTE: the CFWS is already captured by the word, no need to add
|
||||
// it there.
|
||||
word().chain(
|
||||
choice((word(), just('.').repeated().exactly(1)))
|
||||
.repeated()
|
||||
.flatten(),
|
||||
map(
|
||||
pair(word, many0(alt((word, map(char('.'), |c| c.to_string()))))),
|
||||
|(first, rest)| {
|
||||
let mut result = first;
|
||||
for part in rest {
|
||||
result.push_str(&part);
|
||||
}
|
||||
result
|
||||
},
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
// 4.4. Obsolete Addressing
|
||||
// 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>> {
|
||||
word().chain(just('.').chain(word()).repeated().flatten())
|
||||
pub(super) fn obs_local_part(input: &str) -> IResult<&str, String> {
|
||||
map(
|
||||
pair(
|
||||
word,
|
||||
many0(map(pair(char('.'), word), |(dot, w)| format!("{dot}{w}"))),
|
||||
),
|
||||
|(first, rest)| {
|
||||
let mut result = first;
|
||||
for part in rest {
|
||||
result.push_str(&part);
|
||||
}
|
||||
result
|
||||
},
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
// obs-domain = atom *("." atom)
|
||||
pub fn obs_domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
atom().chain(just('.').chain(atom()).repeated().flatten())
|
||||
pub(super) fn obs_domain(input: &str) -> IResult<&str, String> {
|
||||
map(
|
||||
pair(
|
||||
atom,
|
||||
many0(map(pair(char('.'), atom), |(dot, a)| format!("{dot}{a}"))),
|
||||
),
|
||||
|(first, rest)| {
|
||||
let mut result = first;
|
||||
for part in rest {
|
||||
result.push_str(&part);
|
||||
}
|
||||
result
|
||||
},
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//!
|
||||
//! [RFC5336]: https://datatracker.ietf.org/doc/html/rfc5336
|
||||
|
||||
use chumsky::{error::Cheap, prelude::*};
|
||||
use nom::{character::complete::satisfy, IResult, Parser};
|
||||
|
||||
// 3.3. Extended Mailbox Address Syntax
|
||||
// https://datatracker.ietf.org/doc/html/rfc5336#section-3.3
|
||||
@@ -12,6 +12,6 @@ use chumsky::{error::Cheap, prelude::*};
|
||||
// UTF8-2 = <See Section 4 of RFC 3629>
|
||||
// UTF8-3 = <See Section 4 of RFC 3629>
|
||||
// UTF8-4 = <See Section 4 of RFC 3629>
|
||||
pub(super) fn utf8_non_ascii() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c: &char| c.len_utf8() > 1)
|
||||
pub(super) fn utf8_non_ascii(input: &str) -> IResult<&str, char> {
|
||||
satisfy(|c| c.len_utf8() > 1).parse(input)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ use std::{
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use chumsky::prelude::*;
|
||||
use email_encoding::headers::writer::EmailWriter;
|
||||
|
||||
use super::parsers;
|
||||
@@ -114,7 +113,7 @@ impl FromStr for Mailbox {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
|
||||
let (name, (user, domain)) = parsers::mailbox().parse(src).map_err(|_errs| {
|
||||
let (_rest, (name, (user, domain))) = parsers::mailbox(src).map_err(|_errs| {
|
||||
// TODO: improve error management
|
||||
AddressError::InvalidInput
|
||||
})?;
|
||||
@@ -345,7 +344,7 @@ impl FromStr for Mailboxes {
|
||||
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
||||
let mut mailboxes = Vec::new();
|
||||
|
||||
let parsed_mailboxes = parsers::mailbox_list().parse(src).map_err(|_errs| {
|
||||
let (_rest, parsed_mailboxes) = parsers::mailbox_list(src).map_err(|_errs| {
|
||||
// TODO: improve error management
|
||||
AddressError::InvalidInput
|
||||
})?;
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -9,11 +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;
|
||||
use futures_rustls::client::TlsStream as AsyncStd1RustlsStream;
|
||||
#[cfg(any(feature = "tokio1-rustls", feature = "async-std1-rustls"))]
|
||||
use rustls::pki_types::ServerName;
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
@@ -28,7 +28,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 +79,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 +88,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 +113,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 +130,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 +203,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>,
|
||||
@@ -321,7 +323,7 @@ impl AsyncNetworkStream {
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(connector) => {
|
||||
InnerTlsParameters::NativeTls { connector } => {
|
||||
#[cfg(not(feature = "tokio1-native-tls"))]
|
||||
panic!("built without the tokio1-native-tls feature");
|
||||
|
||||
@@ -338,7 +340,7 @@ impl AsyncNetworkStream {
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerTlsParameters::RustlsTls(config) => {
|
||||
InnerTlsParameters::Rustls { config } => {
|
||||
#[cfg(not(feature = "tokio1-rustls"))]
|
||||
panic!("built without the tokio1-rustls feature");
|
||||
|
||||
@@ -354,18 +356,21 @@ impl AsyncNetworkStream {
|
||||
.connect(domain.to_owned(), tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
|
||||
Ok(InnerAsyncNetworkStream::Tokio1Rustls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerTlsParameters::BoringTls(connector) => {
|
||||
InnerTlsParameters::BoringTls {
|
||||
connector,
|
||||
accept_invalid_hostnames,
|
||||
} => {
|
||||
#[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);
|
||||
config.set_verify_hostname(accept_invalid_hostnames);
|
||||
|
||||
let stream = tokio1_boring::connect(config, &domain, tcp_stream)
|
||||
.await
|
||||
@@ -386,11 +391,11 @@ impl AsyncNetworkStream {
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(connector) => {
|
||||
InnerTlsParameters::NativeTls { connector } => {
|
||||
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 { config } => {
|
||||
#[cfg(not(feature = "async-std1-rustls"))]
|
||||
panic!("built without the async-std1-rustls feature");
|
||||
|
||||
@@ -406,11 +411,11 @@ impl AsyncNetworkStream {
|
||||
.connect(domain.to_owned(), tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
|
||||
Ok(InnerAsyncNetworkStream::AsyncStd1Rustls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerTlsParameters::BoringTls(connector) => {
|
||||
InnerTlsParameters::BoringTls { .. } => {
|
||||
panic!("boring-tls isn't supported with async-std yet.");
|
||||
}
|
||||
}
|
||||
@@ -423,18 +428,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 +450,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 +460,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 +474,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 +495,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 +522,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 +542,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 +582,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 +602,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 +624,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 +645,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 +666,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(()))
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ struct ClientCodec {
|
||||
|
||||
impl ClientCodec {
|
||||
/// Creates a new client codec
|
||||
pub fn new() -> Self {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
status: CodecStatus::StartOfNewLine,
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
@@ -174,27 +174,30 @@ impl NetworkStream {
|
||||
) -> Result<InnerNetworkStream, Error> {
|
||||
Ok(match &tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(connector) => {
|
||||
InnerTlsParameters::NativeTls { connector } => {
|
||||
let stream = connector
|
||||
.connect(tls_parameters.domain(), tcp_stream)
|
||||
.map_err(error::connection)?;
|
||||
InnerNetworkStream::NativeTls(stream)
|
||||
}
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerTlsParameters::RustlsTls(connector) => {
|
||||
InnerTlsParameters::Rustls { config } => {
|
||||
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())
|
||||
let connection = ClientConnection::new(Arc::clone(config), domain.to_owned())
|
||||
.map_err(error::connection)?;
|
||||
let stream = StreamOwned::new(connection, tcp_stream);
|
||||
InnerNetworkStream::RustlsTls(stream)
|
||||
InnerNetworkStream::Rustls(stream)
|
||||
}
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerTlsParameters::BoringTls(connector) => {
|
||||
InnerTlsParameters::BoringTls {
|
||||
connector,
|
||||
accept_invalid_hostnames,
|
||||
} => {
|
||||
let stream = connector
|
||||
.configure()
|
||||
.map_err(error::connection)?
|
||||
.verify_hostname(tls_parameters.accept_invalid_hostnames)
|
||||
.verify_hostname(*accept_invalid_hostnames)
|
||||
.connect(tls_parameters.domain(), 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 => {
|
||||
|
||||
@@ -65,6 +65,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 +148,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 +182,34 @@ 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,
|
||||
}
|
||||
|
||||
/// 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 +260,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 +285,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
|
||||
@@ -328,10 +373,8 @@ impl TlsParametersBuilder {
|
||||
|
||||
let connector = tls_builder.build().map_err(error::tls)?;
|
||||
Ok(TlsParameters {
|
||||
connector: InnerTlsParameters::NativeTls(connector),
|
||||
connector: InnerTlsParameters::NativeTls { connector },
|
||||
domain: self.domain,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
accept_invalid_hostnames: self.accept_invalid_hostnames,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -389,9 +432,11 @@ impl TlsParametersBuilder {
|
||||
.map_err(error::tls)?;
|
||||
let connector = tls_builder.build();
|
||||
Ok(TlsParameters {
|
||||
connector: InnerTlsParameters::BoringTls(connector),
|
||||
connector: InnerTlsParameters::BoringTls {
|
||||
connector,
|
||||
accept_invalid_hostnames: self.accept_invalid_hostnames,
|
||||
},
|
||||
domain: self.domain,
|
||||
accept_invalid_hostnames: self.accept_invalid_hostnames,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -419,7 +464,10 @@ impl TlsParametersBuilder {
|
||||
// Build TLS config
|
||||
let mut root_cert_store = RootCertStore::empty();
|
||||
|
||||
#[cfg(feature = "rustls-native-certs")]
|
||||
#[cfg(all(
|
||||
not(feature = "rustls-platform-verifier"),
|
||||
feature = "rustls-native-certs"
|
||||
))]
|
||||
fn load_native_roots(store: &mut RootCertStore) {
|
||||
let rustls_native_certs::CertificateResult { certs, errors, .. } =
|
||||
rustls_native_certs::load_native_certs();
|
||||
@@ -439,11 +487,26 @@ impl TlsParametersBuilder {
|
||||
store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "rustls-platform-verifier"), allow(unused_mut))]
|
||||
let mut extra_roots = None::<Vec<CertificateDer<'static>>>;
|
||||
match self.cert_store {
|
||||
CertificateStore::Default => {
|
||||
#[cfg(feature = "rustls-native-certs")]
|
||||
#[cfg(feature = "rustls-platform-verifier")]
|
||||
{
|
||||
extra_roots = Some(Vec::new());
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
not(feature = "rustls-platform-verifier"),
|
||||
feature = "rustls-native-certs"
|
||||
))]
|
||||
load_native_roots(&mut root_cert_store);
|
||||
#[cfg(all(not(feature = "rustls-native-certs"), feature = "webpki-roots"))]
|
||||
|
||||
#[cfg(all(
|
||||
not(feature = "rustls-platform-verifier"),
|
||||
not(feature = "rustls-native-certs"),
|
||||
feature = "webpki-roots"
|
||||
))]
|
||||
load_webpki_roots(&mut root_cert_store);
|
||||
}
|
||||
#[cfg(all(feature = "rustls", feature = "webpki-roots"))]
|
||||
@@ -454,11 +517,17 @@ impl TlsParametersBuilder {
|
||||
}
|
||||
for cert in self.root_certs {
|
||||
for rustls_cert in cert.rustls {
|
||||
#[cfg(feature = "rustls-platform-verifier")]
|
||||
if let Some(extra_roots) = &mut extra_roots {
|
||||
extra_roots.push(rustls_cert.clone());
|
||||
}
|
||||
root_cert_store.add(rustls_cert).map_err(error::tls)?;
|
||||
}
|
||||
}
|
||||
|
||||
let tls = if self.accept_invalid_certs || self.accept_invalid_hostnames {
|
||||
let tls = if self.accept_invalid_certs
|
||||
|| (extra_roots.is_none() && self.accept_invalid_hostnames)
|
||||
{
|
||||
let verifier = InvalidCertsVerifier {
|
||||
ignore_invalid_hostnames: self.accept_invalid_hostnames,
|
||||
ignore_invalid_certs: self.accept_invalid_certs,
|
||||
@@ -468,7 +537,23 @@ impl TlsParametersBuilder {
|
||||
tls.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(verifier))
|
||||
} else {
|
||||
tls.with_root_certificates(root_cert_store)
|
||||
#[cfg(feature = "rustls-platform-verifier")]
|
||||
if let Some(extra_roots) = extra_roots {
|
||||
tls.dangerous().with_custom_certificate_verifier(Arc::new(
|
||||
rustls_platform_verifier::Verifier::new_with_extra_roots(
|
||||
extra_roots,
|
||||
crypto_provider,
|
||||
)
|
||||
.map_err(error::tls)?,
|
||||
))
|
||||
} else {
|
||||
tls.with_root_certificates(root_cert_store)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "rustls-platform-verifier"))]
|
||||
{
|
||||
tls.with_root_certificates(root_cert_store)
|
||||
}
|
||||
};
|
||||
|
||||
let tls = if let Some(identity) = self.identity {
|
||||
@@ -480,23 +565,26 @@ impl TlsParametersBuilder {
|
||||
};
|
||||
|
||||
Ok(TlsParameters {
|
||||
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
|
||||
connector: InnerTlsParameters::Rustls {
|
||||
config: Arc::new(tls),
|
||||
},
|
||||
domain: self.domain,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
accept_invalid_hostnames: self.accept_invalid_hostnames,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum InnerTlsParameters {
|
||||
pub(crate) enum InnerTlsParameters {
|
||||
#[cfg(feature = "native-tls")]
|
||||
NativeTls(TlsConnector),
|
||||
NativeTls { connector: TlsConnector },
|
||||
#[cfg(feature = "rustls")]
|
||||
RustlsTls(Arc<ClientConfig>),
|
||||
Rustls { config: Arc<ClientConfig> },
|
||||
#[cfg(feature = "boring-tls")]
|
||||
BoringTls(SslConnector),
|
||||
BoringTls {
|
||||
connector: SslConnector,
|
||||
accept_invalid_hostnames: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl TlsParameters {
|
||||
@@ -545,6 +633,16 @@ impl TlsParameters {
|
||||
/// 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,
|
||||
@@ -608,6 +706,16 @@ impl Debug for Certificate {
|
||||
|
||||
/// An identity that can be used with [`TlsParametersBuilder::identify_with`]
|
||||
#[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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user