Compare commits

..

26 Commits

Author SHA1 Message Date
Paolo Barbolini
716d7baac2 regression 2025-06-02 11:40:45 +02:00
Paolo Barbolini
659b0b50b1 fix 2025-06-02 11:14:11 +02:00
Paolo Barbolini
f332439000 clippy 2025-06-02 11:09:21 +02:00
Paolo Barbolini
335cdea3f9 Merge branch 'master' into better-tls-parameters-builder 2025-06-02 11:05:59 +02:00
Paolo Barbolini
7642b2130e simpler stuff 2025-05-02 08:54:34 +02:00
Paolo Barbolini
5a3f189e50 happy clippy 2025-05-02 08:51:24 +02:00
Paolo Barbolini
31b8f297ec enum weirdness 2025-05-02 08:43:50 +02:00
Paolo Barbolini
3a6ab6f398 async-std again 2025-05-02 08:33:34 +02:00
Paolo Barbolini
7b8fc5a678 async-std 2025-05-02 08:30:37 +02:00
Paolo Barbolini
2b36935b1f done 2025-05-02 08:29:20 +02:00
Paolo Barbolini
d31490a2a9 Make this all internal only for now 2025-05-02 07:20:20 +02:00
Paolo Barbolini
b7482f0232 examples 2025-05-02 05:47:53 +02:00
Paolo Barbolini
abc8cdf789 examples 2025-05-02 05:35:54 +02:00
Paolo Barbolini
81b233def4 warnings 2025-05-02 05:30:30 +02:00
Paolo Barbolini
e644a6c2d3 #[allow(missing_copy_implementations)] 2025-05-02 05:27:40 +02:00
Paolo Barbolini
0385ca3b19 deprecations 2025-05-02 05:26:18 +02:00
Paolo Barbolini
d114f9caf3 fix 2025-05-02 05:23:25 +02:00
Paolo Barbolini
785307b091 fix async-std 2025-05-02 05:22:03 +02:00
Paolo Barbolini
f16cbeec51 fixme 2025-05-02 05:19:39 +02:00
Paolo Barbolini
5cbe9ba283 Have the old builder use the new one underneath 2025-05-02 05:17:53 +02:00
Paolo Barbolini
2f4e36ac61 wip 2025-05-01 21:23:31 +02:00
Paolo Barbolini
610b72e93b broken doc build 2025-05-01 20:37:51 +02:00
Paolo Barbolini
69b7c5500a wip 2025-05-01 20:29:43 +02:00
Paolo Barbolini
63c5fcccfc Start moving types over 2025-05-01 20:22:23 +02:00
Paolo Barbolini
b583aff36c Move things 2025-05-01 19:42:30 +02:00
Paolo Barbolini
512c5e3ce8 wip 2025-05-01 19:02:13 +02:00
16 changed files with 1378 additions and 947 deletions

View File

@@ -1,51 +1,3 @@
<a name="v0.11.19"></a>
### v0.11.19 (2025-10-08)
#### Features
* Add raw header setter to `MessageBuilder` ([#1108])
#### Misc
* Fix README example ([#1114])
* Replace custom `static_assert!` macro with `std::assert!` ([#1112])
[#1108]: https://github.com/lettre/lettre/pull/1108
[#1112]: https://github.com/lettre/lettre/pull/1112
[#1114]: https://github.com/lettre/lettre/pull/1114
<a name="v0.11.18"></a>
### v0.11.18 (2025-07-28)
#### Features
* Allow inline attachments to be named ([#1101])
#### Misc
* Upgrade `socket2` to v0.6 ([#1098])
[#1098]: https://github.com/lettre/lettre/pull/1098
[#1101]: https://github.com/lettre/lettre/pull/1101
<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)

777
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package]
name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.11.19"
version = "0.11.16"
description = "Email client"
readme = "README.md"
homepage = "https://lettre.rs"
@@ -42,7 +42,7 @@ serde_json = { version = "1", optional = true }
# smtp-transport
nom = { version = "8", optional = true }
hostname = { version = "0.4", optional = true } # feature
socket2 = { version = "0.6", optional = true }
socket2 = { version = "0.5.1", optional = true }
url = { version = "2.4", optional = true }
percent-encoding = { version = "2.3", optional = true }

View File

@@ -28,8 +28,8 @@
</div>
<div align="center">
<a href="https://deps.rs/crate/lettre/0.11.19">
<img src="https://deps.rs/crate/lettre/0.11.19/status.svg"
<a href="https://deps.rs/crate/lettre/0.11.16">
<img src="https://deps.rs/crate/lettre/0.11.16/status.svg"
alt="dependency status" />
</a>
</div>
@@ -67,15 +67,15 @@ lettre = "0.11"
```
```rust,no_run
use lettre::message::{Mailbox, header::ContentType};
use lettre::message::header::ContentType;
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
fn main() {
let email = Message::builder()
.from(Mailbox::new(Some("NoBody".to_owned()), "nobody@domain.tld".parse().unwrap()))
.reply_to(Mailbox::new(Some("Yuin".to_owned()), "yuin@domain.tld".parse().unwrap()))
.to(Mailbox::new(Some("Hei".to_owned()), "hei@domain.tld".parse().unwrap()))
.from(Mailbox::new("NoBody".to_owned(), "nobody@domain.tld".parse().unwrap()))
.reply_to(Mailbox::new("Yuin".to_owned(), "yuin@domain.tld".parse().unwrap()))
.to(Mailbox::new("Hei".to_owned(), "hei@domain.tld".parse().unwrap()))
.subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!"))

View File

@@ -162,7 +162,7 @@
//! [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.19")]
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.16")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)]

View File

@@ -16,10 +16,7 @@ enum Disposition {
/// File name
Attached(String),
/// Content id
Inline {
content_id: String,
name: Option<String>,
},
Inline(String),
}
impl Attachment {
@@ -84,50 +81,7 @@ impl Attachment {
/// ```
pub fn new_inline(content_id: String) -> Self {
Attachment {
disposition: Disposition::Inline {
content_id,
name: None,
},
}
}
/// Create a new inline attachment giving it a name
///
/// This attachment should be displayed inline into the message
/// body:
///
/// ```html
/// <img src="cid:123">
/// ```
///
///
/// ```rust
/// # use std::error::Error;
/// use std::fs;
///
/// use lettre::message::{header::ContentType, Attachment};
///
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let content_id = String::from("123");
/// let file_name = String::from("image.jpg");
/// # if false {
/// let filebody = fs::read(&file_name)?;
/// # }
/// # let filebody = fs::read("docs/lettre.png")?;
/// let content_type = ContentType::parse("image/jpeg").unwrap();
/// let attachment =
/// Attachment::new_inline_with_name(content_id, file_name).body(filebody, content_type);
///
/// // The image `attachment` will display inline into the email.
/// # Ok(())
/// # }
/// ```
pub fn new_inline_with_name(content_id: String, name: String) -> Self {
Attachment {
disposition: Disposition::Inline {
content_id,
name: Some(name),
},
disposition: Disposition::Inline(content_id),
}
}
@@ -141,18 +95,9 @@ impl Attachment {
Disposition::Attached(filename) => {
builder.header(header::ContentDisposition::attachment(&filename))
}
Disposition::Inline {
content_id,
name: None,
} => builder
Disposition::Inline(content_id) => builder
.header(header::ContentId::from(format!("<{content_id}>")))
.header(header::ContentDisposition::inline()),
Disposition::Inline {
content_id,
name: Some(name),
} => builder
.header(header::ContentId::from(format!("<{content_id}>")))
.header(header::ContentDisposition::inline_with_name(&name)),
};
builder = builder.header(content_type);
builder.body(content)
@@ -197,24 +142,4 @@ mod tests {
)
);
}
#[test]
fn attachment_inline_with_name() {
let id = String::from("id");
let name = String::from("test");
let part = super::Attachment::new_inline_with_name(id, name).body(
String::from("Hello world!"),
ContentType::parse("text/plain").unwrap(),
);
assert_eq!(
&String::from_utf8_lossy(&part.formatted()),
concat!(
"Content-ID: <id>\r\n",
"Content-Disposition: inline; filename=\"test\"\r\n",
"Content-Type: text/plain\r\n",
"Content-Transfer-Encoding: 7bit\r\n\r\n",
"Hello world!\r\n"
)
);
}
}

View File

@@ -186,15 +186,21 @@ impl HeaderName {
/// Creates a new header name, panics on invalid name
pub const fn new_from_ascii_str(ascii: &'static str) -> Self {
assert!(!ascii.is_empty());
assert!(ascii.len() <= 76);
assert!(ascii.is_ascii());
macro_rules! static_assert {
($condition:expr) => {
let _ = [()][(!($condition)) as usize];
};
}
static_assert!(!ascii.is_empty());
static_assert!(ascii.len() <= 76);
let bytes = ascii.as_bytes();
let mut i = 0;
while i < bytes.len() {
assert!(bytes[i] != b' ');
assert!(bytes[i] != b':');
static_assert!(bytes[i].is_ascii());
static_assert!(bytes[i] != b' ');
static_assert!(bytes[i] != b':');
i += 1;
}

View File

@@ -217,7 +217,7 @@ mod mimebody;
use crate::{
address::Envelope,
message::header::{ContentTransferEncoding, Header, HeaderValue, Headers, MailboxesHeader},
message::header::{ContentTransferEncoding, Header, Headers, MailboxesHeader},
Error as EmailError,
};
@@ -369,12 +369,6 @@ impl MessageBuilder {
self
}
/// Set raw custom header to message
pub fn raw_header(mut self, raw_header: HeaderValue) -> Self {
self.headers.insert_raw(raw_header);
self
}
/// Add mailbox to header
pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self {
match self.headers.get::<H>() {
@@ -717,10 +711,7 @@ mod test {
.header(header::To(
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
))
.raw_header(header::HeaderValue::new(
header::HeaderName::new_from_ascii_str("Subject"),
"яңа ел белән!".to_owned(),
))
.header(header::Subject::from(String::from("яңа ел белән!")))
.body(String::from("Happy new year!"))
.unwrap();

View File

@@ -14,8 +14,6 @@ use futures_io::{
};
#[cfg(feature = "async-std1-rustls")]
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")]
use tokio1_boring::SslStream as Tokio1SslStream;
#[cfg(feature = "tokio1")]
@@ -319,11 +317,9 @@ impl AsyncNetworkStream {
tcp_stream: Box<dyn AsyncTokioStream>,
tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = tls_parameters.domain().to_owned();
match tls_parameters.connector {
match tls_parameters.inner {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls { connector } => {
InnerTlsParameters::NativeTls(inner) => {
#[cfg(not(feature = "tokio1-native-tls"))]
panic!("built without the tokio1-native-tls feature");
@@ -331,16 +327,16 @@ impl AsyncNetworkStream {
return {
use tokio1_native_tls_crate::TlsConnector;
let connector = TlsConnector::from(connector);
let connector = TlsConnector::from(inner.connector);
let stream = connector
.connect(&domain, tcp_stream)
.connect(&inner.server_name, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream))
};
}
#[cfg(feature = "rustls")]
InnerTlsParameters::Rustls { config } => {
InnerTlsParameters::Rustls(inner) => {
#[cfg(not(feature = "tokio1-rustls"))]
panic!("built without the tokio1-rustls feature");
@@ -348,31 +344,25 @@ impl AsyncNetworkStream {
return {
use tokio1_rustls::TlsConnector;
let domain = ServerName::try_from(domain.as_str())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connector = TlsConnector::from(config);
let connector = TlsConnector::from(inner.connector);
let stream = connector
.connect(domain.to_owned(), tcp_stream)
.connect(inner.server_name.inner(), tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1Rustls(stream))
};
}
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls {
connector,
accept_invalid_hostnames,
} => {
InnerTlsParameters::BoringTls(inner) => {
#[cfg(not(feature = "tokio1-boring-tls"))]
panic!("built without the tokio1-boring-tls feature");
#[cfg(feature = "tokio1-boring-tls")]
return {
let mut config = connector.configure().map_err(error::connection)?;
config.set_verify_hostname(accept_invalid_hostnames);
let mut config = inner.connector.configure().map_err(error::connection)?;
config.set_verify_hostname(inner.extra_info.accept_invalid_hostnames);
let stream = tokio1_boring::connect(config, &domain, tcp_stream)
let stream = tokio1_boring::connect(config, &inner.server_name, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1BoringTls(stream))
@@ -385,17 +375,15 @@ impl AsyncNetworkStream {
#[cfg(feature = "async-std1-rustls")]
async fn upgrade_asyncstd1_tls(
tcp_stream: AsyncStd1TcpStream,
mut tls_parameters: TlsParameters,
tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = mem::take(&mut tls_parameters.domain);
match tls_parameters.connector {
match tls_parameters.inner {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls { connector } => {
InnerTlsParameters::NativeTls(_) => {
panic!("native-tls isn't supported with async-std yet. See https://github.com/lettre/lettre/pull/531#issuecomment-757893531");
}
#[cfg(feature = "rustls")]
InnerTlsParameters::Rustls { config } => {
InnerTlsParameters::Rustls(inner) => {
#[cfg(not(feature = "async-std1-rustls"))]
panic!("built without the async-std1-rustls feature");
@@ -403,19 +391,16 @@ impl AsyncNetworkStream {
return {
use futures_rustls::TlsConnector;
let domain = ServerName::try_from(domain.as_str())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connector = TlsConnector::from(config);
let connector = TlsConnector::from(inner.connector);
let stream = connector
.connect(domain.to_owned(), tcp_stream)
.connect(inner.server_name.inner(), tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::AsyncStd1Rustls(stream))
};
}
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls { .. } => {
InnerTlsParameters::BoringTls(_inner) => {
panic!("boring-tls isn't supported with async-std yet.");
}
}

View File

@@ -34,12 +34,14 @@ pub use self::async_net::AsyncNetworkStream;
pub use self::async_net::AsyncTokioStream;
use self::net::NetworkStream;
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
pub(super) use self::tls::InnerTlsParameters;
pub(super) use self::tls::current::InnerTlsParameters;
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
pub use self::tls::TlsVersion;
pub use self::tls::current::TlsVersion;
pub use self::{
connection::SmtpConnection,
tls::{Certificate, CertificateStore, Identity, Tls, TlsParameters, TlsParametersBuilder},
tls::current::{
Certificate, CertificateStore, Identity, Tls, TlsParameters, TlsParametersBuilder,
},
};
#[cfg(any(feature = "tokio1", feature = "async-std1"))]

View File

@@ -12,7 +12,7 @@ use boring::ssl::SslStream;
#[cfg(feature = "native-tls")]
use native_tls::TlsStream;
#[cfg(feature = "rustls")]
use rustls::{pki_types::ServerName, ClientConnection, StreamOwned};
use rustls::{ClientConnection, StreamOwned};
use socket2::{Domain, Protocol, Type};
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
@@ -172,33 +172,33 @@ impl NetworkStream {
tcp_stream: TcpStream,
tls_parameters: &TlsParameters,
) -> Result<InnerNetworkStream, Error> {
Ok(match &tls_parameters.connector {
Ok(match &tls_parameters.inner {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls { connector } => {
let stream = connector
.connect(tls_parameters.domain(), tcp_stream)
InnerTlsParameters::NativeTls(inner) => {
let stream = inner
.connector
.connect(&inner.server_name, tcp_stream)
.map_err(error::connection)?;
InnerNetworkStream::NativeTls(stream)
}
#[cfg(feature = "rustls")]
InnerTlsParameters::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(config), domain.to_owned())
.map_err(error::connection)?;
InnerTlsParameters::Rustls(inner) => {
let connection = ClientConnection::new(
Arc::clone(&inner.connector),
inner.server_name.inner_ref().clone(),
)
.map_err(error::connection)?;
let stream = StreamOwned::new(connection, tcp_stream);
InnerNetworkStream::Rustls(stream)
}
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls {
connector,
accept_invalid_hostnames,
} => {
let stream = connector
InnerTlsParameters::BoringTls(inner) => {
let stream = inner
.connector
.configure()
.map_err(error::connection)?
.verify_hostname(*accept_invalid_hostnames)
.connect(tls_parameters.domain(), tcp_stream)
.verify_hostname(inner.extra_info.accept_invalid_hostnames)
.connect(&inner.server_name, tcp_stream)
.map_err(error::connection)?;
InnerNetworkStream::BoringTls(stream)
}

View File

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

View File

@@ -1,26 +1,9 @@
use std::fmt::{self, Debug};
#[cfg(feature = "rustls")]
use std::sync::Arc;
#[cfg(feature = "boring-tls")]
use boring::{
pkey::PKey,
ssl::{SslConnector, SslVersion},
x509::store::X509StoreBuilder,
};
#[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls")]
use rustls::{
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider},
pki_types::{self, pem::PemObject, CertificateDer, PrivateKeyDer, ServerName, UnixTime},
server::ParsedCertificate,
ClientConfig, DigitallySignedStruct, Error as TlsError, RootCertStore, SignatureScheme,
};
use super::TlsBackend;
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
use crate::transport::smtp::{error, Error};
use crate::transport::smtp::error;
use crate::transport::smtp::Error;
/// TLS protocol versions.
#[derive(Debug, Copy, Clone)]
@@ -193,9 +176,7 @@ pub enum CertificateStore {
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,
pub(in crate::transport::smtp) inner: InnerTlsParameters,
}
/// Builder for `TlsParameters`
@@ -335,30 +316,20 @@ impl TlsParametersBuilder {
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
pub fn build_native(self) -> Result<TlsParameters, Error> {
let mut tls_builder = TlsConnector::builder();
match self.cert_store {
CertificateStore::Default => {}
CertificateStore::None => {
tls_builder.disable_built_in_roots(true);
}
let cert_store = match self.cert_store {
CertificateStore::Default => super::native_tls::CertificateStore::System,
CertificateStore::None => super::native_tls::CertificateStore::None,
#[allow(unreachable_patterns)]
other => {
return Err(error::tls(format!(
"{other:?} is not supported in native tls"
)))
}
}
for cert in self.root_certs {
tls_builder.add_root_certificate(cert.native_tls);
}
tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames);
tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs);
};
let min_tls_version = match self.min_tls_version {
TlsVersion::Tlsv10 => Protocol::Tlsv10,
TlsVersion::Tlsv11 => Protocol::Tlsv11,
TlsVersion::Tlsv12 => Protocol::Tlsv12,
TlsVersion::Tlsv10 => super::native_tls::MinTlsVersion::Tlsv10,
TlsVersion::Tlsv11 => super::native_tls::MinTlsVersion::Tlsv11,
TlsVersion::Tlsv12 => super::native_tls::MinTlsVersion::Tlsv12,
TlsVersion::Tlsv13 => {
return Err(error::tls(
"min tls version Tlsv13 not supported in native tls",
@@ -366,225 +337,114 @@ impl TlsParametersBuilder {
}
};
tls_builder.min_protocol_version(Some(min_tls_version));
let mut builder = super::TlsParametersBuilder::<super::NativeTls>::new(self.domain)
.certificate_store(cert_store)
.dangerous_accept_invalid_certs(self.accept_invalid_certs)
.dangerous_accept_invalid_hostnames(self.accept_invalid_hostnames)
.min_tls_version(min_tls_version);
for cert in self.root_certs {
builder = builder.add_root_certificate(cert.native_tls);
}
if let Some(identity) = self.identity {
tls_builder.identity(identity.native_tls);
builder = builder.identify_with(identity.native_tls);
}
let connector = tls_builder.build().map_err(error::tls)?;
Ok(TlsParameters {
connector: InnerTlsParameters::NativeTls { connector },
domain: self.domain,
})
builder
.build()
.map(super::NativeTls::__build_current_tls_parameters)
}
/// Creates a new `TlsParameters` using boring-tls with the provided configuration
///
/// Warning: this uses the certificate store passed via `certificate_store`
/// instead of the one configured in [`TlsParametersBuilder::certificate_store`].
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn build_boring(self) -> Result<TlsParameters, Error> {
use boring::ssl::{SslMethod, SslVerifyMode};
let mut tls_builder = SslConnector::builder(SslMethod::tls_client()).map_err(error::tls)?;
if self.accept_invalid_certs {
tls_builder.set_verify(SslVerifyMode::NONE);
} else {
match self.cert_store {
CertificateStore::Default => {}
CertificateStore::None => {
// Replace the default store with an empty store.
tls_builder
.set_cert_store(X509StoreBuilder::new().map_err(error::tls)?.build());
}
#[allow(unreachable_patterns)]
other => {
return Err(error::tls(format!(
"{other:?} is not supported in boring tls"
)))
}
let cert_store = match self.cert_store {
CertificateStore::Default => super::boring_tls::CertificateStore::System,
CertificateStore::None => super::boring_tls::CertificateStore::None,
#[allow(unreachable_patterns)]
other => {
return Err(error::tls(format!(
"{other:?} is not supported in native tls"
)))
}
let cert_store = tls_builder.cert_store_mut();
for cert in self.root_certs {
cert_store.add_cert(cert.boring_tls).map_err(error::tls)?;
}
}
if let Some(identity) = self.identity {
tls_builder
.set_certificate(identity.boring_tls.0.as_ref())
.map_err(error::tls)?;
tls_builder
.set_private_key(identity.boring_tls.1.as_ref())
.map_err(error::tls)?;
}
};
let min_tls_version = match self.min_tls_version {
TlsVersion::Tlsv10 => SslVersion::TLS1,
TlsVersion::Tlsv11 => SslVersion::TLS1_1,
TlsVersion::Tlsv12 => SslVersion::TLS1_2,
TlsVersion::Tlsv13 => SslVersion::TLS1_3,
TlsVersion::Tlsv10 => super::boring_tls::MinTlsVersion::Tlsv10,
TlsVersion::Tlsv11 => super::boring_tls::MinTlsVersion::Tlsv11,
TlsVersion::Tlsv12 => super::boring_tls::MinTlsVersion::Tlsv12,
TlsVersion::Tlsv13 => super::boring_tls::MinTlsVersion::Tlsv13,
};
tls_builder
.set_min_proto_version(Some(min_tls_version))
.map_err(error::tls)?;
let connector = tls_builder.build();
Ok(TlsParameters {
connector: InnerTlsParameters::BoringTls {
connector,
accept_invalid_hostnames: self.accept_invalid_hostnames,
},
domain: self.domain,
})
let mut builder = super::TlsParametersBuilder::<super::BoringTls>::new(self.domain)
.certificate_store(cert_store)
.dangerous_accept_invalid_certs(self.accept_invalid_certs)
.dangerous_accept_invalid_hostnames(self.accept_invalid_hostnames)
.min_tls_version(min_tls_version);
for cert in self.root_certs {
builder = builder.add_root_certificate(cert.boring_tls);
}
if let Some(identity) = self.identity {
builder = builder.identify_with(identity.boring_tls);
}
builder
.build()
.map(super::BoringTls::__build_current_tls_parameters)
}
/// Creates a new `TlsParameters` using rustls with the provided configuration
#[cfg(feature = "rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
let just_version3 = &[&rustls::version::TLS13];
let supported_versions = match self.min_tls_version {
let cert_store = match self.cert_store {
CertificateStore::Default => super::rustls::CertificateStore::default(),
#[cfg(feature = "webpki-roots")]
CertificateStore::WebpkiRoots => super::rustls::CertificateStore::WebpkiRoots,
CertificateStore::None => super::rustls::CertificateStore::None,
};
let min_tls_version = match self.min_tls_version {
TlsVersion::Tlsv10 => {
return Err(error::tls("min tls version Tlsv10 not supported in rustls"))
}
TlsVersion::Tlsv11 => {
return Err(error::tls("min tls version Tlsv11 not supported in rustls"))
}
TlsVersion::Tlsv12 => rustls::ALL_VERSIONS,
TlsVersion::Tlsv13 => just_version3,
TlsVersion::Tlsv12 => super::rustls::MinTlsVersion::Tlsv12,
TlsVersion::Tlsv13 => super::rustls::MinTlsVersion::Tlsv13,
};
let crypto_provider = crate::rustls_crypto::crypto_provider();
let tls = ClientConfig::builder_with_provider(Arc::clone(&crypto_provider))
.with_protocol_versions(supported_versions)
.map_err(error::tls)?;
// Build TLS config
let mut root_cert_store = RootCertStore::empty();
#[cfg(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();
let errors_len = errors.len();
let (added, ignored) = store.add_parsable_certificates(certs);
#[cfg(feature = "tracing")]
tracing::debug!(
"loaded platform certs with {errors_len} failing to load, {added} valid and {ignored} ignored (invalid) certs"
);
#[cfg(not(feature = "tracing"))]
let _ = (errors_len, added, ignored);
}
#[cfg(all(feature = "rustls", feature = "webpki-roots"))]
fn load_webpki_roots(store: &mut RootCertStore) {
store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
}
#[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-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-platform-verifier"),
not(feature = "rustls-native-certs"),
feature = "webpki-roots"
))]
load_webpki_roots(&mut root_cert_store);
}
#[cfg(all(feature = "rustls", feature = "webpki-roots"))]
CertificateStore::WebpkiRoots => {
load_webpki_roots(&mut root_cert_store);
}
CertificateStore::None => {}
}
let mut builder = super::TlsParametersBuilder::<super::Rustls>::new(self.domain)
.certificate_store(cert_store)
.dangerous_accept_invalid_certs(self.accept_invalid_certs)
.dangerous_accept_invalid_hostnames(self.accept_invalid_hostnames)
.min_tls_version(min_tls_version);
for cert in self.root_certs {
for rustls_cert in cert.rustls {
#[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)?;
for cert in cert.rustls {
builder = builder.add_root_certificate(cert);
}
}
if let Some(identity) = self.identity {
builder = builder.identify_with(identity.rustls_tls);
}
let tls = if self.accept_invalid_certs
|| (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,
roots: root_cert_store,
crypto_provider,
};
tls.dangerous()
.with_custom_certificate_verifier(Arc::new(verifier))
} else {
#[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 {
let (client_certificates, private_key) = identity.rustls_tls;
tls.with_client_auth_cert(client_certificates, private_key)
.map_err(error::tls)?
} else {
tls.with_no_client_auth()
};
Ok(TlsParameters {
connector: InnerTlsParameters::Rustls {
config: Arc::new(tls),
},
domain: self.domain,
})
builder
.build()
.map(super::Rustls::__build_current_tls_parameters)
}
}
#[derive(Clone)]
#[allow(clippy::enum_variant_names)]
pub(crate) enum InnerTlsParameters {
pub(in crate::transport::smtp) enum InnerTlsParameters {
#[cfg(feature = "native-tls")]
NativeTls { connector: TlsConnector },
NativeTls(super::TlsParameters<super::NativeTls>),
#[cfg(feature = "rustls")]
Rustls { config: Arc<ClientConfig> },
Rustls(super::TlsParameters<super::Rustls>),
#[cfg(feature = "boring-tls")]
BoringTls {
connector: SslConnector,
accept_invalid_hostnames: bool,
},
BoringTls(super::TlsParameters<super::BoringTls>),
}
impl TlsParameters {
@@ -596,7 +456,7 @@ impl TlsParameters {
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn new(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build()
Self::new_with::<super::DefaultTlsBackend>(domain)
}
/// Creates a new `TlsParameters` builder
@@ -608,25 +468,38 @@ impl TlsParameters {
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
pub fn new_native(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build_native()
Self::new_with::<super::NativeTls>(domain)
}
/// Creates a new `TlsParameters` using rustls
#[cfg(feature = "rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
pub fn new_rustls(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build_rustls()
Self::new_with::<super::Rustls>(domain)
}
/// Creates a new `TlsParameters` using boring
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn new_boring(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build_boring()
Self::new_with::<super::BoringTls>(domain)
}
fn new_with<B: TlsBackend>(domain: String) -> Result<Self, Error> {
super::TlsParametersBuilder::<B>::new(domain)
.build()
.map(B::__build_current_tls_parameters)
}
pub fn domain(&self) -> &str {
&self.domain
match self.inner {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(ref inner) => &inner.server_name,
#[cfg(feature = "rustls")]
InnerTlsParameters::Rustls(ref inner) => inner.server_name.as_ref(),
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(ref inner) => &inner.server_name,
}
}
}
@@ -645,55 +518,36 @@ impl TlsParameters {
)]
pub struct Certificate {
#[cfg(feature = "native-tls")]
native_tls: native_tls::Certificate,
native_tls: super::native_tls::Certificate,
#[cfg(feature = "rustls")]
rustls: Vec<CertificateDer<'static>>,
rustls: Vec<super::rustls::Certificate>,
#[cfg(feature = "boring-tls")]
boring_tls: boring::x509::X509,
boring_tls: super::boring_tls::Certificate,
}
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
impl Certificate {
/// Create a `Certificate` from a DER encoded certificate
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
#[cfg(feature = "native-tls")]
let native_tls_cert = native_tls::Certificate::from_der(&der).map_err(error::tls)?;
#[cfg(feature = "boring-tls")]
let boring_tls_cert = boring::x509::X509::from_der(&der).map_err(error::tls)?;
Ok(Self {
#[cfg(feature = "native-tls")]
native_tls: native_tls_cert,
#[cfg(feature = "rustls")]
rustls: vec![der.into()],
native_tls: super::native_tls::Certificate::from_der(&der)?,
#[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert,
boring_tls: super::boring_tls::Certificate::from_der(&der)?,
#[cfg(feature = "rustls")]
rustls: vec![super::rustls::Certificate::from_der(der)],
})
}
/// Create a `Certificate` from a PEM encoded certificate
pub fn from_pem(pem: &[u8]) -> Result<Self, Error> {
#[cfg(feature = "native-tls")]
let native_tls_cert = native_tls::Certificate::from_pem(pem).map_err(error::tls)?;
#[cfg(feature = "boring-tls")]
let boring_tls_cert = boring::x509::X509::from_pem(pem).map_err(error::tls)?;
#[cfg(feature = "rustls")]
let rustls_cert = {
CertificateDer::pem_slice_iter(pem)
.collect::<Result<Vec<_>, pki_types::pem::Error>>()
.map_err(|_| error::tls("invalid certificates"))?
};
Ok(Self {
#[cfg(feature = "native-tls")]
native_tls: native_tls_cert,
native_tls: super::native_tls::Certificate::from_pem(pem)?,
#[cfg(feature = "rustls")]
rustls: rustls_cert,
rustls: super::rustls::Certificate::from_pem_bundle(pem)?,
#[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert,
boring_tls: super::boring_tls::Certificate::from_pem(pem)?,
})
}
}
@@ -705,6 +559,7 @@ impl Debug for Certificate {
}
/// An identity that can be used with [`TlsParametersBuilder::identify_with`]
#[derive(Clone)]
#[allow(missing_copy_implementations)]
#[cfg_attr(
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
@@ -718,11 +573,11 @@ impl Debug for Certificate {
)]
pub struct Identity {
#[cfg(feature = "native-tls")]
native_tls: native_tls::Identity,
native_tls: super::native_tls::Identity,
#[cfg(feature = "rustls")]
rustls_tls: (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>),
rustls_tls: super::rustls::Identity,
#[cfg(feature = "boring-tls")]
boring_tls: (boring::x509::X509, PKey<boring::pkey::Private>),
boring_tls: super::boring_tls::Identity,
}
impl Debug for Identity {
@@ -731,132 +586,16 @@ impl Debug for Identity {
}
}
impl Clone for Identity {
fn clone(&self) -> Self {
Identity {
#[cfg(feature = "native-tls")]
native_tls: self.native_tls.clone(),
#[cfg(feature = "rustls")]
rustls_tls: (self.rustls_tls.0.clone(), self.rustls_tls.1.clone_key()),
#[cfg(feature = "boring-tls")]
boring_tls: (self.boring_tls.0.clone(), self.boring_tls.1.clone()),
}
}
}
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
impl Identity {
pub fn from_pem(pem: &[u8], key: &[u8]) -> Result<Self, Error> {
Ok(Self {
#[cfg(feature = "native-tls")]
native_tls: Identity::from_pem_native_tls(pem, key)?,
native_tls: super::native_tls::Identity::from_pem(pem, key)?,
#[cfg(feature = "rustls")]
rustls_tls: Identity::from_pem_rustls_tls(pem, key)?,
rustls_tls: super::rustls::Identity::from_pem(pem, key)?,
#[cfg(feature = "boring-tls")]
boring_tls: Identity::from_pem_boring_tls(pem, key)?,
boring_tls: super::boring_tls::Identity::from_pem(pem, key)?,
})
}
#[cfg(feature = "native-tls")]
fn from_pem_native_tls(pem: &[u8], key: &[u8]) -> Result<native_tls::Identity, Error> {
native_tls::Identity::from_pkcs8(pem, key).map_err(error::tls)
}
#[cfg(feature = "rustls")]
fn from_pem_rustls_tls(
pem: &[u8],
key: &[u8],
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), Error> {
let key = match PrivateKeyDer::from_pem_slice(key) {
Ok(key) => key,
Err(pki_types::pem::Error::NoItemsFound) => {
return Err(error::tls("no private key found"))
}
Err(err) => return Err(error::tls(err)),
};
Ok((vec![pem.to_owned().into()], key))
}
#[cfg(feature = "boring-tls")]
fn from_pem_boring_tls(
pem: &[u8],
key: &[u8],
) -> Result<(boring::x509::X509, PKey<boring::pkey::Private>), Error> {
let cert = boring::x509::X509::from_pem(pem).map_err(error::tls)?;
let key = boring::pkey::PKey::private_key_from_pem(key).map_err(error::tls)?;
Ok((cert, key))
}
}
#[cfg(feature = "rustls")]
#[derive(Debug)]
struct InvalidCertsVerifier {
ignore_invalid_hostnames: bool,
ignore_invalid_certs: bool,
roots: RootCertStore,
crypto_provider: Arc<CryptoProvider>,
}
#[cfg(feature = "rustls")]
impl ServerCertVerifier for InvalidCertsVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>],
server_name: &ServerName<'_>,
_ocsp_response: &[u8],
now: UnixTime,
) -> Result<ServerCertVerified, TlsError> {
let cert = ParsedCertificate::try_from(end_entity)?;
if !self.ignore_invalid_certs {
rustls::client::verify_server_cert_signed_by_trust_anchor(
&cert,
&self.roots,
intermediates,
now,
self.crypto_provider.signature_verification_algorithms.all,
)?;
}
if !self.ignore_invalid_hostnames {
rustls::client::verify_server_name(&cert, server_name)?;
}
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, TlsError> {
verify_tls12_signature(
message,
cert,
dss,
&self.crypto_provider.signature_verification_algorithms,
)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, TlsError> {
verify_tls13_signature(
message,
cert,
dss,
&self.crypto_provider.signature_verification_algorithms,
)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.crypto_provider
.signature_verification_algorithms
.supported_schemes()
}
}

View File

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

View File

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

View File

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