Compare commits

...

21 Commits

Author SHA1 Message Date
Paolo Barbolini
51794aa912 Prepare v0.11.10 (#1002) 2024-10-23 23:04:45 +02:00
Paolo Barbolini
eb42651401 Use case insensitive comparisons for login challenge requests (#1000) 2024-10-23 20:01:00 +02:00
Paolo Barbolini
99c6dc2a87 Replace quit with abort in transport connection drop code (#999) 2024-10-23 20:00:31 +02:00
Paolo Barbolini
b6babbce00 Prepare v0.11.9 (#991) 2024-09-13 15:48:32 +02:00
Paolo Barbolini
c9895c52de readme: add fn main to getting started example (#990) 2024-09-13 15:41:46 +02:00
Paolo Barbolini
575492b9ed Bump rustls-native-certs to v0.8 (#992) 2024-09-13 15:41:18 +02:00
Paolo Barbolini
ad665cd01e chore: bump locked dependencies (#989)
And then downgrades:

cargo update -p clap --precise 4.3.24
cargo update -p clap_lex --precise 0.5.0
cargo update -p anstyle --precise 1.0.2
2024-09-13 02:37:04 +02:00
Arnaud de Bossoreille
e2ac5dadfb Fix parsing Mailbox with spaces (#986) 2024-09-12 17:26:43 +02:00
Max Breitenfeldt
1c6a348eb8 Enable accept_invalid_hostnames for rustls (#988)
With #977 dangerous_accept_invalid_hostnames is implemented for rustls. This add the config flag so that the feature can actually be used when rustls-tls is enabled.
2024-09-10 09:58:48 +02:00
Paolo Barbolini
e8b2498ad7 Prepare v0.11.8 (#985) 2024-09-03 15:43:04 +02:00
Paolo Barbolini
bf48bd6b96 ci: bump dependencies (#984) 2024-09-02 17:37:44 +02:00
André Cruz
fa6191983a feat(tls-peer-certificates): Provide peer certs (#976)
Add a method that, when using a TLS toolkit that supports it, returns
the entire certificate chain. This is useful, for example, when
implementing DANE support which has directives that apply to the issuer
and not just to the leaf certificate.

Not all TLS toolkits support this, so the code will panic if the method
is called when using a TLS toolkit that has no way to return these
certificates.
2024-09-02 17:26:46 +02:00
Paolo Barbolini
ca405040ae chore: replace manual impl of #[non_exhaustive] for InvalidHeaderName (#981) 2024-08-29 05:43:23 +02:00
Paolo Barbolini
f7a1b790df Make HeaderName comparisons case insensitive (#980) 2024-08-29 05:43:14 +02:00
Paolo Barbolini
caff354cbf chore: bump vulnerable dependencies 2024-08-23 09:28:21 +02:00
Paolo Barbolini
a81401c4cb Fix latest clippy warnings (#979) 2024-08-23 09:24:05 +02:00
Jonas Osburg
54df594d6c Implement accept_invalid_hostnames for rustls (#977)
Fixes #957

Co-authored-by: Paolo Barbolini <paolo.barbolini@m4ss.net>
2024-08-21 10:29:10 +02:00
Felix Rodemund
cada01d039 Add mTLS Support (#974)
This adds support for mutual authentication to transport layer secured
connections used to deliver mails.

Client authentication requires the client certificate and the
corresponding private key in pem format to be passed to
Identity::from_pem. The resulting Identity needs then to be provided to
TlsParametersBuilder::identify_with.
2024-07-30 21:28:34 +02:00
Paolo Barbolini
0132bee59d Bump idna to v1 (#966) 2024-06-11 18:12:49 +02:00
Paolo Barbolini
acdf189717 Fix clippy warnings (#967) 2024-06-11 18:12:32 +02:00
Ilka Schulz
3aea65315f resolve #806: forbid empty forward path when deserializing address::Envelop (#964) 2024-06-11 09:48:30 +00:00
21 changed files with 1212 additions and 822 deletions

View File

@@ -13,16 +13,16 @@ env:
jobs:
rustfmt:
name: rustfmt / nightly-2023-06-22
name: rustfmt / nightly-2024-09-01
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install rust
run: |
rustup default nightly-2023-06-22
rustup default nightly-2024-09-01
rustup component add rustfmt
- name: cargo fmt
@@ -34,7 +34,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install rust
run: |
@@ -50,7 +50,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install rust
run: rustup update --no-self-update stable
@@ -80,7 +80,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install rust
run: |
@@ -112,12 +112,6 @@ jobs:
- name: Install dkimverify
run: sudo apt -y install python3-dkim
- name: Work around early dependencies MSRV bump
run: |
cargo update -p anstyle --precise 1.0.2
cargo update -p clap --precise 4.3.24
cargo update -p clap_lex --precise 0.5.0
- name: Test with no default features
run: cargo test --no-default-features
@@ -134,7 +128,7 @@ jobs:
# name: Coverage
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v2
# - uses: actions/checkout@v4
# - uses: actions-rs/toolchain@v1
# with:
# toolchain: nightly

View File

@@ -1,3 +1,56 @@
<a name="v0.11.10"></a>
### v0.11.10 (2024-10-23)
#### Bug fixes
* Ignore disconnect errors when `pool` feature of SMTP transport is disabled ([#999])
* Use case insensitive comparisons for matching login challenge requests ([#1000])
[#999]: https://github.com/lettre/lettre/pull/999
[#1000]: https://github.com/lettre/lettre/pull/1000
<a name="v0.11.9"></a>
### v0.11.9 (2024-09-13)
#### Bug fixes
* Fix feature gate for `accept_invalid_hostnames` for rustls ([#988])
* Fix parsing `Mailbox` with trailing spaces ([#986])
#### Misc
* Bump `rustls-native-certs` to v0.8 ([#992])
* Make getting started example in readme complete ([#990])
[#988]: https://github.com/lettre/lettre/pull/988
[#986]: https://github.com/lettre/lettre/pull/986
[#990]: https://github.com/lettre/lettre/pull/990
[#992]: https://github.com/lettre/lettre/pull/992
<a name="v0.11.8"></a>
### v0.11.8 (2024-09-03)
#### Features
* Add mTLS support ([#974])
* Implement `accept_invalid_hostnames` for rustls ([#977])
* Provide certificate chain for peer certificates when using `rustls` or `boring-tls` ([#976])
#### Changes
* Make `HeaderName` comparisons via `PartialEq` case insensitive ([#980])
#### Misc
* Fix clippy warnings ([#979])
* Replace manual impl of `#[non_exhaustive]` for `InvalidHeaderName` ([#981])
[#974]: https://github.com/lettre/lettre/pull/974
[#976]: https://github.com/lettre/lettre/pull/976
[#977]: https://github.com/lettre/lettre/pull/977
[#980]: https://github.com/lettre/lettre/pull/980
[#981]: https://github.com/lettre/lettre/pull/981
<a name="v0.11.7"></a>
### v0.11.7 (2024-04-23)

1282
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.7"
version = "0.11.10"
description = "Email client"
readme = "README.md"
homepage = "https://lettre.rs"
@@ -20,7 +20,7 @@ maintenance = { status = "actively-developed" }
[dependencies]
chumsky = "0.9"
idna = "0.5"
idna = "1"
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
# builder
@@ -47,7 +47,8 @@ percent-encoding = { version = "2.3", optional = true }
native-tls = { version = "0.2.5", optional = true } # feature
rustls = { version = "0.23.5", default-features = false, features = ["ring", "logging", "std", "tls12"], optional = true }
rustls-pemfile = { version = "2", optional = true }
rustls-native-certs = { version = "0.7", optional = true }
rustls-native-certs = { version = "0.8", optional = true }
rustls-pki-types = { version = "1.7", optional = true }
webpki-roots = { version = "0.26", optional = true }
boring = { version = "4", optional = true }
@@ -58,7 +59,6 @@ async-trait = { version = "0.1", optional = true }
## async-std
async-std = { version = "1.8", optional = true }
#async-native-tls = { version = "0.3.3", optional = true }
futures-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12", "ring"], optional = true }
## tokio
@@ -108,13 +108,12 @@ smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:url", "dep:percen
pool = ["dep:futures-util"]
rustls-tls = ["dep:webpki-roots", "dep:rustls", "dep:rustls-pemfile"]
rustls-tls = ["dep:webpki-roots", "dep:rustls", "dep:rustls-pemfile", "dep:rustls-pki-types"]
boring-tls = ["dep:boring"]
# async
async-std1 = ["dep:async-std", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
#async-std1-native-tls = ["async-std1", "native-tls", "dep:async-native-tls"]
async-std1-rustls-tls = ["async-std1", "rustls-tls", "dep:futures-rustls"]
tokio1 = ["dep:tokio1_crate", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
tokio1-native-tls = ["tokio1", "native-tls", "dep:tokio1_native_tls_crate"]
@@ -123,6 +122,9 @@ tokio1-boring-tls = ["tokio1", "boring-tls", "dep:tokio1_boring"]
dkim = ["dep:base64", "dep:sha2", "dep:rsa", "dep:ed25519-dalek"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(lettre_ignore_tls_mismatch)'] }
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"]

View File

@@ -28,8 +28,8 @@
</div>
<div align="center">
<a href="https://deps.rs/crate/lettre/0.11.7">
<img src="https://deps.rs/crate/lettre/0.11.7/status.svg"
<a href="https://deps.rs/crate/lettre/0.11.10">
<img src="https://deps.rs/crate/lettre/0.11.10/status.svg"
alt="dependency status" />
</a>
</div>
@@ -71,7 +71,8 @@ use lettre::message::header::ContentType;
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
let email = Message::builder()
fn main() {
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
@@ -80,18 +81,19 @@ let email = Message::builder()
.body(String::from("Be happy!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com")
// Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(&email) {
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {e:?}"),
}
}
```

View File

@@ -14,11 +14,71 @@ pub struct Envelope {
/// The envelope recipient's addresses
///
/// This can not be empty.
#[cfg_attr(
feature = "serde",
serde(deserialize_with = "serde_forward_path::deserialize")
)]
forward_path: Vec<Address>,
/// The envelope sender address
reverse_path: Option<Address>,
}
/// just like the default implementation to deserialize `Vec<Address>` but it
/// forbids **de**serializing empty lists
#[cfg(feature = "serde")]
mod serde_forward_path {
use super::Address;
/// dummy type required for serde
/// see example: https://serde.rs/deserialize-map.html
struct CustomVisitor;
impl<'de> serde::de::Visitor<'de> for CustomVisitor {
type Value = Vec<Address>;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a non-empty list of recipient addresses")
}
fn visit_seq<S>(self, mut access: S) -> Result<Self::Value, S::Error>
where
S: serde::de::SeqAccess<'de>,
{
let mut seq: Vec<Address> = Vec::with_capacity(access.size_hint().unwrap_or(0));
while let Some(key) = access.next_element()? {
seq.push(key);
}
if seq.is_empty() {
Err(serde::de::Error::invalid_length(seq.len(), &self))
} else {
Ok(seq)
}
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Address>, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_seq(CustomVisitor {})
}
#[cfg(test)]
mod tests {
#[test]
fn deserializing_empty_recipient_list_returns_error() {
assert!(
serde_json::from_str::<crate::address::Envelope>(r#"{"forward_path": []}"#)
.is_err()
);
}
#[test]
fn deserializing_non_empty_recipient_list_is_ok() {
serde_json::from_str::<crate::address::Envelope>(
r#"{ "forward_path": [ {"user":"foo", "domain":"example.com"} ] }"#,
)
.unwrap();
}
}
}
impl Envelope {
/// Creates a new envelope, which may fail if `to` is empty.
///

View File

@@ -230,7 +230,7 @@ impl Executor for AsyncStd1Executor {
) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match tls {
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
#[cfg(feature = "async-std1-rustls-tls")]
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
_ => None,
};
@@ -243,7 +243,7 @@ impl Executor for AsyncStd1Executor {
)
.await?;
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
#[cfg(feature = "async-std1-rustls-tls")]
match tls {
Tls::Opportunistic(tls_parameters) => {
if conn.can_starttls() {

View File

@@ -109,7 +109,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.7")]
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.10")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)]
@@ -174,21 +174,7 @@ mod compiletime_checks {
If you'd like to use `boring-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
/*
#[cfg(all(
feature = "async-std1",
feature = "native-tls",
not(feature = "async-std1-native-tls")
))]
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the `async-std1-native-tls` feature hasn't been turned on.
If you'd like to use rustls make sure that the `native-tls` hasn't been enabled by mistake (you may need to import lettre without default features)
If you're building a library which depends on lettre import it without default features and enable just the features you need.");
*/
#[cfg(all(
feature = "async-std1",
feature = "native-tls",
not(feature = "async-std1-native-tls")
))]
#[cfg(all(feature = "async-std1", feature = "native-tls",))]
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the async-std integration doesn't support native-tls yet.
If you'd like to work on the issue please take a look at https://github.com/lettre/lettre/issues/576.
If you were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.

View File

@@ -124,22 +124,18 @@ impl Headers {
}
pub(crate) fn find_header(&self, name: &str) -> Option<&HeaderValue> {
self.headers
.iter()
.find(|value| name.eq_ignore_ascii_case(&value.name))
self.headers.iter().find(|value| name == value.name)
}
fn find_header_mut(&mut self, name: &str) -> Option<&mut HeaderValue> {
self.headers
.iter_mut()
.find(|value| name.eq_ignore_ascii_case(&value.name))
self.headers.iter_mut().find(|value| name == value.name)
}
fn find_header_index(&self, name: &str) -> Option<usize> {
self.headers
.iter()
.enumerate()
.find(|(_i, value)| name.eq_ignore_ascii_case(&value.name))
.find(|(_i, value)| name == value.name)
.map(|(i, _)| i)
}
}
@@ -161,18 +157,9 @@ impl Display for Headers {
/// A possible error when converting a `HeaderName` from another type.
// comes from `http` crate
#[allow(missing_copy_implementations)]
#[derive(Clone)]
pub struct InvalidHeaderName {
_priv: (),
}
impl fmt::Debug for InvalidHeaderName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InvalidHeaderName")
// skip _priv noise
.finish()
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct InvalidHeaderName;
impl fmt::Display for InvalidHeaderName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -189,14 +176,11 @@ pub struct HeaderName(Cow<'static, str>);
impl HeaderName {
/// Creates a new header name
pub fn new_from_ascii(ascii: String) -> Result<Self, InvalidHeaderName> {
if !ascii.is_empty()
&& ascii.len() <= 76
&& ascii.is_ascii()
&& !ascii.contains(|c| c == ':' || c == ' ')
if !ascii.is_empty() && ascii.len() <= 76 && ascii.is_ascii() && !ascii.contains([':', ' '])
{
Ok(Self(Cow::Owned(ascii)))
} else {
Err(InvalidHeaderName { _priv: () })
Err(InvalidHeaderName)
}
}
@@ -257,23 +241,19 @@ impl AsRef<str> for HeaderName {
impl PartialEq<HeaderName> for HeaderName {
fn eq(&self, other: &HeaderName) -> bool {
let s1: &str = self.as_ref();
let s2: &str = other.as_ref();
s1 == s2
self.eq_ignore_ascii_case(other)
}
}
impl PartialEq<&str> for HeaderName {
fn eq(&self, other: &&str) -> bool {
let s: &str = self.as_ref();
s == *other
self.eq_ignore_ascii_case(other)
}
}
impl PartialEq<HeaderName> for &str {
fn eq(&self, other: &HeaderName) -> bool {
let s: &str = other.as_ref();
*self == s
self.eq_ignore_ascii_case(other)
}
}
@@ -467,6 +447,60 @@ mod tests {
let _ = HeaderName::new_from_ascii_str("");
}
#[test]
fn headername_headername_eq() {
assert_eq!(
HeaderName::new_from_ascii_str("From"),
HeaderName::new_from_ascii_str("From")
);
}
#[test]
fn headername_str_eq() {
assert_eq!(HeaderName::new_from_ascii_str("From"), "From");
}
#[test]
fn str_headername_eq() {
assert_eq!("From", HeaderName::new_from_ascii_str("From"));
}
#[test]
fn headername_headername_eq_case_insensitive() {
assert_eq!(
HeaderName::new_from_ascii_str("From"),
HeaderName::new_from_ascii_str("from")
);
}
#[test]
fn headername_str_eq_case_insensitive() {
assert_eq!(HeaderName::new_from_ascii_str("From"), "from");
}
#[test]
fn str_headername_eq_case_insensitive() {
assert_eq!("from", HeaderName::new_from_ascii_str("From"));
}
#[test]
fn headername_headername_ne() {
assert_ne!(
HeaderName::new_from_ascii_str("From"),
HeaderName::new_from_ascii_str("To")
);
}
#[test]
fn headername_str_ne() {
assert_ne!(HeaderName::new_from_ascii_str("From"), "To");
}
#[test]
fn str_headername_ne() {
assert_ne!("From", HeaderName::new_from_ascii_str("To"));
}
// names taken randomly from https://it.wikipedia.org/wiki/Pinco_Pallino
#[test]

View File

@@ -170,7 +170,9 @@ fn phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
// 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)))).then_ignore(end())
choice((name_addr(), addr_spec().map(|addr| (None, addr))))
.padded()
.then_ignore(end())
}
// name-addr = [display-name] angle-addr

View File

@@ -556,6 +556,14 @@ mod test {
);
}
#[test]
fn parse_address_only_trim() {
assert_eq!(
" kayo@example.com ".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}
#[test]
fn parse_address_with_name() {
assert_eq!(
@@ -567,6 +575,17 @@ mod test {
);
}
#[test]
fn parse_address_with_name_trim() {
assert_eq!(
" K. <kayo@example.com> ".parse(),
Ok(Mailbox::new(
Some("K.".into()),
"kayo@example.com".parse().unwrap()
))
);
}
#[test]
fn parse_address_with_empty_name() {
assert_eq!(
@@ -578,7 +597,7 @@ mod test {
#[test]
fn parse_address_with_empty_name_trim() {
assert_eq!(
" <kayo@example.com>".parse(),
" <kayo@example.com> ".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}

View File

@@ -45,7 +45,7 @@ impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
let result = conn.send(envelope, email).await?;
#[cfg(not(feature = "pool"))]
conn.quit().await?;
conn.abort().await;
Ok(result)
}
@@ -82,7 +82,6 @@ where
#[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
#[cfg_attr(
@@ -117,7 +116,6 @@ where
#[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
#[cfg_attr(
@@ -353,7 +351,6 @@ impl AsyncSmtpTransportBuilder {
#[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
#[cfg_attr(

View File

@@ -98,13 +98,17 @@ impl Mechanism {
let decoded_challenge = challenge
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
if ["User Name", "Username:", "Username", "User Name\0"]
.contains(&decoded_challenge)
{
if contains_ignore_ascii_case(
decoded_challenge,
["User Name", "Username:", "Username", "User Name\0"],
) {
return Ok(credentials.authentication_identity.clone());
}
if ["Password", "Password:", "Password\0"].contains(&decoded_challenge) {
if contains_ignore_ascii_case(
decoded_challenge,
["Password", "Password:", "Password\0"],
) {
return Ok(credentials.secret.clone());
}
@@ -121,6 +125,15 @@ impl Mechanism {
}
}
fn contains_ignore_ascii_case<'a>(
haystack: &str,
needles: impl IntoIterator<Item = &'a str>,
) -> bool {
needles
.into_iter()
.any(|item| item.eq_ignore_ascii_case(haystack))
}
#[cfg(test)]
mod test {
use super::{Credentials, Mechanism};
@@ -155,6 +168,23 @@ mod test {
assert!(mechanism.response(&credentials, None).is_err());
}
#[test]
fn test_login_case_insensitive() {
let mechanism = Mechanism::Login;
let credentials = Credentials::new("alice".to_owned(), "wonderland".to_owned());
assert_eq!(
mechanism.response(&credentials, Some("username")).unwrap(),
"alice"
);
assert_eq!(
mechanism.response(&credentials, Some("password")).unwrap(),
"wonderland"
);
assert!(mechanism.response(&credentials, None).is_err());
}
#[test]
fn test_xoauth2() {
let mechanism = Mechanism::Xoauth2;

View File

@@ -373,4 +373,10 @@ impl AsyncSmtpConnection {
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate()
}
/// All the X509 certificates of the chain (DER encoded)
#[cfg(any(feature = "rustls-tls", feature = "boring-tls"))]
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
self.stream.get_ref().certificate_chain()
}
}

View File

@@ -6,8 +6,6 @@ use std::{
time::Duration,
};
#[cfg(feature = "async-std1-native-tls")]
use async_native_tls::TlsStream as AsyncStd1TlsStream;
#[cfg(feature = "async-std1")]
use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs};
use futures_io::{
@@ -36,7 +34,6 @@ use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
use super::InnerTlsParameters;
@@ -86,9 +83,6 @@ enum InnerAsyncNetworkStream {
#[cfg(feature = "async-std1")]
AsyncStd1Tcp(AsyncStd1TcpStream),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "async-std1-native-tls")]
AsyncStd1NativeTls(AsyncStd1TlsStream<AsyncStd1TcpStream>),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "async-std1-rustls-tls")]
AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>),
/// Can't be built
@@ -119,8 +113,6 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1BoringTls(s) => s.get_ref().peer_addr(),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => s.peer_addr(),
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => s.get_ref().peer_addr(),
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => s.get_ref().0.peer_addr(),
InnerAsyncNetworkStream::None => {
@@ -288,16 +280,13 @@ impl AsyncNetworkStream {
.map_err(error::connection)?;
Ok(())
}
#[cfg(all(
feature = "async-std1",
not(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))
))]
#[cfg(all(feature = "async-std1", not(feature = "async-std1-rustls-tls")))]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the async-std1-native-tls or the async-std1-rustls-tls feature");
panic!("Trying to upgrade an AsyncNetworkStream without having enabled the async-std1-rustls-tls feature");
}
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
// get owned TcpStream
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
@@ -385,11 +374,7 @@ impl AsyncNetworkStream {
}
#[allow(unused_variables)]
#[cfg(any(
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls",
feature = "async-std1-boring-tls"
))]
#[cfg(feature = "async-std1-rustls-tls")]
async fn upgrade_asyncstd1_tls(
tcp_stream: AsyncStd1TcpStream,
mut tls_parameters: TlsParameters,
@@ -400,22 +385,6 @@ impl AsyncNetworkStream {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
panic!("native-tls isn't supported with async-std yet. See https://github.com/lettre/lettre/pull/531#issuecomment-757893531");
/*
#[cfg(not(feature = "async-std1-native-tls"))]
panic!("built without the async-std1-native-tls feature");
#[cfg(feature = "async-std1-native-tls")]
return {
use async_native_tls::TlsConnector;
// TODO: fix
let connector: TlsConnector = todo!();
// let connector = TlsConnector::from(connector);
let stream = connector.connect(&domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::AsyncStd1NativeTls(stream))
};
*/
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(config) => {
@@ -456,14 +425,54 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1BoringTls(_) => true,
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false,
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(_) => true,
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => true,
InnerAsyncNetworkStream::None => false,
}
}
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
match &self.inner {
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(_) => panic!("Unsupported"),
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
.unwrap()
.iter()
.map(|c| c.to_vec())
.collect()),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
.ssl()
.peer_cert_chain()
.unwrap()
.iter()
.map(|c| c.to_der().map_err(error::tls))
.collect::<Result<Vec<_>, _>>()?),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
.unwrap()
.iter()
.map(|c| c.to_vec())
.collect()),
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
match &self.inner {
#[cfg(feature = "tokio1")]
@@ -498,8 +507,6 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(t) => panic!("Unsupported"),
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream
.get_ref()
@@ -559,8 +566,6 @@ impl FuturesAsyncRead for AsyncNetworkStream {
}
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_read(cx, buf),
InnerAsyncNetworkStream::None => {
@@ -588,8 +593,6 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_write(cx, buf),
InnerAsyncNetworkStream::None => {
@@ -611,8 +614,6 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_flush(cx),
InnerAsyncNetworkStream::None => {
@@ -634,8 +635,6 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_close(cx),
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_close(cx),
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_close(cx),
InnerAsyncNetworkStream::None => {

View File

@@ -307,4 +307,10 @@ impl SmtpConnection {
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate()
}
/// All the X509 certificates of the chain (DER encoded)
#[cfg(any(feature = "rustls-tls", feature = "boring-tls"))]
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
self.stream.get_ref().certificate_chain()
}
}

View File

@@ -38,7 +38,7 @@ pub(super) use self::tls::InnerTlsParameters;
pub use self::tls::TlsVersion;
pub use self::{
connection::SmtpConnection,
tls::{Certificate, CertificateStore, Tls, TlsParameters, TlsParametersBuilder},
tls::{Certificate, CertificateStore, Identity, Tls, TlsParameters, TlsParametersBuilder},
};
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
@@ -139,7 +139,7 @@ mod test {
}
#[test]
#[cfg(feature = "log")]
#[cfg(feature = "tracing")]
fn test_escape_crlf() {
assert_eq!(escape_crlf("\r\n"), "<CRLF>");
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>");

View File

@@ -223,6 +223,32 @@ impl NetworkStream {
}
}
#[cfg(any(feature = "rustls-tls", feature = "boring-tls"))]
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
match &self.inner {
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(_) => panic!("Unsupported"),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(stream) => Ok(stream
.conn
.peer_certificates()
.unwrap()
.iter()
.map(|c| c.to_vec())
.collect()),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => Ok(stream
.ssl()
.peer_cert_chain()
.unwrap()
.iter()
.map(|c| c.to_der().map_err(error::tls))
.collect::<Result<Vec<_>, _>>()?),
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
match &self.inner {

View File

@@ -4,6 +4,7 @@ use std::{io, sync::Arc};
#[cfg(feature = "boring-tls")]
use boring::{
pkey::PKey,
ssl::{SslConnector, SslVersion},
x509::store::X509StoreBuilder,
};
@@ -12,8 +13,10 @@ use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls-tls")]
use rustls::{
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
crypto::WebPkiSupportedAlgorithms,
crypto::{verify_tls12_signature, verify_tls13_signature},
pki_types::{CertificateDer, ServerName, UnixTime},
pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime},
server::ParsedCertificate,
ClientConfig, DigitallySignedStruct, Error as TlsError, RootCertStore, SignatureScheme,
};
@@ -108,7 +111,7 @@ pub enum CertificateStore {
/// For native-tls, this will use the system certificate store on Windows, the keychain on
/// macOS, and OpenSSL directories on Linux (usually `/etc/ssl`).
///
/// For rustls, this will also use the the system store if the `rustls-native-certs` feature is
/// For rustls, this will also use the system store if the `rustls-native-certs` feature is
/// enabled, or will fall back to `webpki-roots`.
///
/// The boring-tls backend uses the same logic as OpenSSL on all platforms.
@@ -139,6 +142,7 @@ pub struct TlsParametersBuilder {
domain: String,
cert_store: CertificateStore,
root_certs: Vec<Certificate>,
identity: Option<Identity>,
accept_invalid_hostnames: bool,
accept_invalid_certs: bool,
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
@@ -152,6 +156,7 @@ impl TlsParametersBuilder {
domain,
cert_store: CertificateStore::Default,
root_certs: Vec::new(),
identity: None,
accept_invalid_hostnames: false,
accept_invalid_certs: false,
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
@@ -167,12 +172,20 @@ impl TlsParametersBuilder {
/// Add a custom root certificate
///
/// Can be used to safely connect to a server using a self signed certificate, for example.
/// Can be used to safely connect to a server using a self-signed certificate, for example.
pub fn add_root_certificate(mut self, cert: Certificate) -> Self {
self.root_certs.push(cert);
self
}
/// Add a client certificate
///
/// Can be used to configure a client certificate to present to the server.
pub fn identify_with(mut self, identity: Identity) -> Self {
self.identity = Some(identity);
self
}
/// Controls whether certificates with an invalid hostname are accepted
///
/// Defaults to `false`.
@@ -184,10 +197,11 @@ impl TlsParametersBuilder {
/// including those from other sites, are trusted.
///
/// This method introduces significant vulnerabilities to man-in-the-middle attacks.
///
/// Hostname verification can only be disabled with the `native-tls` TLS backend.
#[cfg(any(feature = "native-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "boring-tls"))))]
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
self.accept_invalid_hostnames = accept_invalid_hostnames;
self
@@ -275,6 +289,10 @@ impl TlsParametersBuilder {
};
tls_builder.min_protocol_version(Some(min_tls_version));
if let Some(identity) = self.identity {
tls_builder.identity(identity.native_tls);
}
let connector = tls_builder.build().map_err(error::tls)?;
Ok(TlsParameters {
connector: InnerTlsParameters::NativeTls(connector),
@@ -317,6 +335,15 @@ impl TlsParametersBuilder {
}
}
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,
@@ -352,20 +379,25 @@ impl TlsParametersBuilder {
};
let tls = ClientConfig::builder_with_protocol_versions(supported_versions);
let provider = rustls::crypto::CryptoProvider::get_default()
.cloned()
.unwrap_or_else(|| Arc::new(rustls::crypto::ring::default_provider()));
// Build TLS config
let signature_algorithms = provider.signature_verification_algorithms;
let tls = if self.accept_invalid_certs {
tls.dangerous()
.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
} else {
let mut root_cert_store = RootCertStore::empty();
#[cfg(feature = "rustls-native-certs")]
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?;
let (added, ignored) = store.add_parsable_certificates(native_certs);
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 {added} valid and {ignored} ignored (invalid) certs"
"loaded platform certs with {errors_len} failing to load, {added} valid and {ignored} ignored (invalid) certs"
);
Ok(())
}
@@ -394,9 +426,26 @@ impl TlsParametersBuilder {
}
}
let tls = if self.accept_invalid_certs || self.accept_invalid_hostnames {
let verifier = InvalidCertsVerifier {
ignore_invalid_hostnames: self.accept_invalid_hostnames,
ignore_invalid_certs: self.accept_invalid_certs,
roots: root_cert_store,
signature_algorithms,
};
tls.dangerous()
.with_custom_certificate_verifier(Arc::new(verifier))
} else {
tls.with_root_certificates(root_cert_store)
};
let tls = tls.with_no_client_auth();
let tls = if let Some(identity) = self.identity {
let (client_certificates, private_key) = identity.rustls_tls;
tls.with_client_auth_cert(client_certificates, private_key)
.map_err(error::tls)?
} else {
tls.with_no_client_auth()
};
Ok(TlsParameters {
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
@@ -461,7 +510,7 @@ impl TlsParameters {
}
}
/// A client certificate that can be used with [`TlsParametersBuilder::add_root_certificate`]
/// A certificate that can be used with [`TlsParametersBuilder::add_root_certificate`]
#[derive(Clone)]
#[allow(missing_copy_implementations)]
pub struct Certificate {
@@ -528,20 +577,109 @@ impl Debug for Certificate {
}
}
/// An identity that can be used with [`TlsParametersBuilder::identify_with`]
#[allow(missing_copy_implementations)]
pub struct Identity {
#[cfg(feature = "native-tls")]
native_tls: native_tls::Identity,
#[cfg(feature = "rustls-tls")]
rustls_tls: (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>),
#[cfg(feature = "boring-tls")]
boring_tls: (boring::x509::X509, PKey<boring::pkey::Private>),
}
impl Debug for Identity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Identity").finish()
}
}
impl Clone for Identity {
fn clone(&self) -> Self {
Identity {
#[cfg(feature = "native-tls")]
native_tls: self.native_tls.clone(),
#[cfg(feature = "rustls-tls")]
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-tls", 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)?,
#[cfg(feature = "rustls-tls")]
rustls_tls: Identity::from_pem_rustls_tls(pem, key)?,
#[cfg(feature = "boring-tls")]
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-tls")]
fn from_pem_rustls_tls(
pem: &[u8],
key: &[u8],
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), Error> {
let mut key = key;
let key = rustls_pemfile::private_key(&mut key).unwrap().unwrap();
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-tls")]
#[derive(Debug)]
struct InvalidCertsVerifier;
struct InvalidCertsVerifier {
ignore_invalid_hostnames: bool,
ignore_invalid_certs: bool,
roots: RootCertStore,
signature_algorithms: WebPkiSupportedAlgorithms,
}
#[cfg(feature = "rustls-tls")]
impl ServerCertVerifier for InvalidCertsVerifier {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>],
server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
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.signature_algorithms.all,
)?;
}
if !self.ignore_invalid_hostnames {
rustls::client::verify_server_name(&cert, server_name)?;
}
Ok(ServerCertVerified::assertion())
}

View File

@@ -32,7 +32,7 @@ impl Transport for SmtpTransport {
let result = conn.send(envelope, email)?;
#[cfg(not(feature = "pool"))]
conn.quit()?;
conn.abort();
Ok(result)
}