Compare commits
5 Commits
better-tls
...
chumsky-to
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e347cf9ae2 | ||
|
|
4c738085e5 | ||
|
|
b752170bd8 | ||
|
|
b073df7666 | ||
|
|
cf6b767a9c |
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,3 +1,21 @@
|
||||
<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)
|
||||
|
||||
|
||||
105
Cargo.lock
generated
105
Cargo.lock
generated
@@ -17,18 +17,6 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@@ -38,12 +26,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
@@ -458,16 +440,6 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chumsky"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"stacker",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
@@ -1044,16 +1016,6 @@ dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.4.0"
|
||||
@@ -1325,13 +1287,12 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "lettre"
|
||||
version = "0.11.16"
|
||||
version = "0.11.17"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"boring",
|
||||
"chumsky",
|
||||
"criterion",
|
||||
"ed25519-dalek",
|
||||
"email-encoding",
|
||||
@@ -1358,7 +1319,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"socket2",
|
||||
"socket2 0.6.0",
|
||||
"tokio",
|
||||
"tokio-boring",
|
||||
"tokio-native-tls",
|
||||
@@ -1374,9 +1335,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.171"
|
||||
version = "0.2.174"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
@@ -1765,7 +1726,7 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy 0.8.24",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1820,15 +1781,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psm"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f58e5423e24c18cc840e1c98370b3993c6649cd1678b4d24318bcf0a083cbe88"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
@@ -2256,6 +2208,16 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
@@ -2278,19 +2240,6 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "stacker"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "601f9201feb9b09c00266478bf459952b9ef9a6b94edb2f21eba14ab681a60a9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"psm",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -2405,7 +2354,7 @@ dependencies = [
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"socket2 0.5.9",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
@@ -2967,33 +2916,13 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"zerocopy-derive 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879"
|
||||
dependencies = [
|
||||
"zerocopy-derive 0.8.24",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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.16"
|
||||
version = "0.11.17"
|
||||
description = "Email client"
|
||||
readme = "README.md"
|
||||
homepage = "https://lettre.rs"
|
||||
@@ -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
|
||||
@@ -40,9 +40,8 @@ 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 }
|
||||
|
||||
@@ -107,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"]
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://deps.rs/crate/lettre/0.11.16">
|
||||
<img src="https://deps.rs/crate/lettre/0.11.16/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>
|
||||
|
||||
@@ -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.16")]
|
||||
#![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)]
|
||||
|
||||
@@ -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(super) 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(super) 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(super) 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(super) 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(super) fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
atext().repeated().at_least(1).chain(
|
||||
just('.')
|
||||
.chain(atext().repeated().at_least(1))
|
||||
.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(super) 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(super) 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(super) 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(super) 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(super) 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(super) 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
|
||||
})?;
|
||||
|
||||
@@ -14,6 +14,8 @@ 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")]
|
||||
@@ -317,9 +319,11 @@ impl AsyncNetworkStream {
|
||||
tcp_stream: Box<dyn AsyncTokioStream>,
|
||||
tls_parameters: TlsParameters,
|
||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||
match tls_parameters.inner {
|
||||
let domain = tls_parameters.domain().to_owned();
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(inner) => {
|
||||
InnerTlsParameters::NativeTls { connector } => {
|
||||
#[cfg(not(feature = "tokio1-native-tls"))]
|
||||
panic!("built without the tokio1-native-tls feature");
|
||||
|
||||
@@ -327,16 +331,16 @@ impl AsyncNetworkStream {
|
||||
return {
|
||||
use tokio1_native_tls_crate::TlsConnector;
|
||||
|
||||
let connector = TlsConnector::from(inner.connector);
|
||||
let connector = TlsConnector::from(connector);
|
||||
let stream = connector
|
||||
.connect(&inner.server_name, tcp_stream)
|
||||
.connect(&domain, tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerTlsParameters::Rustls(inner) => {
|
||||
InnerTlsParameters::Rustls { config } => {
|
||||
#[cfg(not(feature = "tokio1-rustls"))]
|
||||
panic!("built without the tokio1-rustls feature");
|
||||
|
||||
@@ -344,25 +348,31 @@ impl AsyncNetworkStream {
|
||||
return {
|
||||
use tokio1_rustls::TlsConnector;
|
||||
|
||||
let connector = TlsConnector::from(inner.connector);
|
||||
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 stream = connector
|
||||
.connect(inner.server_name.inner(), tcp_stream)
|
||||
.connect(domain.to_owned(), tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio1Rustls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerTlsParameters::BoringTls(inner) => {
|
||||
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 = inner.connector.configure().map_err(error::connection)?;
|
||||
config.set_verify_hostname(inner.extra_info.accept_invalid_hostnames);
|
||||
let mut config = connector.configure().map_err(error::connection)?;
|
||||
config.set_verify_hostname(accept_invalid_hostnames);
|
||||
|
||||
let stream = tokio1_boring::connect(config, &inner.server_name, tcp_stream)
|
||||
let stream = tokio1_boring::connect(config, &domain, tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio1BoringTls(stream))
|
||||
@@ -375,15 +385,17 @@ impl AsyncNetworkStream {
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
async fn upgrade_asyncstd1_tls(
|
||||
tcp_stream: AsyncStd1TcpStream,
|
||||
tls_parameters: TlsParameters,
|
||||
mut tls_parameters: TlsParameters,
|
||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||
match tls_parameters.inner {
|
||||
let domain = mem::take(&mut tls_parameters.domain);
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(_) => {
|
||||
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::Rustls(inner) => {
|
||||
InnerTlsParameters::Rustls { config } => {
|
||||
#[cfg(not(feature = "async-std1-rustls"))]
|
||||
panic!("built without the async-std1-rustls feature");
|
||||
|
||||
@@ -391,16 +403,19 @@ impl AsyncNetworkStream {
|
||||
return {
|
||||
use futures_rustls::TlsConnector;
|
||||
|
||||
let connector = TlsConnector::from(inner.connector);
|
||||
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 stream = connector
|
||||
.connect(inner.server_name.inner(), tcp_stream)
|
||||
.connect(domain.to_owned(), tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::AsyncStd1Rustls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerTlsParameters::BoringTls(_inner) => {
|
||||
InnerTlsParameters::BoringTls { .. } => {
|
||||
panic!("boring-tls isn't supported with async-std yet.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +34,12 @@ 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::current::InnerTlsParameters;
|
||||
pub(super) use self::tls::InnerTlsParameters;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
pub use self::tls::current::TlsVersion;
|
||||
pub use self::tls::TlsVersion;
|
||||
pub use self::{
|
||||
connection::SmtpConnection,
|
||||
tls::current::{
|
||||
Certificate, CertificateStore, Identity, Tls, TlsParameters, TlsParametersBuilder,
|
||||
},
|
||||
tls::{Certificate, CertificateStore, Identity, Tls, TlsParameters, TlsParametersBuilder},
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
|
||||
@@ -12,7 +12,7 @@ use boring::ssl::SslStream;
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::TlsStream;
|
||||
#[cfg(feature = "rustls")]
|
||||
use rustls::{ClientConnection, StreamOwned};
|
||||
use rustls::{pki_types::ServerName, 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.inner {
|
||||
Ok(match &tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(inner) => {
|
||||
let stream = inner
|
||||
.connector
|
||||
.connect(&inner.server_name, tcp_stream)
|
||||
InnerTlsParameters::NativeTls { connector } => {
|
||||
let stream = connector
|
||||
.connect(tls_parameters.domain(), tcp_stream)
|
||||
.map_err(error::connection)?;
|
||||
InnerNetworkStream::NativeTls(stream)
|
||||
}
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerTlsParameters::Rustls(inner) => {
|
||||
let connection = ClientConnection::new(
|
||||
Arc::clone(&inner.connector),
|
||||
inner.server_name.inner_ref().clone(),
|
||||
)
|
||||
.map_err(error::connection)?;
|
||||
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)?;
|
||||
let stream = StreamOwned::new(connection, tcp_stream);
|
||||
InnerNetworkStream::Rustls(stream)
|
||||
}
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerTlsParameters::BoringTls(inner) => {
|
||||
let stream = inner
|
||||
.connector
|
||||
InnerTlsParameters::BoringTls {
|
||||
connector,
|
||||
accept_invalid_hostnames,
|
||||
} => {
|
||||
let stream = connector
|
||||
.configure()
|
||||
.map_err(error::connection)?
|
||||
.verify_hostname(inner.extra_info.accept_invalid_hostnames)
|
||||
.connect(&inner.server_name, tcp_stream)
|
||||
.verify_hostname(*accept_invalid_hostnames)
|
||||
.connect(tls_parameters.domain(), tcp_stream)
|
||||
.map_err(error::connection)?;
|
||||
InnerNetworkStream::BoringTls(stream)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
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;
|
||||
use crate::transport::smtp::Error;
|
||||
use crate::transport::smtp::{error, Error};
|
||||
|
||||
/// TLS protocol versions.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
@@ -176,7 +193,9 @@ pub enum CertificateStore {
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub struct TlsParameters {
|
||||
pub(in crate::transport::smtp) inner: InnerTlsParameters,
|
||||
pub(crate) connector: InnerTlsParameters,
|
||||
/// The domain name which is expected in the TLS certificate from the server
|
||||
pub(super) domain: String,
|
||||
}
|
||||
|
||||
/// Builder for `TlsParameters`
|
||||
@@ -316,20 +335,30 @@ impl TlsParametersBuilder {
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
pub fn build_native(self) -> Result<TlsParameters, Error> {
|
||||
let cert_store = match self.cert_store {
|
||||
CertificateStore::Default => super::native_tls::CertificateStore::System,
|
||||
CertificateStore::None => super::native_tls::CertificateStore::None,
|
||||
let mut tls_builder = TlsConnector::builder();
|
||||
|
||||
match self.cert_store {
|
||||
CertificateStore::Default => {}
|
||||
CertificateStore::None => {
|
||||
tls_builder.disable_built_in_roots(true);
|
||||
}
|
||||
#[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 => super::native_tls::MinTlsVersion::Tlsv10,
|
||||
TlsVersion::Tlsv11 => super::native_tls::MinTlsVersion::Tlsv11,
|
||||
TlsVersion::Tlsv12 => super::native_tls::MinTlsVersion::Tlsv12,
|
||||
TlsVersion::Tlsv10 => Protocol::Tlsv10,
|
||||
TlsVersion::Tlsv11 => Protocol::Tlsv11,
|
||||
TlsVersion::Tlsv12 => Protocol::Tlsv12,
|
||||
TlsVersion::Tlsv13 => {
|
||||
return Err(error::tls(
|
||||
"min tls version Tlsv13 not supported in native tls",
|
||||
@@ -337,114 +366,225 @@ impl TlsParametersBuilder {
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
tls_builder.min_protocol_version(Some(min_tls_version));
|
||||
if let Some(identity) = self.identity {
|
||||
builder = builder.identify_with(identity.native_tls);
|
||||
tls_builder.identity(identity.native_tls);
|
||||
}
|
||||
|
||||
builder
|
||||
.build()
|
||||
.map(super::NativeTls::__build_current_tls_parameters)
|
||||
let connector = tls_builder.build().map_err(error::tls)?;
|
||||
Ok(TlsParameters {
|
||||
connector: InnerTlsParameters::NativeTls { connector },
|
||||
domain: self.domain,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
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"
|
||||
)))
|
||||
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 min_tls_version = match self.min_tls_version {
|
||||
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,
|
||||
};
|
||||
|
||||
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);
|
||||
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 {
|
||||
builder = builder.identify_with(identity.boring_tls);
|
||||
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)?;
|
||||
}
|
||||
|
||||
builder
|
||||
.build()
|
||||
.map(super::BoringTls::__build_current_tls_parameters)
|
||||
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,
|
||||
};
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 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 {
|
||||
let just_version3 = &[&rustls::version::TLS13];
|
||||
let supported_versions = 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 => super::rustls::MinTlsVersion::Tlsv12,
|
||||
TlsVersion::Tlsv13 => super::rustls::MinTlsVersion::Tlsv13,
|
||||
TlsVersion::Tlsv12 => rustls::ALL_VERSIONS,
|
||||
TlsVersion::Tlsv13 => just_version3,
|
||||
};
|
||||
|
||||
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 cert in cert.rustls {
|
||||
builder = builder.add_root_certificate(cert);
|
||||
}
|
||||
}
|
||||
if let Some(identity) = self.identity {
|
||||
builder = builder.identify_with(identity.rustls_tls);
|
||||
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);
|
||||
}
|
||||
|
||||
builder
|
||||
.build()
|
||||
.map(super::Rustls::__build_current_tls_parameters)
|
||||
#[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 => {}
|
||||
}
|
||||
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
|
||||
|| (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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub(in crate::transport::smtp) enum InnerTlsParameters {
|
||||
pub(crate) enum InnerTlsParameters {
|
||||
#[cfg(feature = "native-tls")]
|
||||
NativeTls(super::TlsParameters<super::NativeTls>),
|
||||
NativeTls { connector: TlsConnector },
|
||||
#[cfg(feature = "rustls")]
|
||||
Rustls(super::TlsParameters<super::Rustls>),
|
||||
Rustls { config: Arc<ClientConfig> },
|
||||
#[cfg(feature = "boring-tls")]
|
||||
BoringTls(super::TlsParameters<super::BoringTls>),
|
||||
BoringTls {
|
||||
connector: SslConnector,
|
||||
accept_invalid_hostnames: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl TlsParameters {
|
||||
@@ -456,7 +596,7 @@ impl TlsParameters {
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn new(domain: String) -> Result<Self, Error> {
|
||||
Self::new_with::<super::DefaultTlsBackend>(domain)
|
||||
TlsParametersBuilder::new(domain).build()
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` builder
|
||||
@@ -468,38 +608,25 @@ impl TlsParameters {
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
pub fn new_native(domain: String) -> Result<Self, Error> {
|
||||
Self::new_with::<super::NativeTls>(domain)
|
||||
TlsParametersBuilder::new(domain).build_native()
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
Self::new_with::<super::Rustls>(domain)
|
||||
TlsParametersBuilder::new(domain).build_rustls()
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
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)
|
||||
TlsParametersBuilder::new(domain).build_boring()
|
||||
}
|
||||
|
||||
pub fn domain(&self) -> &str {
|
||||
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,
|
||||
}
|
||||
&self.domain
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,36 +645,55 @@ impl TlsParameters {
|
||||
)]
|
||||
pub struct Certificate {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: super::native_tls::Certificate,
|
||||
native_tls: native_tls::Certificate,
|
||||
#[cfg(feature = "rustls")]
|
||||
rustls: Vec<super::rustls::Certificate>,
|
||||
rustls: Vec<CertificateDer<'static>>,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: super::boring_tls::Certificate,
|
||||
boring_tls: boring::x509::X509,
|
||||
}
|
||||
|
||||
#[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: super::native_tls::Certificate::from_der(&der)?,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: super::boring_tls::Certificate::from_der(&der)?,
|
||||
native_tls: native_tls_cert,
|
||||
#[cfg(feature = "rustls")]
|
||||
rustls: vec![super::rustls::Certificate::from_der(der)],
|
||||
rustls: vec![der.into()],
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: boring_tls_cert,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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: super::native_tls::Certificate::from_pem(pem)?,
|
||||
native_tls: native_tls_cert,
|
||||
#[cfg(feature = "rustls")]
|
||||
rustls: super::rustls::Certificate::from_pem_bundle(pem)?,
|
||||
rustls: rustls_cert,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: super::boring_tls::Certificate::from_pem(pem)?,
|
||||
boring_tls: boring_tls_cert,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -559,7 +705,6 @@ 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")),
|
||||
@@ -573,11 +718,11 @@ impl Debug for Certificate {
|
||||
)]
|
||||
pub struct Identity {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: super::native_tls::Identity,
|
||||
native_tls: native_tls::Identity,
|
||||
#[cfg(feature = "rustls")]
|
||||
rustls_tls: super::rustls::Identity,
|
||||
rustls_tls: (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: super::boring_tls::Identity,
|
||||
boring_tls: (boring::x509::X509, PKey<boring::pkey::Private>),
|
||||
}
|
||||
|
||||
impl Debug for Identity {
|
||||
@@ -586,16 +731,132 @@ 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: super::native_tls::Identity::from_pem(pem, key)?,
|
||||
native_tls: Identity::from_pem_native_tls(pem, key)?,
|
||||
#[cfg(feature = "rustls")]
|
||||
rustls_tls: super::rustls::Identity::from_pem(pem, key)?,
|
||||
rustls_tls: Identity::from_pem_rustls_tls(pem, key)?,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: super::boring_tls::Identity::from_pem(pem, key)?,
|
||||
boring_tls: Identity::from_pem_boring_tls(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()
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user