Compare commits

...

19 Commits

Author SHA1 Message Date
Paolo Barbolini
c3d00051b2 Prepare 0.11.4 (#936) 2024-01-28 08:08:26 +01:00
Birk Tjelmeland
12580d82f4 style(email): Change Part::body_raw to Part::format_body 2024-01-28 07:54:16 +01:00
Birk Tjelmeland
f7849078b8 fix(email): Fix mimebody DKIM body-hash computation 2024-01-28 07:54:16 +01:00
Paolo Barbolini
f2c94cdf4d chore(cargo): bump maud to v0.26 (#935) 2024-01-25 20:08:32 +01:00
Paolo Barbolini
74f64b81ab test(transport/smtp): test credentials percent decoding from URL (#934) 2024-01-25 20:05:31 +01:00
42triangles
39c71dbfd2 transport/smtp: percent decode credentials in URL (#932) 2024-01-12 11:43:30 +00:00
Paolo Barbolini
c1bf5dfda1 Prepare 0.11.3 (#929) 2024-01-02 18:45:34 +01:00
Paolo Barbolini
1c1fef8055 Drop once_cell dependency in favor of OnceLock from std (#928) 2024-01-02 11:53:47 +01:00
Paolo Barbolini
1540f16015 Upgrade rustls to v0.22 (#921) 2024-01-02 11:41:16 +01:00
Tobias Bieniek
330daa1173 transport/smtp: Implement Debug trait (#925) 2023-12-17 09:20:51 +01:00
Tobias Bieniek
47f2fe0750 transport/file: Derive Clone impls (#924) 2023-12-16 21:30:57 +01:00
Paolo Barbolini
8b6cee30ee Prepare 0.11.2 (#919) 2023-11-23 09:49:21 +01:00
Paolo Barbolini
62c16e90ef Bump idna to v0.5 (#918) 2023-11-23 08:23:25 +00:00
Paolo Barbolini
e0494a5f9d Bump boringssl crates to v4 (#915) 2023-11-19 11:49:43 +01:00
Paolo Barbolini
8c3bffa728 Bump MSRV to 1.70 (#916) 2023-11-19 11:42:49 +01:00
Paolo Barbolini
47eda90433 Prepare 0.11.1 (#910) 2023-10-24 23:47:49 +02:00
Paolo Barbolini
46ea8c48ac Ignore rustls deprecation warning 2023-10-24 22:55:16 +02:00
Paolo Barbolini
5f7063fdc3 Fix accidental disabling of webpki-roots setup (#909) 2023-10-24 22:54:22 +02:00
Paolo Barbolini
61c1f6bc6f Fix date in changelog 2023-10-15 17:22:48 +02:00
14 changed files with 215 additions and 103 deletions

View File

@@ -75,8 +75,8 @@ jobs:
rust: stable rust: stable
- name: beta - name: beta
rust: beta rust: beta
- name: 1.65.0 - name: '1.70'
rust: 1.65.0 rust: '1.70'
steps: steps:
- name: Checkout - name: Checkout

View File

@@ -1,5 +1,60 @@
<a name="v0.11.4"></a>
### v0.11.4 (2024-01-28)
#### Bug fixes
* Percent decode credentials in SMTP connect URL ([#932], [#934])
* Fix mimebody DKIM body-hash computation ([#923])
[#923]: https://github.com/lettre/lettre/pull/923
[#932]: https://github.com/lettre/lettre/pull/932
[#934]: https://github.com/lettre/lettre/pull/934
<a name="v0.11.3"></a>
### v0.11.3 (2024-01-02)
#### Features
* Derive `Clone` for `FileTransport` and `AsyncFileTransport` ([#924])
* Derive `Debug` for `SmtpTransport` ([#925])
#### Misc
* Upgrade `rustls` to v0.22 ([#921])
* Drop once_cell dependency in favor of OnceLock from std ([#928])
[#921]: https://github.com/lettre/lettre/pull/921
[#924]: https://github.com/lettre/lettre/pull/924
[#925]: https://github.com/lettre/lettre/pull/925
[#928]: https://github.com/lettre/lettre/pull/928
<a name="v0.11.2"></a>
### v0.11.2 (2023-11-23)
#### Upgrade notes
* MSRV is now 1.70 ([#916])
#### Misc
* Bump `idna` to v0.5 ([#918])
* Bump `boring` and `tokio-boring` to v4 ([#915])
[#915]: https://github.com/lettre/lettre/pull/915
[#916]: https://github.com/lettre/lettre/pull/916
[#918]: https://github.com/lettre/lettre/pull/918
<a name="v0.11.1"></a>
### v0.11.1 (2023-10-24)
#### Bug fixes
* Fix `webpki-roots` certificate store setup ([#909])
[#909]: https://github.com/lettre/lettre/pull/909
<a name="v0.11.0"></a> <a name="v0.11.0"></a>
### v0.11.0 (2023-08-15) ### v0.11.0 (2023-10-15)
While this release technically contains breaking changes, we expect most projects While this release technically contains breaking changes, we expect most projects
to be able to upgrade by only bumping the version in `Cargo.toml`. to be able to upgrade by only bumping the version in `Cargo.toml`.

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lettre" name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge) # remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.11.0" version = "0.11.4"
description = "Email client" description = "Email client"
readme = "README.md" readme = "README.md"
homepage = "https://lettre.rs" homepage = "https://lettre.rs"
@@ -11,7 +11,7 @@ authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo
categories = ["email", "network-programming"] categories = ["email", "network-programming"]
keywords = ["email", "smtp", "mailer", "message", "sendmail"] keywords = ["email", "smtp", "mailer", "message", "sendmail"]
edition = "2021" edition = "2021"
rust-version = "1.65" rust-version = "1.70"
[badges] [badges]
is-it-maintained-issue-resolution = { repository = "lettre/lettre" } is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
@@ -20,8 +20,7 @@ maintenance = { status = "actively-developed" }
[dependencies] [dependencies]
chumsky = "0.9" chumsky = "0.9"
idna = "0.4" idna = "0.5"
once_cell = { version = "1", optional = true }
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
# builder # builder
@@ -42,14 +41,15 @@ nom = { version = "7", optional = true }
hostname = { version = "0.3", optional = true } # feature hostname = { version = "0.3", optional = true } # feature
socket2 = { version = "0.5.1", optional = true } socket2 = { version = "0.5.1", optional = true }
url = { version = "2.4", optional = true } url = { version = "2.4", optional = true }
percent-encoding = { version = "2.3", optional = true }
## tls ## tls
native-tls = { version = "0.2.5", optional = true } # feature native-tls = { version = "0.2.5", optional = true } # feature
rustls = { version = "0.21", features = ["dangerous_configuration"], optional = true } rustls = { version = "0.22.1", optional = true }
rustls-pemfile = { version = "1", optional = true } rustls-pemfile = { version = "2", optional = true }
rustls-native-certs = { version = "0.6.2", optional = true } rustls-native-certs = { version = "0.7", optional = true }
webpki-roots = { version = "0.25", optional = true } webpki-roots = { version = "0.26", optional = true }
boring = { version = "3", optional = true } boring = { version = "4", optional = true }
# async # async
futures-io = { version = "0.3.7", optional = true } futures-io = { version = "0.3.7", optional = true }
@@ -59,13 +59,13 @@ async-trait = { version = "0.1", optional = true }
## async-std ## async-std
async-std = { version = "1.8", optional = true } async-std = { version = "1.8", optional = true }
#async-native-tls = { version = "0.3.3", optional = true } #async-native-tls = { version = "0.3.3", optional = true }
futures-rustls = { version = "0.24", optional = true } futures-rustls = { version = "0.25", optional = true }
## tokio ## tokio
tokio1_crate = { package = "tokio", version = "1", optional = true } tokio1_crate = { package = "tokio", version = "1", optional = true }
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true } tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
tokio1_rustls = { package = "tokio-rustls", version = "0.24", optional = true } tokio1_rustls = { package = "tokio-rustls", version = "0.25", optional = true }
tokio1_boring = { package = "tokio-boring", version = "3", optional = true } tokio1_boring = { package = "tokio-boring", version = "4", optional = true }
## dkim ## dkim
sha2 = { version = "0.10", optional = true, features = ["oid"] } sha2 = { version = "0.10", optional = true, features = ["oid"] }
@@ -85,7 +85,7 @@ walkdir = "2"
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] } tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
async-std = { version = "1.8", features = ["attributes"] } async-std = { version = "1.8", features = ["attributes"] }
serde_json = "1" serde_json = "1"
maud = "0.25" maud = "0.26"
[[bench]] [[bench]]
harness = false harness = false
@@ -104,7 +104,7 @@ mime03 = ["dep:mime"]
file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"] file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
file-transport-envelope = ["serde", "dep:serde_json", "file-transport"] file-transport-envelope = ["serde", "dep:serde_json", "file-transport"]
sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"] sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:once_cell", "dep:url", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"] smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:url", "dep:percent-encoding", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
pool = ["dep:futures-util"] pool = ["dep:futures-util"]

View File

@@ -28,8 +28,8 @@
</div> </div>
<div align="center"> <div align="center">
<a href="https://deps.rs/crate/lettre/0.11.0"> <a href="https://deps.rs/crate/lettre/0.11.4">
<img src="https://deps.rs/crate/lettre/0.11.0/status.svg" <img src="https://deps.rs/crate/lettre/0.11.4/status.svg"
alt="dependency status" /> alt="dependency status" />
</a> </a>
</div> </div>
@@ -53,12 +53,12 @@ Lettre does not provide (for now):
## Supported Rust Versions ## Supported Rust Versions
Lettre supports all Rust versions released in the last 6 months. At the time of writing Lettre supports all Rust versions released in the last 6 months. At the time of writing
the minimum supported Rust version is 1.65, but this could change at any time either from the minimum supported Rust version is 1.70, but this could change at any time either from
one of our dependencies bumping their MSRV or by a new patch release of lettre. one of our dependencies bumping their MSRV or by a new patch release of lettre.
## Example ## Example
This library requires Rust 1.65 or newer. This library requires Rust 1.70 or newer.
To use this library, add the following to your `Cargo.toml`: To use this library, add the following to your `Cargo.toml`:
```toml ```toml

View File

@@ -6,7 +6,7 @@
//! * Secure defaults //! * Secure defaults
//! * Async support //! * Async support
//! //!
//! Lettre requires Rust 1.65 or newer. //! Lettre requires Rust 1.70 or newer.
//! //!
//! ## Features //! ## Features
//! //!
@@ -109,7 +109,7 @@
//! [mime 0.3]: https://docs.rs/mime/0.3 //! [mime 0.3]: https://docs.rs/mime/0.3
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376 //! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.0")] #![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.4")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")] #![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")] #![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]

View File

@@ -17,6 +17,16 @@ pub(super) enum Part {
Multi(MultiPart), Multi(MultiPart),
} }
impl Part {
#[cfg(feature = "dkim")]
pub(super) fn format_body(&self, out: &mut Vec<u8>) {
match self {
Part::Single(part) => part.format_body(out),
Part::Multi(part) => part.format_body(out),
}
}
}
impl EmailFormat for Part { impl EmailFormat for Part {
fn format(&self, out: &mut Vec<u8>) { fn format(&self, out: &mut Vec<u8>) {
match self { match self {
@@ -132,6 +142,12 @@ impl SinglePart {
self.format(&mut out); self.format(&mut out);
out out
} }
/// Format only the signlepart body
fn format_body(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.body);
out.extend_from_slice(b"\r\n");
}
} }
impl EmailFormat for SinglePart { impl EmailFormat for SinglePart {
@@ -139,8 +155,7 @@ impl EmailFormat for SinglePart {
write!(out, "{}", self.headers) write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers"); .expect("A Write implementation panicked while formatting headers");
out.extend_from_slice(b"\r\n"); out.extend_from_slice(b"\r\n");
out.extend_from_slice(&self.body); self.format_body(out);
out.extend_from_slice(b"\r\n");
} }
} }
@@ -373,14 +388,9 @@ impl MultiPart {
self.format(&mut out); self.format(&mut out);
out out
} }
}
impl EmailFormat for MultiPart {
fn format(&self, out: &mut Vec<u8>) {
write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers");
out.extend_from_slice(b"\r\n");
/// Format only the multipart body
fn format_body(&self, out: &mut Vec<u8>) {
let boundary = self.boundary(); let boundary = self.boundary();
for part in &self.parts { for part in &self.parts {
@@ -396,6 +406,15 @@ impl EmailFormat for MultiPart {
} }
} }
impl EmailFormat for MultiPart {
fn format(&self, out: &mut Vec<u8>) {
write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers");
out.extend_from_slice(b"\r\n");
self.format_body(out);
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;

View File

@@ -525,7 +525,7 @@ impl Message {
pub(crate) fn body_raw(&self) -> Vec<u8> { pub(crate) fn body_raw(&self) -> Vec<u8> {
let mut out = Vec::new(); let mut out = Vec::new();
match &self.body { match &self.body {
MessageBody::Mime(p) => p.format(&mut out), MessageBody::Mime(p) => p.format_body(&mut out),
MessageBody::Raw(r) => out.extend_from_slice(r), MessageBody::Raw(r) => out.extend_from_slice(r),
}; };
out.extend_from_slice(b"\r\n"); out.extend_from_slice(b"\r\n");

View File

@@ -157,7 +157,7 @@ mod error;
type Id = String; type Id = String;
/// Writes the content and the envelope information to a file /// Writes the content and the envelope information to a file
#[derive(Debug)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))] #[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
pub struct FileTransport { pub struct FileTransport {
@@ -167,7 +167,7 @@ pub struct FileTransport {
} }
/// Asynchronously writes the content and the envelope information to a file /// Asynchronously writes the content and the envelope information to a file
#[derive(Debug)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))] #[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
#[cfg(any(feature = "async-std1", feature = "tokio1"))] #[cfg(any(feature = "async-std1", feature = "tokio1"))]

View File

@@ -16,6 +16,8 @@ use futures_io::{
}; };
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream; use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
#[cfg(any(feature = "tokio1-rustls-tls", feature = "async-std1-rustls-tls"))]
use rustls::pki_types::ServerName;
#[cfg(feature = "tokio1-boring-tls")] #[cfg(feature = "tokio1-boring-tls")]
use tokio1_boring::SslStream as Tokio1SslStream; use tokio1_boring::SslStream as Tokio1SslStream;
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
@@ -350,7 +352,6 @@ impl AsyncNetworkStream {
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
return { return {
use rustls::ServerName;
use tokio1_rustls::TlsConnector; use tokio1_rustls::TlsConnector;
let domain = ServerName::try_from(domain.as_str()) let domain = ServerName::try_from(domain.as_str())
@@ -358,7 +359,7 @@ impl AsyncNetworkStream {
let connector = TlsConnector::from(config); let connector = TlsConnector::from(config);
let stream = connector let stream = connector
.connect(domain, tcp_stream) .connect(domain.to_owned(), tcp_stream)
.await .await
.map_err(error::connection)?; .map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream)) Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
@@ -424,14 +425,13 @@ impl AsyncNetworkStream {
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
return { return {
use futures_rustls::TlsConnector; use futures_rustls::TlsConnector;
use rustls::ServerName;
let domain = ServerName::try_from(domain.as_str()) let domain = ServerName::try_from(domain.as_str())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?; .map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connector = TlsConnector::from(config); let connector = TlsConnector::from(config);
let stream = connector let stream = connector
.connect(domain, tcp_stream) .connect(domain.to_owned(), tcp_stream)
.await .await
.map_err(error::connection)?; .map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream)) Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
@@ -486,8 +486,7 @@ impl AsyncNetworkStream {
.unwrap() .unwrap()
.first() .first()
.unwrap() .unwrap()
.clone() .to_vec()),
.0),
#[cfg(feature = "tokio1-boring-tls")] #[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
.ssl() .ssl()
@@ -509,8 +508,7 @@ impl AsyncNetworkStream {
.unwrap() .unwrap()
.first() .first()
.unwrap() .unwrap()
.clone() .to_vec()),
.0),
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"), InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
} }
} }

View File

@@ -12,7 +12,7 @@ use boring::ssl::SslStream;
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
use native_tls::TlsStream; use native_tls::TlsStream;
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
use rustls::{ClientConnection, ServerName, StreamOwned}; use rustls::{pki_types::ServerName, ClientConnection, StreamOwned};
use socket2::{Domain, Protocol, Type}; use socket2::{Domain, Protocol, Type};
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
@@ -189,7 +189,7 @@ impl NetworkStream {
InnerTlsParameters::RustlsTls(connector) => { InnerTlsParameters::RustlsTls(connector) => {
let domain = ServerName::try_from(tls_parameters.domain()) let domain = ServerName::try_from(tls_parameters.domain())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?; .map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connection = ClientConnection::new(Arc::clone(connector), domain) let connection = ClientConnection::new(Arc::clone(connector), domain.to_owned())
.map_err(error::connection)?; .map_err(error::connection)?;
let stream = StreamOwned::new(connection, tcp_stream); let stream = StreamOwned::new(connection, tcp_stream);
InnerNetworkStream::RustlsTls(stream) InnerNetworkStream::RustlsTls(stream)
@@ -241,8 +241,7 @@ impl NetworkStream {
.unwrap() .unwrap()
.first() .first()
.unwrap() .unwrap()
.clone() .to_vec()),
.0),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => Ok(stream InnerNetworkStream::BoringTls(stream) => Ok(stream
.ssl() .ssl()

View File

@@ -1,6 +1,6 @@
use std::fmt::{self, Debug}; use std::fmt::{self, Debug};
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
use std::{sync::Arc, time::SystemTime}; use std::{io, sync::Arc};
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
use boring::{ use boring::{
@@ -11,8 +11,10 @@ use boring::{
use native_tls::{Protocol, TlsConnector}; use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
use rustls::{ use rustls::{
client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier}, client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
ClientConfig, Error as TlsError, RootCertStore, ServerName, crypto::{verify_tls12_signature, verify_tls13_signature},
pki_types::{CertificateDer, ServerName, UnixTime},
ClientConfig, DigitallySignedStruct, Error as TlsError, RootCertStore, SignatureScheme,
}; };
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
@@ -115,7 +117,7 @@ pub enum CertificateStore {
/// Use a hardcoded set of Mozilla roots via the `webpki-roots` crate. /// Use a hardcoded set of Mozilla roots via the `webpki-roots` crate.
/// ///
/// This option is only available in the rustls backend. /// This option is only available in the rustls backend.
#[cfg(feature = "webpki-roots")] #[cfg(feature = "rustls-tls")]
WebpkiRoots, WebpkiRoots,
/// Don't use any system certificates. /// Don't use any system certificates.
None, None,
@@ -337,8 +339,6 @@ impl TlsParametersBuilder {
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))] #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
pub fn build_rustls(self) -> Result<TlsParameters, Error> { pub fn build_rustls(self) -> Result<TlsParameters, Error> {
let tls = ClientConfig::builder();
let just_version3 = &[&rustls::version::TLS13]; let just_version3 = &[&rustls::version::TLS13];
let supported_versions = match self.min_tls_version { let supported_versions = match self.min_tls_version {
TlsVersion::Tlsv10 => { TlsVersion::Tlsv10 => {
@@ -351,58 +351,38 @@ impl TlsParametersBuilder {
TlsVersion::Tlsv13 => just_version3, TlsVersion::Tlsv13 => just_version3,
}; };
let tls = tls let tls = ClientConfig::builder_with_protocol_versions(supported_versions);
.with_safe_default_cipher_suites()
.with_safe_default_kx_groups()
.with_protocol_versions(supported_versions)
.map_err(error::tls)?;
let tls = if self.accept_invalid_certs { let tls = if self.accept_invalid_certs {
tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {})) tls.dangerous()
.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
} else { } else {
let mut root_cert_store = RootCertStore::empty(); let mut root_cert_store = RootCertStore::empty();
#[cfg(feature = "rustls-native-certs")] #[cfg(feature = "rustls-native-certs")]
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> { fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?; let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?;
let mut valid_count = 0; let (added, ignored) = store.add_parsable_certificates(native_certs);
let mut invalid_count = 0;
for cert in native_certs {
match store.add(&rustls::Certificate(cert.0)) {
Ok(_) => valid_count += 1,
Err(err) => {
#[cfg(feature = "tracing")]
tracing::debug!("certificate parsing failed: {:?}", err);
invalid_count += 1;
}
}
}
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!( tracing::debug!(
"loaded platform certs with {valid_count} valid and {invalid_count} invalid certs" "loaded platform certs with {added} valid and {ignored} ignored (invalid) certs"
); );
Ok(()) Ok(())
} }
#[cfg(feature = "webpki-roots")] #[cfg(feature = "rustls-tls")]
fn load_webpki_roots(store: &mut RootCertStore) { fn load_webpki_roots(store: &mut RootCertStore) {
store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| { store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
}));
} }
match self.cert_store { match self.cert_store {
CertificateStore::Default => { CertificateStore::Default => {
#[cfg(feature = "rustls-native-certs")] #[cfg(feature = "rustls-native-certs")]
load_native_roots(&mut root_cert_store)?; load_native_roots(&mut root_cert_store)?;
#[cfg(all(not(feature = "rustls-native-certs"), feature = "webpki-roots"))] #[cfg(not(feature = "rustls-native-certs"))]
load_webpki_roots(&mut root_cert_store); load_webpki_roots(&mut root_cert_store);
} }
#[cfg(feature = "webpki-roots")] #[cfg(feature = "rustls-tls")]
CertificateStore::WebpkiRoots => { CertificateStore::WebpkiRoots => {
load_webpki_roots(&mut root_cert_store); load_webpki_roots(&mut root_cert_store);
} }
@@ -410,14 +390,11 @@ impl TlsParametersBuilder {
} }
for cert in self.root_certs { for cert in self.root_certs {
for rustls_cert in cert.rustls { for rustls_cert in cert.rustls {
root_cert_store.add(&rustls_cert).map_err(error::tls)?; root_cert_store.add(rustls_cert).map_err(error::tls)?;
} }
} }
tls.with_custom_certificate_verifier(Arc::new(WebPkiVerifier::new( tls.with_root_certificates(root_cert_store)
root_cert_store,
None,
)))
}; };
let tls = tls.with_no_client_auth(); let tls = tls.with_no_client_auth();
@@ -491,7 +468,7 @@ pub struct Certificate {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
native_tls: native_tls::Certificate, native_tls: native_tls::Certificate,
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
rustls: Vec<rustls::Certificate>, rustls: Vec<CertificateDer<'static>>,
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
boring_tls: boring::x509::X509, boring_tls: boring::x509::X509,
} }
@@ -510,7 +487,7 @@ impl Certificate {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
native_tls: native_tls_cert, native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
rustls: vec![rustls::Certificate(der)], rustls: vec![der.into()],
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert, boring_tls: boring_tls_cert,
}) })
@@ -530,10 +507,8 @@ impl Certificate {
let mut pem = Cursor::new(pem); let mut pem = Cursor::new(pem);
rustls_pemfile::certs(&mut pem) rustls_pemfile::certs(&mut pem)
.collect::<io::Result<Vec<_>>>()
.map_err(|_| error::tls("invalid certificates"))? .map_err(|_| error::tls("invalid certificates"))?
.into_iter()
.map(rustls::Certificate)
.collect::<Vec<_>>()
}; };
Ok(Self { Ok(Self {
@@ -554,19 +529,53 @@ impl Debug for Certificate {
} }
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
#[derive(Debug)]
struct InvalidCertsVerifier; struct InvalidCertsVerifier;
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
impl ServerCertVerifier for InvalidCertsVerifier { impl ServerCertVerifier for InvalidCertsVerifier {
fn verify_server_cert( fn verify_server_cert(
&self, &self,
_end_entity: &rustls::Certificate, _end_entity: &CertificateDer<'_>,
_intermediates: &[rustls::Certificate], _intermediates: &[CertificateDer<'_>],
_server_name: &ServerName, _server_name: &ServerName<'_>,
_scts: &mut dyn Iterator<Item = &[u8]>,
_ocsp_response: &[u8], _ocsp_response: &[u8],
_now: SystemTime, _now: UnixTime,
) -> Result<ServerCertVerified, TlsError> { ) -> Result<ServerCertVerified, TlsError> {
Ok(ServerCertVerified::assertion()) Ok(ServerCertVerified::assertion())
} }
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, TlsError> {
verify_tls12_signature(
message,
cert,
dss,
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, TlsError> {
verify_tls13_signature(
message,
cert,
dss,
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
rustls::crypto::ring::default_provider()
.signature_verification_algorithms
.supported_schemes()
}
} }

View File

@@ -112,7 +112,16 @@ pub(crate) fn from_connection_url<B: TransportBuilder>(connection_url: &str) ->
} }
if let Some(password) = connection_url.password() { if let Some(password) = connection_url.password() {
let credentials = Credentials::new(connection_url.username().into(), password.into()); let percent_decode = |s: &str| {
percent_encoding::percent_decode_str(s)
.decode_utf8()
.map(|cow| cow.into_owned())
.map_err(error::connection)
};
let credentials = Credentials::new(
percent_decode(connection_url.username())?,
percent_decode(password)?,
);
builder = builder.credentials(credentials); builder = builder.credentials(credentials);
} }

View File

@@ -2,7 +2,7 @@ use std::{
fmt::{self, Debug}, fmt::{self, Debug},
mem, mem,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
sync::Arc, sync::{Arc, OnceLock},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@@ -10,7 +10,6 @@ use futures_util::{
lock::Mutex, lock::Mutex,
stream::{self, StreamExt}, stream::{self, StreamExt},
}; };
use once_cell::sync::OnceCell;
use super::{ use super::{
super::{client::AsyncSmtpConnection, Error}, super::{client::AsyncSmtpConnection, Error},
@@ -22,7 +21,7 @@ pub struct Pool<E: Executor> {
config: PoolConfig, config: PoolConfig,
connections: Mutex<Vec<ParkedConnection>>, connections: Mutex<Vec<ParkedConnection>>,
client: AsyncSmtpClient<E>, client: AsyncSmtpClient<E>,
handle: OnceCell<E::Handle>, handle: OnceLock<E::Handle>,
} }
struct ParkedConnection { struct ParkedConnection {
@@ -41,7 +40,7 @@ impl<E: Executor> Pool<E> {
config, config,
connections: Mutex::new(Vec::new()), connections: Mutex::new(Vec::new()),
client, client,
handle: OnceCell::new(), handle: OnceLock::new(),
}); });
{ {

View File

@@ -1,6 +1,6 @@
#[cfg(feature = "pool")] #[cfg(feature = "pool")]
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::{fmt::Debug, time::Duration};
#[cfg(feature = "pool")] #[cfg(feature = "pool")]
use super::pool::sync_impl::Pool; use super::pool::sync_impl::Pool;
@@ -38,6 +38,14 @@ impl Transport for SmtpTransport {
} }
} }
impl Debug for SmtpTransport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut builder = f.debug_struct("SmtpTransport");
builder.field("inner", &self.inner);
builder.finish()
}
}
impl SmtpTransport { impl SmtpTransport {
/// Simple and secure transport, using TLS connections to communicate with the SMTP server /// Simple and secure transport, using TLS connections to communicate with the SMTP server
/// ///
@@ -373,6 +381,22 @@ mod tests {
assert!(matches!(builder.info.tls, Tls::Wrapper(_))); assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
assert_eq!(builder.info.server, "smtp.example.com"); assert_eq!(builder.info.server, "smtp.example.com");
let builder = SmtpTransport::from_url(
"smtps://user%40example.com:pa$$word%3F%22!@smtp.example.com:465",
)
.unwrap();
assert_eq!(builder.info.port, 465);
assert_eq!(
builder.info.credentials,
Some(Credentials::new(
"user@example.com".to_owned(),
"pa$$word?\"!".to_owned()
))
);
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
assert_eq!(builder.info.server, "smtp.example.com");
let builder = let builder =
SmtpTransport::from_url("smtp://username:password@smtp.example.com:587?tls=required") SmtpTransport::from_url("smtp://username:password@smtp.example.com:587?tls=required")
.unwrap(); .unwrap();