Compare commits

..

29 Commits

Author SHA1 Message Date
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
Paolo Barbolini
9d3ebfab1a Prepare 0.11.7 (#961) 2024-04-23 16:04:19 +02:00
rezabet
6fb69086fb style: Maintain message consistency (#960) 2024-04-21 16:10:32 +02:00
Paolo Barbolini
dfdf3a61d2 Drop ref syntax (#959) 2024-04-21 11:22:07 +02:00
dependabot[bot]
e30ac2dbff Bump rustls from 0.23.3 to 0.23.5 (#958)
Bumps [rustls](https://github.com/rustls/rustls) from 0.23.3 to 0.23.5.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.23.3...v/0.23.5)

---
updated-dependencies:
- dependency-name: rustls
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-21 10:28:08 +02:00
Paolo Barbolini
22dca340a7 Bump hostname to v0.4 (#956) 2024-04-01 21:36:57 +02:00
Paolo Barbolini
c7d1f35676 Prepare 0.11.6 (#955) 2024-03-28 16:19:10 +01:00
Paolo Barbolini
eebea56f16 Upgrade email-encoding to v0.3 (#952) 2024-03-28 15:22:55 +01:00
Paolo Barbolini
851d6ae164 Bump license year (#954) 2024-03-28 14:50:53 +01:00
Paolo Barbolini
6f38e6b9a9 Fix latest clippy warnings (#953) 2024-03-28 05:12:13 +01:00
Paolo Barbolini
c40af78809 Prepare 0.11.5 (#951) 2024-03-25 17:59:47 +01:00
Paolo Barbolini
6d2e0d5046 Bump rustls to v0.23 (#950) 2024-03-22 20:09:27 +01:00
26 changed files with 1483 additions and 1030 deletions

View File

@@ -13,16 +13,16 @@ env:
jobs: jobs:
rustfmt: rustfmt:
name: rustfmt / nightly-2023-06-22 name: rustfmt / nightly-2024-09-01
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Install rust - name: Install rust
run: | run: |
rustup default nightly-2023-06-22 rustup default nightly-2024-09-01
rustup component add rustfmt rustup component add rustfmt
- name: cargo fmt - name: cargo fmt
@@ -34,7 +34,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Install rust - name: Install rust
run: | run: |
@@ -50,7 +50,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Install rust - name: Install rust
run: rustup update --no-self-update stable run: rustup update --no-self-update stable
@@ -80,7 +80,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Install rust - name: Install rust
run: | run: |
@@ -112,12 +112,6 @@ jobs:
- name: Install dkimverify - name: Install dkimverify
run: sudo apt -y install python3-dkim 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 - name: Test with no default features
run: cargo test --no-default-features run: cargo test --no-default-features
@@ -134,7 +128,7 @@ jobs:
# name: Coverage # name: Coverage
# runs-on: ubuntu-latest # runs-on: ubuntu-latest
# steps: # steps:
# - uses: actions/checkout@v2 # - uses: actions/checkout@v4
# - uses: actions-rs/toolchain@v1 # - uses: actions-rs/toolchain@v1
# with: # with:
# toolchain: nightly # toolchain: nightly

View File

@@ -1,3 +1,97 @@
<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)
#### Misc
* Bump `hostname` to v0.4 ([#956])
* Fix `tracing` message consistency ([#960])
* Bump minimum required `rustls` to v0.23.5 ([#958])
* Dropped use of `ref` syntax in the entire project ([#959])
[#956]: https://github.com/lettre/lettre/pull/956
[#958]: https://github.com/lettre/lettre/pull/958
[#959]: https://github.com/lettre/lettre/pull/959
[#960]: https://github.com/lettre/lettre/pull/960
<a name="v0.11.6"></a>
### v0.11.6 (2024-03-28)
#### Bug fixes
* Upgraded `email-encoding` to v0.3 - fixing multiple encoding bugs in the process ([#952])
#### Misc
* Updated copyright year in license ([#954])
[#952]: https://github.com/lettre/lettre/pull/952
[#954]: https://github.com/lettre/lettre/pull/954
<a name="v0.11.5"></a>
### v0.11.5 (2024-03-25)
#### Features
* Support SMTP SASL draft login challenge ([#911])
* Add conversion from SMTP response code to integer ([#941])
#### Misc
* Upgrade `rustls` to v0.23 ([#950])
* Bump `base64` to v0.22 ([#945])
* Fix typos in documentation ([#943], [#944])
* Add `Cargo.lock` ([#942])
[#911]: https://github.com/lettre/lettre/pull/911
[#941]: https://github.com/lettre/lettre/pull/941
[#942]: https://github.com/lettre/lettre/pull/942
[#943]: https://github.com/lettre/lettre/pull/943
[#944]: https://github.com/lettre/lettre/pull/944
[#945]: https://github.com/lettre/lettre/pull/945
[#950]: https://github.com/lettre/lettre/pull/950
<a name="v0.11.4"></a> <a name="v0.11.4"></a>
### v0.11.4 (2024-01-28) ### v0.11.4 (2024-01-28)

1314
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lettre" name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge) # remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.11.4" version = "0.11.9"
description = "Email client" description = "Email client"
readme = "README.md" readme = "README.md"
homepage = "https://lettre.rs" homepage = "https://lettre.rs"
@@ -20,7 +20,7 @@ maintenance = { status = "actively-developed" }
[dependencies] [dependencies]
chumsky = "0.9" chumsky = "0.9"
idna = "0.5" idna = "1"
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
# builder # builder
@@ -29,7 +29,7 @@ mime = { version = "0.3.4", optional = true }
fastrand = { version = "2.0", optional = true } fastrand = { version = "2.0", optional = true }
quoted_printable = { version = "0.5", optional = true } quoted_printable = { version = "0.5", optional = true }
base64 = { version = "0.22", optional = true } base64 = { version = "0.22", optional = true }
email-encoding = { version = "0.2", optional = true } email-encoding = { version = "0.3", optional = true }
# file transport # file transport
uuid = { version = "1", features = ["v4"], optional = true } uuid = { version = "1", features = ["v4"], optional = true }
@@ -38,16 +38,17 @@ serde_json = { version = "1", optional = true }
# smtp-transport # smtp-transport
nom = { version = "7", optional = true } nom = { version = "7", optional = true }
hostname = { version = "0.3", optional = true } # feature hostname = { version = "0.4", optional = true } # feature
socket2 = { version = "0.5.1", optional = true } socket2 = { version = "0.5.1", optional = true }
url = { version = "2.4", optional = true } url = { version = "2.4", optional = true }
percent-encoding = { version = "2.3", optional = true } percent-encoding = { version = "2.3", optional = true }
## tls ## tls
native-tls = { version = "0.2.5", optional = true } # feature native-tls = { version = "0.2.5", optional = true } # feature
rustls = { version = "0.22.1", optional = true } rustls = { version = "0.23.5", default-features = false, features = ["ring", "logging", "std", "tls12"], optional = true }
rustls-pemfile = { version = "2", 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 } webpki-roots = { version = "0.26", optional = true }
boring = { version = "4", optional = true } boring = { version = "4", optional = true }
@@ -58,13 +59,12 @@ async-trait = { version = "0.1", optional = true }
## async-std ## async-std
async-std = { version = "1.8", optional = true } async-std = { version = "1.8", optional = true }
#async-native-tls = { version = "0.3.3", optional = true } futures-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12", "ring"], optional = true }
futures-rustls = { version = "0.25", optional = true }
## tokio ## tokio
tokio1_crate = { package = "tokio", version = "1", optional = true } tokio1_crate = { package = "tokio", version = "1", optional = true }
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true } tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
tokio1_rustls = { package = "tokio-rustls", version = "0.25", optional = true } tokio1_rustls = { package = "tokio-rustls", version = "0.26", default-features = false, features = ["logging", "tls12", "ring"], optional = true }
tokio1_boring = { package = "tokio-boring", version = "4", optional = true } tokio1_boring = { package = "tokio-boring", version = "4", optional = true }
## dkim ## dkim
@@ -108,13 +108,12 @@ smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:url", "dep:percen
pool = ["dep:futures-util"] 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"] boring-tls = ["dep:boring"]
# async # async
async-std1 = ["dep:async-std", "dep:async-trait", "dep:futures-io", "dep:futures-util"] 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"] 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 = ["dep:tokio1_crate", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
tokio1-native-tls = ["tokio1", "native-tls", "dep:tokio1_native_tls_crate"] 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"] 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] [package.metadata.docs.rs]
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"] rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"]

View File

@@ -1,5 +1,5 @@
Copyright (c) 2014-2022 Alexis Mousset <contact@amousset.me> Copyright (c) 2014-2024 Alexis Mousset <contact@amousset.me>
Copyright (c) 2019-2022 Paolo Barbolini <paolo@paolo565.org> Copyright (c) 2019-2024 Paolo Barbolini <paolo@paolo565.org>
Copyright (c) 2018 K. <kayo@illumium.org> Copyright (c) 2018 K. <kayo@illumium.org>
Permission is hereby granted, free of charge, to any Permission is hereby granted, free of charge, to any

View File

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

View File

@@ -41,7 +41,7 @@ fn main() {
// Plaintext connection which MUST then successfully upgrade to TLS via STARTTLS // Plaintext connection which MUST then successfully upgrade to TLS via STARTTLS
{ {
tracing::info!("Trying to establish a plaintext connection to {} and then updating it via the SMTP STARTTLS extension", smtp_host); tracing::info!("Trying to establish a plaintext connection to {} and then upgrading it via the SMTP STARTTLS extension", smtp_host);
let transport = SmtpTransport::starttls_relay(&smtp_host) let transport = SmtpTransport::starttls_relay(&smtp_host)
.expect("build SmtpTransport::starttls_relay") .expect("build SmtpTransport::starttls_relay")

View File

@@ -14,11 +14,71 @@ pub struct Envelope {
/// The envelope recipient's addresses /// The envelope recipient's addresses
/// ///
/// This can not be empty. /// This can not be empty.
#[cfg_attr(
feature = "serde",
serde(deserialize_with = "serde_forward_path::deserialize")
)]
forward_path: Vec<Address>, forward_path: Vec<Address>,
/// The envelope sender address /// The envelope sender address
reverse_path: Option<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 { impl Envelope {
/// Creates a new envelope, which may fail if `to` is empty. /// Creates a new envelope, which may fail if `to` is empty.
/// ///

View File

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

View File

@@ -109,7 +109,7 @@
//! [mime 0.3]: https://docs.rs/mime/0.3 //! [mime 0.3]: https://docs.rs/mime/0.3
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376 //! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.4")] #![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.9")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")] #![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")] #![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
@@ -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. 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."); 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",))]
#[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")
))]
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. 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'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. 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

@@ -1,6 +1,6 @@
use std::fmt::Write; use std::fmt::Write;
use email_encoding::headers::EmailWriter; use email_encoding::headers::writer::EmailWriter;
use super::{Header, HeaderName, HeaderValue}; use super::{Header, HeaderName, HeaderValue};
use crate::BoxError; use crate::BoxError;
@@ -38,10 +38,10 @@ impl ContentDisposition {
let mut encoded_value = String::new(); let mut encoded_value = String::new();
let line_len = "Content-Disposition: ".len(); let line_len = "Content-Disposition: ".len();
{ {
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false); let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
w.write_str(kind).expect("writing `kind` returned an error"); w.write_str(kind).expect("writing `kind` returned an error");
w.write_char(';').expect("writing `;` returned an error"); w.write_char(';').expect("writing `;` returned an error");
w.optional_breakpoint(); w.space();
email_encoding::headers::rfc2231::encode("filename", file_name, &mut w) email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
.expect("some Write implementation returned an error"); .expect("some Write implementation returned an error");

View File

@@ -1,4 +1,4 @@
use email_encoding::headers::EmailWriter; use email_encoding::headers::writer::EmailWriter;
use super::{Header, HeaderName, HeaderValue}; use super::{Header, HeaderName, HeaderValue};
use crate::{ use crate::{
@@ -31,7 +31,7 @@ macro_rules! mailbox_header {
let mut encoded_value = String::new(); let mut encoded_value = String::new();
let line_len = $header_name.len() + ": ".len(); let line_len = $header_name.len() + ": ".len();
{ {
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false); let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
self.0.encode(&mut w).expect("writing `Mailbox` returned an error"); self.0.encode(&mut w).expect("writing `Mailbox` returned an error");
} }
@@ -81,7 +81,7 @@ macro_rules! mailboxes_header {
let mut encoded_value = String::new(); let mut encoded_value = String::new();
let line_len = $header_name.len() + ": ".len(); let line_len = $header_name.len() + ": ".len();
{ {
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false); let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
self.0.encode(&mut w).expect("writing `Mailboxes` returned an error"); self.0.encode(&mut w).expect("writing `Mailboxes` returned an error");
} }

View File

@@ -7,7 +7,7 @@ use std::{
ops::Deref, ops::Deref,
}; };
use email_encoding::headers::EmailWriter; use email_encoding::headers::writer::EmailWriter;
pub use self::{ pub use self::{
content::*, content::*,
@@ -124,22 +124,18 @@ impl Headers {
} }
pub(crate) fn find_header(&self, name: &str) -> Option<&HeaderValue> { pub(crate) fn find_header(&self, name: &str) -> Option<&HeaderValue> {
self.headers self.headers.iter().find(|value| name == value.name)
.iter()
.find(|value| name.eq_ignore_ascii_case(&value.name))
} }
fn find_header_mut(&mut self, name: &str) -> Option<&mut HeaderValue> { fn find_header_mut(&mut self, name: &str) -> Option<&mut HeaderValue> {
self.headers self.headers.iter_mut().find(|value| name == value.name)
.iter_mut()
.find(|value| name.eq_ignore_ascii_case(&value.name))
} }
fn find_header_index(&self, name: &str) -> Option<usize> { fn find_header_index(&self, name: &str) -> Option<usize> {
self.headers self.headers
.iter() .iter()
.enumerate() .enumerate()
.find(|(_i, value)| name.eq_ignore_ascii_case(&value.name)) .find(|(_i, value)| name == value.name)
.map(|(i, _)| i) .map(|(i, _)| i)
} }
} }
@@ -161,18 +157,9 @@ impl Display for Headers {
/// A possible error when converting a `HeaderName` from another type. /// A possible error when converting a `HeaderName` from another type.
// comes from `http` crate // comes from `http` crate
#[allow(missing_copy_implementations)] #[allow(missing_copy_implementations)]
#[derive(Clone)] #[derive(Debug, Clone)]
pub struct InvalidHeaderName { #[non_exhaustive]
_priv: (), pub struct InvalidHeaderName;
}
impl fmt::Debug for InvalidHeaderName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InvalidHeaderName")
// skip _priv noise
.finish()
}
}
impl fmt::Display for InvalidHeaderName { impl fmt::Display for InvalidHeaderName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -189,14 +176,11 @@ pub struct HeaderName(Cow<'static, str>);
impl HeaderName { impl HeaderName {
/// Creates a new header name /// Creates a new header name
pub fn new_from_ascii(ascii: String) -> Result<Self, InvalidHeaderName> { pub fn new_from_ascii(ascii: String) -> Result<Self, InvalidHeaderName> {
if !ascii.is_empty() if !ascii.is_empty() && ascii.len() <= 76 && ascii.is_ascii() && !ascii.contains([':', ' '])
&& ascii.len() <= 76
&& ascii.is_ascii()
&& !ascii.contains(|c| c == ':' || c == ' ')
{ {
Ok(Self(Cow::Owned(ascii))) Ok(Self(Cow::Owned(ascii)))
} else { } else {
Err(InvalidHeaderName { _priv: () }) Err(InvalidHeaderName)
} }
} }
@@ -257,23 +241,19 @@ impl AsRef<str> for HeaderName {
impl PartialEq<HeaderName> for HeaderName { impl PartialEq<HeaderName> for HeaderName {
fn eq(&self, other: &HeaderName) -> bool { fn eq(&self, other: &HeaderName) -> bool {
let s1: &str = self.as_ref(); self.eq_ignore_ascii_case(other)
let s2: &str = other.as_ref();
s1 == s2
} }
} }
impl PartialEq<&str> for HeaderName { impl PartialEq<&str> for HeaderName {
fn eq(&self, other: &&str) -> bool { fn eq(&self, other: &&str) -> bool {
let s: &str = self.as_ref(); self.eq_ignore_ascii_case(other)
s == *other
} }
} }
impl PartialEq<HeaderName> for &str { impl PartialEq<HeaderName> for &str {
fn eq(&self, other: &HeaderName) -> bool { fn eq(&self, other: &HeaderName) -> bool {
let s: &str = other.as_ref(); self.eq_ignore_ascii_case(other)
*self == s
} }
} }
@@ -348,7 +328,7 @@ impl<'a> HeaderValueEncoder<'a> {
fn new(name: &str, writer: &'a mut dyn Write) -> Self { fn new(name: &str, writer: &'a mut dyn Write) -> Self {
let line_len = name.len() + ": ".len(); let line_len = name.len() + ": ".len();
let writer = EmailWriter::new(writer, line_len, 0, false, false); let writer = EmailWriter::new(writer, line_len, 0, false);
Self { Self {
writer, writer,
@@ -467,6 +447,60 @@ mod tests {
let _ = HeaderName::new_from_ascii_str(""); 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 // names taken randomly from https://it.wikipedia.org/wiki/Pinco_Pallino
#[test] #[test]
@@ -612,17 +646,14 @@ mod tests {
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_owned(), "🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_owned(),
)); ));
// TODO: fix the fact that the encoder doesn't know that
// the space between the name and the address should be
// removed when wrapping.
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
concat!( concat!(
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n", "To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n", " Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n", " =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n", " =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n", " =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
) )
); );
} }
@@ -687,9 +718,6 @@ mod tests {
"quoted-printable".to_owned(), "quoted-printable".to_owned(),
)); ));
// TODO: fix the fact that the encoder doesn't know that
// the space between the name and the address should be
// removed when wrapping.
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
concat!( concat!(
@@ -699,8 +727,8 @@ mod tests {
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n", "To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n", " Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n", " =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n", " =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n", " =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
"From: Someone <somewhere@example.com>\r\n", "From: Someone <somewhere@example.com>\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n", "Content-Transfer-Encoding: quoted-printable\r\n",
) )

View File

@@ -170,7 +170,9 @@ fn phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
// mailbox = name-addr / addr-spec // mailbox = name-addr / addr-spec
pub(crate) fn mailbox() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>> 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 // name-addr = [display-name] angle-addr

View File

@@ -6,7 +6,7 @@ use std::{
}; };
use chumsky::prelude::*; use chumsky::prelude::*;
use email_encoding::headers::EmailWriter; use email_encoding::headers::writer::EmailWriter;
use super::parsers; use super::parsers;
use crate::address::{Address, AddressError}; use crate::address::{Address, AddressError};
@@ -72,7 +72,7 @@ impl Mailbox {
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult { pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
if let Some(name) = &self.name { if let Some(name) = &self.name {
email_encoding::headers::quoted_string::encode(name, w)?; email_encoding::headers::quoted_string::encode(name, w)?;
w.optional_breakpoint(); w.space();
w.write_char('<')?; w.write_char('<')?;
} }
@@ -261,7 +261,7 @@ impl Mailboxes {
for mailbox in self.iter() { for mailbox in self.iter() {
if !mem::take(&mut first) { if !mem::take(&mut first) {
w.write_char(',')?; w.write_char(',')?;
w.optional_breakpoint(); w.space();
} }
mailbox.encode(w)?; mailbox.encode(w)?;
@@ -444,8 +444,6 @@ fn write_quoted_string_char(f: &mut Formatter<'_>, c: char) -> FmtResult {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::convert::TryInto;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::Mailbox; use super::Mailbox;
@@ -558,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] #[test]
fn parse_address_with_name() { fn parse_address_with_name() {
assert_eq!( assert_eq!(
@@ -569,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] #[test]
fn parse_address_with_empty_name() { fn parse_address_with_empty_name() {
assert_eq!( assert_eq!(
@@ -580,7 +597,7 @@ mod test {
#[test] #[test]
fn parse_address_with_empty_name_trim() { fn parse_address_with_empty_name_trim() {
assert_eq!( assert_eq!(
" <kayo@example.com>".parse(), " <kayo@example.com> ".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap())) Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
); );
} }

View File

@@ -420,7 +420,6 @@ mod test {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*; use super::*;
use crate::message::header;
#[test] #[test]
fn single_part_binary() { fn single_part_binary() {

View File

@@ -12,7 +12,7 @@
//! //!
//! * a service from your Cloud or hosting provider //! * a service from your Cloud or hosting provider
//! * an email server ([MTA] for Mail Transfer Agent, like Postfix or Exchange), running either //! * an email server ([MTA] for Mail Transfer Agent, like Postfix or Exchange), running either
//! locally on your servers or accessible over the network //! locally on your servers or accessible over the network
//! * a dedicated external service, like Mailchimp, Mailgun, etc. //! * a dedicated external service, like Mailchimp, Mailgun, etc.
//! //!
//! In most cases, the best option is to: //! In most cases, the best option is to:

View File

@@ -82,7 +82,6 @@ where
#[cfg(any( #[cfg(any(
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls", feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls" feature = "async-std1-rustls-tls"
))] ))]
#[cfg_attr( #[cfg_attr(
@@ -117,7 +116,6 @@ where
#[cfg(any( #[cfg(any(
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls", feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls" feature = "async-std1-rustls-tls"
))] ))]
#[cfg_attr( #[cfg_attr(
@@ -353,7 +351,6 @@ impl AsyncSmtpTransportBuilder {
#[cfg(any( #[cfg(any(
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls", feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls" feature = "async-std1-rustls-tls"
))] ))]
#[cfg_attr( #[cfg_attr(

View File

@@ -6,7 +6,7 @@ use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use super::async_net::AsyncTokioStream; use super::async_net::AsyncTokioStream;
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
use super::escape_crlf; use super::escape_crlf;
use super::{AsyncNetworkStream, ClientCodec, ConnectionState, TlsParameters}; use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
use crate::{ use crate::{
transport::smtp::{ transport::smtp::{
authentication::{Credentials, Mechanism}, authentication::{Credentials, Mechanism},
@@ -19,11 +19,25 @@ use crate::{
Envelope, Envelope,
}; };
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
$client.abort().await;
return Err(From::from(err))
},
}
})
);
/// Structure that implements the SMTP client /// Structure that implements the SMTP client
pub struct AsyncSmtpConnection { pub struct AsyncSmtpConnection {
/// TCP stream between client and server /// TCP stream between client and server
/// Value is None before connection /// Value is None before connection
stream: BufReader<AsyncNetworkStream>, stream: BufReader<AsyncNetworkStream>,
/// Panic state
panic: bool,
/// Information about the server /// Information about the server
server_info: ServerInfo, server_info: ServerInfo,
} }
@@ -112,6 +126,7 @@ impl AsyncSmtpConnection {
let stream = BufReader::new(stream); let stream = BufReader::new(stream);
let mut conn = AsyncSmtpConnection { let mut conn = AsyncSmtpConnection {
stream, stream,
panic: false,
server_info: ServerInfo::default(), server_info: ServerInfo::default(),
}; };
// TODO log // TODO log
@@ -155,26 +170,30 @@ impl AsyncSmtpConnection {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime)); mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
} }
self.command(Mail::new(envelope.from().cloned(), mail_options)) try_smtp!(
.await?; self.command(Mail::new(envelope.from().cloned(), mail_options))
.await,
self
);
// Recipient // Recipient
for to_address in envelope.to() { for to_address in envelope.to() {
self.command(Rcpt::new(to_address.clone(), vec![])).await?; try_smtp!(
self.command(Rcpt::new(to_address.clone(), vec![])).await,
self
);
} }
// Data // Data
self.command(Data).await?; try_smtp!(self.command(Data).await, self);
// Message content // Message content
self.message(email).await let result = try_smtp!(self.message(email).await, self);
Ok(result)
} }
pub fn has_broken(&self) -> bool { pub fn has_broken(&self) -> bool {
match self.stream.get_ref().state() { self.panic
ConnectionState::Ok => false,
ConnectionState::Broken | ConnectionState::Closed => true,
}
} }
pub fn can_starttls(&self) -> bool { pub fn can_starttls(&self) -> bool {
@@ -189,20 +208,18 @@ impl AsyncSmtpConnection {
/// [rfc8314]: https://www.rfc-editor.org/rfc/rfc8314 /// [rfc8314]: https://www.rfc-editor.org/rfc/rfc8314
#[allow(unused_variables)] #[allow(unused_variables)]
pub async fn starttls( pub async fn starttls(
mut self, &mut self,
tls_parameters: TlsParameters, tls_parameters: TlsParameters,
hello_name: &ClientId, hello_name: &ClientId,
) -> Result<Self, Error> { ) -> Result<(), Error> {
if self.server_info.supports_feature(Extension::StartTls) { if self.server_info.supports_feature(Extension::StartTls) {
self.command(Starttls).await?; try_smtp!(self.command(Starttls).await, self);
let stream = self.stream.into_inner(); self.stream.get_mut().upgrade_tls(tls_parameters).await?;
let stream = stream.upgrade_tls(tls_parameters).await?;
self.stream = BufReader::new(stream);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("connection encrypted"); tracing::debug!("connection encrypted");
// Send EHLO again // Send EHLO again
self.ehlo(hello_name).await?; try_smtp!(self.ehlo(hello_name).await, self);
Ok(self) Ok(())
} else { } else {
Err(error::client("STARTTLS is not supported on this server")) Err(error::client("STARTTLS is not supported on this server"))
} }
@@ -210,24 +227,22 @@ impl AsyncSmtpConnection {
/// Send EHLO and update server info /// Send EHLO and update server info
async fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> { async fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
let ehlo_response = self.command(Ehlo::new(hello_name.clone())).await?; let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())).await, self);
self.server_info = ServerInfo::from_response(&ehlo_response)?; self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
Ok(()) Ok(())
} }
pub async fn quit(&mut self) -> Result<Response, Error> { pub async fn quit(&mut self) -> Result<Response, Error> {
self.command(Quit).await Ok(try_smtp!(self.command(Quit).await, self))
} }
pub async fn abort(&mut self) { pub async fn abort(&mut self) {
match self.stream.get_ref().state() { // Only try to quit if we are not already broken
ConnectionState::Ok | ConnectionState::Broken => { if !self.panic {
let _ = self.command(Quit).await; self.panic = true;
let _ = self.stream.close().await; let _ = self.command(Quit).await;
self.stream.get_mut().set_state(ConnectionState::Closed);
}
ConnectionState::Closed => {}
} }
let _ = self.stream.close().await;
} }
/// Sets the underlying stream /// Sets the underlying stream
@@ -264,13 +279,15 @@ impl AsyncSmtpConnection {
while challenges > 0 && response.has_code(334) { while challenges > 0 && response.has_code(334) {
challenges -= 1; challenges -= 1;
response = self response = try_smtp!(
.command(Auth::new_from_response( self.command(Auth::new_from_response(
mechanism, mechanism,
credentials.clone(), credentials.clone(),
&response, &response,
)?) )?)
.await?; .await,
self
);
} }
if challenges == 0 { if challenges == 0 {
@@ -298,9 +315,6 @@ impl AsyncSmtpConnection {
/// Writes a string to the server /// Writes a string to the server
async fn write(&mut self, string: &[u8]) -> Result<(), Error> { async fn write(&mut self, string: &[u8]) -> Result<(), Error> {
self.stream.get_ref().state().verify()?;
self.stream.get_mut().set_state(ConnectionState::Broken);
self.stream self.stream
.get_mut() .get_mut()
.write_all(string) .write_all(string)
@@ -312,8 +326,6 @@ impl AsyncSmtpConnection {
.await .await
.map_err(error::network)?; .map_err(error::network)?;
self.stream.get_mut().set_state(ConnectionState::Ok);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string))); tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
Ok(()) Ok(())
@@ -321,9 +333,6 @@ impl AsyncSmtpConnection {
/// Gets the SMTP response /// Gets the SMTP response
pub async fn read_response(&mut self) -> Result<Response, Error> { pub async fn read_response(&mut self) -> Result<Response, Error> {
self.stream.get_ref().state().verify()?;
self.stream.get_mut().set_state(ConnectionState::Broken);
let mut buffer = String::with_capacity(100); let mut buffer = String::with_capacity(100);
while self while self
@@ -337,8 +346,6 @@ impl AsyncSmtpConnection {
tracing::debug!("<< {}", escape_crlf(&buffer)); tracing::debug!("<< {}", escape_crlf(&buffer));
match parse_response(&buffer) { match parse_response(&buffer) {
Ok((_remaining, response)) => { Ok((_remaining, response)) => {
self.stream.get_mut().set_state(ConnectionState::Ok);
return if response.is_positive() { return if response.is_positive() {
Ok(response) Ok(response)
} else { } else {
@@ -346,7 +353,7 @@ impl AsyncSmtpConnection {
response.code(), response.code(),
Some(response.message().collect()), Some(response.message().collect()),
)) ))
}; }
} }
Err(nom::Err::Failure(e)) => { Err(nom::Err::Failure(e)) => {
return Err(error::response(e.to_string())); return Err(error::response(e.to_string()));
@@ -366,4 +373,10 @@ impl AsyncSmtpConnection {
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> { pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate() 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,12 +6,11 @@ use std::{
time::Duration, time::Duration,
}; };
#[cfg(feature = "async-std1-native-tls")]
use async_native_tls::TlsStream as AsyncStd1TlsStream;
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs}; use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs};
use futures_io::{ use futures_io::{
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Result as IoResult, AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind,
Result as IoResult,
}; };
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream; use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
@@ -35,11 +34,10 @@ use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls", feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls", feature = "tokio1-boring-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls" feature = "async-std1-rustls-tls"
))] ))]
use super::InnerTlsParameters; use super::InnerTlsParameters;
use super::{ConnectionState, TlsParameters}; use super::TlsParameters;
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
use crate::transport::smtp::client::net::resolved_address_filter; use crate::transport::smtp::client::net::resolved_address_filter;
use crate::transport::smtp::{error, Error}; use crate::transport::smtp::{error, Error};
@@ -48,7 +46,6 @@ use crate::transport::smtp::{error, Error};
#[derive(Debug)] #[derive(Debug)]
pub struct AsyncNetworkStream { pub struct AsyncNetworkStream {
inner: InnerAsyncNetworkStream, inner: InnerAsyncNetworkStream,
state: ConnectionState,
} }
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
@@ -86,27 +83,19 @@ enum InnerAsyncNetworkStream {
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
AsyncStd1Tcp(AsyncStd1TcpStream), AsyncStd1Tcp(AsyncStd1TcpStream),
/// Encrypted Tokio 1.x TCP stream /// 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")] #[cfg(feature = "async-std1-rustls-tls")]
AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>), AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>),
/// Can't be built
None,
} }
impl AsyncNetworkStream { impl AsyncNetworkStream {
fn new(inner: InnerAsyncNetworkStream) -> Self { fn new(inner: InnerAsyncNetworkStream) -> Self {
AsyncNetworkStream { if let InnerAsyncNetworkStream::None = inner {
inner, debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
state: ConnectionState::Ok,
} }
}
pub(super) fn state(&self) -> ConnectionState { AsyncNetworkStream { inner }
self.state
}
pub(super) fn set_state(&mut self, state: ConnectionState) {
self.state = state;
} }
/// Returns peer's address /// Returns peer's address
@@ -124,10 +113,15 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1BoringTls(s) => s.get_ref().peer_addr(), InnerAsyncNetworkStream::Tokio1BoringTls(s) => s.get_ref().peer_addr(),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => s.peer_addr(), 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")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => s.get_ref().0.peer_addr(), InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => s.get_ref().0.peer_addr(),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Err(IoError::new(
ErrorKind::Other,
"InnerAsyncNetworkStream::None must never be built",
))
}
} }
} }
@@ -197,7 +191,7 @@ impl AsyncNetworkStream {
let mut stream = let mut stream =
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(Box::new(tcp_stream))); AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(Box::new(tcp_stream)));
if let Some(tls_parameters) = tls_parameters { if let Some(tls_parameters) = tls_parameters {
stream = stream.upgrade_tls(tls_parameters).await?; stream.upgrade_tls(tls_parameters).await?;
} }
Ok(stream) Ok(stream)
} }
@@ -248,13 +242,13 @@ impl AsyncNetworkStream {
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream)); let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters { if let Some(tls_parameters) = tls_parameters {
stream = stream.upgrade_tls(tls_parameters).await?; stream.upgrade_tls(tls_parameters).await?;
} }
Ok(stream) Ok(stream)
} }
pub async fn upgrade_tls(self, tls_parameters: TlsParameters) -> Result<Self, Error> { pub async fn upgrade_tls(&mut self, tls_parameters: TlsParameters) -> Result<(), Error> {
match self.inner { match &self.inner {
#[cfg(all( #[cfg(all(
feature = "tokio1", feature = "tokio1",
not(any( not(any(
@@ -273,35 +267,40 @@ impl AsyncNetworkStream {
feature = "tokio1-rustls-tls", feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls" feature = "tokio1-boring-tls"
))] ))]
InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => { InnerAsyncNetworkStream::Tokio1Tcp(_) => {
let inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters) // get owned TcpStream
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
.await .await
.map_err(error::connection)?; .map_err(error::connection)?;
Ok(Self { Ok(())
inner,
state: ConnectionState::Ok,
})
} }
#[cfg(all( #[cfg(all(feature = "async-std1", not(feature = "async-std1-rustls-tls")))]
feature = "async-std1",
not(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))
))]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => { InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
let _ = tls_parameters; 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(tcp_stream) => { InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
let inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters) // get owned TcpStream
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
.await .await
.map_err(error::connection)?; .map_err(error::connection)?;
Ok(Self { Ok(())
inner,
state: ConnectionState::Ok,
})
} }
_ => Ok(self), _ => Ok(()),
} }
} }
@@ -375,11 +374,7 @@ impl AsyncNetworkStream {
} }
#[allow(unused_variables)] #[allow(unused_variables)]
#[cfg(any( #[cfg(feature = "async-std1-rustls-tls")]
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls",
feature = "async-std1-boring-tls"
))]
async fn upgrade_asyncstd1_tls( async fn upgrade_asyncstd1_tls(
tcp_stream: AsyncStd1TcpStream, tcp_stream: AsyncStd1TcpStream,
mut tls_parameters: TlsParameters, mut tls_parameters: TlsParameters,
@@ -390,22 +385,6 @@ impl AsyncNetworkStream {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => { InnerTlsParameters::NativeTls(connector) => {
panic!("native-tls isn't supported with async-std yet. See https://github.com/lettre/lettre/pull/531#issuecomment-757893531"); 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")] #[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(config) => { InnerTlsParameters::RustlsTls(config) => {
@@ -446,10 +425,51 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1BoringTls(_) => true, InnerAsyncNetworkStream::Tokio1BoringTls(_) => true,
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false, InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false,
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(_) => true,
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => true, 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"),
} }
} }
@@ -487,8 +507,6 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => { InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
Err(error::client("Connection is not encrypted")) Err(error::client("Connection is not encrypted"))
} }
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(t) => panic!("Unsupported"),
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream
.get_ref() .get_ref()
@@ -498,6 +516,7 @@ impl AsyncNetworkStream {
.first() .first()
.unwrap() .unwrap()
.to_vec()), .to_vec()),
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
} }
} }
} }
@@ -547,10 +566,12 @@ impl FuturesAsyncRead for AsyncNetworkStream {
} }
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_read(cx, buf), 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")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_read(cx, buf), InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_read(cx, buf),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(0))
}
} }
} }
} }
@@ -572,10 +593,12 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_write(cx, buf), 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")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_write(cx, buf),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(0))
}
} }
} }
@@ -591,16 +614,16 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_flush(cx), 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")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_flush(cx),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(()))
}
} }
} }
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> { fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
self.state = ConnectionState::Closed;
match &mut self.inner { match &mut self.inner {
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_shutdown(cx), InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_shutdown(cx),
@@ -612,10 +635,12 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_shutdown(cx), InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_close(cx), 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")] #[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_close(cx), InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_close(cx),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(()))
}
} }
} }
} }

View File

@@ -7,7 +7,7 @@ use std::{
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
use super::escape_crlf; use super::escape_crlf;
use super::{ClientCodec, ConnectionState, NetworkStream, TlsParameters}; use super::{ClientCodec, NetworkStream, TlsParameters};
use crate::{ use crate::{
address::Envelope, address::Envelope,
transport::smtp::{ transport::smtp::{
@@ -20,11 +20,25 @@ use crate::{
}, },
}; };
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
$client.abort();
return Err(From::from(err))
},
}
})
);
/// Structure that implements the SMTP client /// Structure that implements the SMTP client
pub struct SmtpConnection { pub struct SmtpConnection {
/// TCP stream between client and server /// TCP stream between client and server
/// Value is None before connection /// Value is None before connection
stream: BufReader<NetworkStream>, stream: BufReader<NetworkStream>,
/// Panic state
panic: bool,
/// Information about the server /// Information about the server
server_info: ServerInfo, server_info: ServerInfo,
} }
@@ -51,6 +65,7 @@ impl SmtpConnection {
let stream = BufReader::new(stream); let stream = BufReader::new(stream);
let mut conn = SmtpConnection { let mut conn = SmtpConnection {
stream, stream,
panic: false,
server_info: ServerInfo::default(), server_info: ServerInfo::default(),
}; };
conn.set_timeout(timeout).map_err(error::network)?; conn.set_timeout(timeout).map_err(error::network)?;
@@ -95,25 +110,26 @@ impl SmtpConnection {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime)); mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
} }
self.command(Mail::new(envelope.from().cloned(), mail_options))?; try_smtp!(
self.command(Mail::new(envelope.from().cloned(), mail_options)),
self
);
// Recipient // Recipient
for to_address in envelope.to() { for to_address in envelope.to() {
self.command(Rcpt::new(to_address.clone(), vec![]))?; try_smtp!(self.command(Rcpt::new(to_address.clone(), vec![])), self);
} }
// Data // Data
self.command(Data)?; try_smtp!(self.command(Data), self);
// Message content // Message content
self.message(email) let result = try_smtp!(self.message(email), self);
Ok(result)
} }
pub fn has_broken(&self) -> bool { pub fn has_broken(&self) -> bool {
match self.stream.get_ref().state() { self.panic
ConnectionState::Ok => false,
ConnectionState::Broken | ConnectionState::Closed => true,
}
} }
pub fn can_starttls(&self) -> bool { pub fn can_starttls(&self) -> bool {
@@ -122,22 +138,20 @@ impl SmtpConnection {
#[allow(unused_variables)] #[allow(unused_variables)]
pub fn starttls( pub fn starttls(
mut self, &mut self,
tls_parameters: &TlsParameters, tls_parameters: &TlsParameters,
hello_name: &ClientId, hello_name: &ClientId,
) -> Result<Self, Error> { ) -> Result<(), Error> {
if self.server_info.supports_feature(Extension::StartTls) { if self.server_info.supports_feature(Extension::StartTls) {
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
{ {
self.command(Starttls)?; try_smtp!(self.command(Starttls), self);
let stream = self.stream.into_inner(); self.stream.get_mut().upgrade_tls(tls_parameters)?;
let stream = stream.upgrade_tls(tls_parameters)?;
self.stream = BufReader::new(stream);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("connection encrypted"); tracing::debug!("connection encrypted");
// Send EHLO again // Send EHLO again
self.ehlo(hello_name)?; try_smtp!(self.ehlo(hello_name), self);
Ok(self) Ok(())
} }
#[cfg(not(any( #[cfg(not(any(
feature = "native-tls", feature = "native-tls",
@@ -154,24 +168,22 @@ impl SmtpConnection {
/// Send EHLO and update server info /// Send EHLO and update server info
fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> { fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
let ehlo_response = self.command(Ehlo::new(hello_name.clone()))?; let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())), self);
self.server_info = ServerInfo::from_response(&ehlo_response)?; self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
Ok(()) Ok(())
} }
pub fn quit(&mut self) -> Result<Response, Error> { pub fn quit(&mut self) -> Result<Response, Error> {
self.command(Quit) Ok(try_smtp!(self.command(Quit), self))
} }
pub fn abort(&mut self) { pub fn abort(&mut self) {
match self.stream.get_ref().state() { // Only try to quit if we are not already broken
ConnectionState::Ok | ConnectionState::Broken => { if !self.panic {
let _ = self.command(Quit); self.panic = true;
let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both); let _ = self.command(Quit);
self.stream.get_mut().set_state(ConnectionState::Closed);
}
ConnectionState::Closed => {}
} }
let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both);
} }
/// Sets the underlying stream /// Sets the underlying stream
@@ -212,11 +224,14 @@ impl SmtpConnection {
while challenges > 0 && response.has_code(334) { while challenges > 0 && response.has_code(334) {
challenges -= 1; challenges -= 1;
response = self.command(Auth::new_from_response( response = try_smtp!(
mechanism, self.command(Auth::new_from_response(
credentials.clone(), mechanism,
&response, credentials.clone(),
)?)?; &response,
)?),
self
);
} }
if challenges == 0 { if challenges == 0 {
@@ -245,17 +260,12 @@ impl SmtpConnection {
/// Writes a string to the server /// Writes a string to the server
fn write(&mut self, string: &[u8]) -> Result<(), Error> { fn write(&mut self, string: &[u8]) -> Result<(), Error> {
self.stream.get_ref().state().verify()?;
self.stream.get_mut().set_state(ConnectionState::Broken);
self.stream self.stream
.get_mut() .get_mut()
.write_all(string) .write_all(string)
.map_err(error::network)?; .map_err(error::network)?;
self.stream.get_mut().flush().map_err(error::network)?; self.stream.get_mut().flush().map_err(error::network)?;
self.stream.get_mut().set_state(ConnectionState::Ok);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string))); tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
Ok(()) Ok(())
@@ -263,9 +273,6 @@ impl SmtpConnection {
/// Gets the SMTP response /// Gets the SMTP response
pub fn read_response(&mut self) -> Result<Response, Error> { pub fn read_response(&mut self) -> Result<Response, Error> {
self.stream.get_ref().state().verify()?;
self.stream.get_mut().set_state(ConnectionState::Broken);
let mut buffer = String::with_capacity(100); let mut buffer = String::with_capacity(100);
while self.stream.read_line(&mut buffer).map_err(error::network)? > 0 { while self.stream.read_line(&mut buffer).map_err(error::network)? > 0 {
@@ -273,8 +280,6 @@ impl SmtpConnection {
tracing::debug!("<< {}", escape_crlf(&buffer)); tracing::debug!("<< {}", escape_crlf(&buffer));
match parse_response(&buffer) { match parse_response(&buffer) {
Ok((_remaining, response)) => { Ok((_remaining, response)) => {
self.stream.get_mut().set_state(ConnectionState::Ok);
return if response.is_positive() { return if response.is_positive() {
Ok(response) Ok(response)
} else { } else {
@@ -302,4 +307,10 @@ impl SmtpConnection {
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> { pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate() 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,9 +38,8 @@ pub(super) use self::tls::InnerTlsParameters;
pub use self::tls::TlsVersion; pub use self::tls::TlsVersion;
pub use self::{ pub use self::{
connection::SmtpConnection, connection::SmtpConnection,
tls::{Certificate, CertificateStore, Tls, TlsParameters, TlsParametersBuilder}, tls::{Certificate, CertificateStore, Identity, Tls, TlsParameters, TlsParametersBuilder},
}; };
use super::{error, Error};
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
mod async_connection; mod async_connection;
@@ -50,23 +49,6 @@ mod connection;
mod net; mod net;
mod tls; mod tls;
#[derive(Debug, Copy, Clone)]
enum ConnectionState {
Ok,
Broken,
Closed,
}
impl ConnectionState {
fn verify(&mut self) -> Result<(), Error> {
match self {
Self::Ok => Ok(()),
Self::Broken => Err(error::connection("connection broken")),
Self::Closed => Err(error::connection("connection closed")),
}
}
}
/// The codec used for transparency /// The codec used for transparency
#[derive(Debug)] #[derive(Debug)]
struct ClientCodec { struct ClientCodec {
@@ -157,7 +139,7 @@ mod test {
} }
#[test] #[test]
#[cfg(feature = "log")] #[cfg(feature = "tracing")]
fn test_escape_crlf() { fn test_escape_crlf() {
assert_eq!(escape_crlf("\r\n"), "<CRLF>"); assert_eq!(escape_crlf("\r\n"), "<CRLF>");
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>"); assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>");

View File

@@ -2,7 +2,8 @@
use std::sync::Arc; use std::sync::Arc;
use std::{ use std::{
io::{self, Read, Write}, io::{self, Read, Write},
net::{IpAddr, Shutdown, SocketAddr, TcpStream, ToSocketAddrs}, mem,
net::{IpAddr, Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
time::Duration, time::Duration,
}; };
@@ -16,13 +17,12 @@ use socket2::{Domain, Protocol, Type};
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use super::InnerTlsParameters; use super::InnerTlsParameters;
use super::{ConnectionState, TlsParameters}; use super::TlsParameters;
use crate::transport::smtp::{error, Error}; use crate::transport::smtp::{error, Error};
/// A network stream /// A network stream
pub struct NetworkStream { pub struct NetworkStream {
inner: InnerNetworkStream, inner: InnerNetworkStream,
state: ConnectionState,
} }
/// Represents the different types of underlying network streams /// Represents the different types of underlying network streams
@@ -40,22 +40,17 @@ enum InnerNetworkStream {
RustlsTls(StreamOwned<ClientConnection, TcpStream>), RustlsTls(StreamOwned<ClientConnection, TcpStream>),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
BoringTls(SslStream<TcpStream>), BoringTls(SslStream<TcpStream>),
/// Can't be built
None,
} }
impl NetworkStream { impl NetworkStream {
fn new(inner: InnerNetworkStream) -> Self { fn new(inner: InnerNetworkStream) -> Self {
NetworkStream { if let InnerNetworkStream::None = inner {
inner, debug_assert!(false, "InnerNetworkStream::None must never be built");
state: ConnectionState::Ok,
} }
}
pub(super) fn state(&self) -> ConnectionState { NetworkStream { inner }
self.state
}
pub(super) fn set_state(&mut self, state: ConnectionState) {
self.state = state;
} }
/// Returns peer's address /// Returns peer's address
@@ -68,13 +63,18 @@ impl NetworkStream {
InnerNetworkStream::RustlsTls(s) => s.get_ref().peer_addr(), InnerNetworkStream::RustlsTls(s) => s.get_ref().peer_addr(),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.get_ref().peer_addr(), InnerNetworkStream::BoringTls(s) => s.get_ref().peer_addr(),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(127, 0, 0, 1),
80,
)))
}
} }
} }
/// Shutdowns the connection /// Shutdowns the connection
pub fn shutdown(&mut self, how: Shutdown) -> io::Result<()> { pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
self.state = ConnectionState::Closed;
match &self.inner { match &self.inner {
InnerNetworkStream::Tcp(s) => s.shutdown(how), InnerNetworkStream::Tcp(s) => s.shutdown(how),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
@@ -83,6 +83,10 @@ impl NetworkStream {
InnerNetworkStream::RustlsTls(s) => s.get_ref().shutdown(how), InnerNetworkStream::RustlsTls(s) => s.get_ref().shutdown(how),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.get_ref().shutdown(how), InnerNetworkStream::BoringTls(s) => s.get_ref().shutdown(how),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
} }
} }
@@ -135,13 +139,13 @@ impl NetworkStream {
let tcp_stream = try_connect(server, timeout, local_addr)?; let tcp_stream = try_connect(server, timeout, local_addr)?;
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream)); let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters { if let Some(tls_parameters) = tls_parameters {
stream = stream.upgrade_tls(tls_parameters)?; stream.upgrade_tls(tls_parameters)?;
} }
Ok(stream) Ok(stream)
} }
pub fn upgrade_tls(self, tls_parameters: &TlsParameters) -> Result<Self, Error> { pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
match self.inner { match &self.inner {
#[cfg(not(any( #[cfg(not(any(
feature = "native-tls", feature = "native-tls",
feature = "rustls-tls", feature = "rustls-tls",
@@ -153,14 +157,18 @@ impl NetworkStream {
} }
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
InnerNetworkStream::Tcp(tcp_stream) => { InnerNetworkStream::Tcp(_) => {
let inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?; // get owned TcpStream
Ok(Self { let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
inner, let tcp_stream = match tcp_stream {
state: ConnectionState::Ok, InnerNetworkStream::Tcp(tcp_stream) => tcp_stream,
}) _ => unreachable!(),
};
self.inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
Ok(())
} }
_ => Ok(self), _ => Ok(()),
} }
} }
@@ -208,6 +216,36 @@ impl NetworkStream {
InnerNetworkStream::RustlsTls(_) => true, InnerNetworkStream::RustlsTls(_) => true,
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(_) => true, InnerNetworkStream::BoringTls(_) => true,
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
false
}
}
}
#[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"),
} }
} }
@@ -237,6 +275,7 @@ impl NetworkStream {
.unwrap() .unwrap()
.to_der() .to_der()
.map_err(error::tls)?), .map_err(error::tls)?),
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
} }
} }
@@ -249,6 +288,10 @@ impl NetworkStream {
InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_read_timeout(duration), InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_read_timeout(duration),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_read_timeout(duration), InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_read_timeout(duration),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
} }
} }
@@ -263,6 +306,10 @@ impl NetworkStream {
InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_write_timeout(duration), InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_write_timeout(duration),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_write_timeout(duration), InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_write_timeout(duration),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
} }
} }
} }
@@ -277,6 +324,10 @@ impl Read for NetworkStream {
InnerNetworkStream::RustlsTls(s) => s.read(buf), InnerNetworkStream::RustlsTls(s) => s.read(buf),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.read(buf), InnerNetworkStream::BoringTls(s) => s.read(buf),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(0)
}
} }
} }
} }
@@ -291,6 +342,10 @@ impl Write for NetworkStream {
InnerNetworkStream::RustlsTls(s) => s.write(buf), InnerNetworkStream::RustlsTls(s) => s.write(buf),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.write(buf), InnerNetworkStream::BoringTls(s) => s.write(buf),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(0)
}
} }
} }
@@ -303,6 +358,10 @@ impl Write for NetworkStream {
InnerNetworkStream::RustlsTls(s) => s.flush(), InnerNetworkStream::RustlsTls(s) => s.flush(),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.flush(), InnerNetworkStream::BoringTls(s) => s.flush(),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
} }
} }
} }

View File

@@ -4,6 +4,7 @@ use std::{io, sync::Arc};
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
use boring::{ use boring::{
pkey::PKey,
ssl::{SslConnector, SslVersion}, ssl::{SslConnector, SslVersion},
x509::store::X509StoreBuilder, x509::store::X509StoreBuilder,
}; };
@@ -12,8 +13,10 @@ use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
use rustls::{ use rustls::{
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
crypto::WebPkiSupportedAlgorithms,
crypto::{verify_tls12_signature, verify_tls13_signature}, 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, 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 /// For native-tls, this will use the system certificate store on Windows, the keychain on
/// macOS, and OpenSSL directories on Linux (usually `/etc/ssl`). /// 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`. /// enabled, or will fall back to `webpki-roots`.
/// ///
/// The boring-tls backend uses the same logic as OpenSSL on all platforms. /// The boring-tls backend uses the same logic as OpenSSL on all platforms.
@@ -139,6 +142,7 @@ pub struct TlsParametersBuilder {
domain: String, domain: String,
cert_store: CertificateStore, cert_store: CertificateStore,
root_certs: Vec<Certificate>, root_certs: Vec<Certificate>,
identity: Option<Identity>,
accept_invalid_hostnames: bool, accept_invalid_hostnames: bool,
accept_invalid_certs: bool, accept_invalid_certs: bool,
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
@@ -152,6 +156,7 @@ impl TlsParametersBuilder {
domain, domain,
cert_store: CertificateStore::Default, cert_store: CertificateStore::Default,
root_certs: Vec::new(), root_certs: Vec::new(),
identity: None,
accept_invalid_hostnames: false, accept_invalid_hostnames: false,
accept_invalid_certs: false, accept_invalid_certs: false,
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
@@ -167,12 +172,20 @@ impl TlsParametersBuilder {
/// Add a custom root certificate /// 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 { pub fn add_root_certificate(mut self, cert: Certificate) -> Self {
self.root_certs.push(cert); self.root_certs.push(cert);
self 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 /// Controls whether certificates with an invalid hostname are accepted
/// ///
/// Defaults to `false`. /// Defaults to `false`.
@@ -184,10 +197,11 @@ impl TlsParametersBuilder {
/// including those from other sites, are trusted. /// including those from other sites, are trusted.
/// ///
/// This method introduces significant vulnerabilities to man-in-the-middle attacks. /// This method introduces significant vulnerabilities to man-in-the-middle attacks.
/// #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
/// Hostname verification can only be disabled with the `native-tls` TLS backend. #[cfg_attr(
#[cfg(any(feature = "native-tls", feature = "boring-tls"))] docsrs,
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "boring-tls"))))] 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 { pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
self.accept_invalid_hostnames = accept_invalid_hostnames; self.accept_invalid_hostnames = accept_invalid_hostnames;
self self
@@ -275,6 +289,10 @@ impl TlsParametersBuilder {
}; };
tls_builder.min_protocol_version(Some(min_tls_version)); 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)?; let connector = tls_builder.build().map_err(error::tls)?;
Ok(TlsParameters { Ok(TlsParameters {
connector: InnerTlsParameters::NativeTls(connector), 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 { let min_tls_version = match self.min_tls_version {
TlsVersion::Tlsv10 => SslVersion::TLS1, TlsVersion::Tlsv10 => SslVersion::TLS1,
TlsVersion::Tlsv11 => SslVersion::TLS1_1, TlsVersion::Tlsv11 => SslVersion::TLS1_1,
@@ -352,51 +379,73 @@ impl TlsParametersBuilder {
}; };
let tls = ClientConfig::builder_with_protocol_versions(supported_versions); 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()));
let tls = if self.accept_invalid_certs { // Build TLS config
tls.dangerous() let signature_algorithms = provider.signature_verification_algorithms;
.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
} else {
let mut root_cert_store = RootCertStore::empty();
#[cfg(feature = "rustls-native-certs")] let mut root_cert_store = RootCertStore::empty();
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?; #[cfg(feature = "rustls-native-certs")]
let (added, ignored) = store.add_parsable_certificates(native_certs); fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
#[cfg(feature = "tracing")] let rustls_native_certs::CertificateResult { certs, errors, .. } =
tracing::debug!( rustls_native_certs::load_native_certs();
"loaded platform certs with {added} valid and {ignored} ignored (invalid) certs" let errors_len = errors.len();
);
Ok(()) 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"
);
Ok(())
}
#[cfg(feature = "rustls-tls")]
fn load_webpki_roots(store: &mut RootCertStore) {
store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
}
match self.cert_store {
CertificateStore::Default => {
#[cfg(feature = "rustls-native-certs")]
load_native_roots(&mut root_cert_store)?;
#[cfg(not(feature = "rustls-native-certs"))]
load_webpki_roots(&mut root_cert_store);
} }
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
fn load_webpki_roots(store: &mut RootCertStore) { CertificateStore::WebpkiRoots => {
store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); load_webpki_roots(&mut root_cert_store);
}
match self.cert_store {
CertificateStore::Default => {
#[cfg(feature = "rustls-native-certs")]
load_native_roots(&mut root_cert_store)?;
#[cfg(not(feature = "rustls-native-certs"))]
load_webpki_roots(&mut root_cert_store);
}
#[cfg(feature = "rustls-tls")]
CertificateStore::WebpkiRoots => {
load_webpki_roots(&mut root_cert_store);
}
CertificateStore::None => {}
}
for cert in self.root_certs {
for rustls_cert in cert.rustls {
root_cert_store.add(rustls_cert).map_err(error::tls)?;
}
} }
CertificateStore::None => {}
}
for cert in self.root_certs {
for rustls_cert in cert.rustls {
root_cert_store.add(rustls_cert).map_err(error::tls)?;
}
}
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) 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 { Ok(TlsParameters {
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)), 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)] #[derive(Clone)]
#[allow(missing_copy_implementations)] #[allow(missing_copy_implementations)]
pub struct Certificate { 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")] #[cfg(feature = "rustls-tls")]
#[derive(Debug)] #[derive(Debug)]
struct InvalidCertsVerifier; struct InvalidCertsVerifier {
ignore_invalid_hostnames: bool,
ignore_invalid_certs: bool,
roots: RootCertStore,
signature_algorithms: WebPkiSupportedAlgorithms,
}
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
impl ServerCertVerifier for InvalidCertsVerifier { impl ServerCertVerifier for InvalidCertsVerifier {
fn verify_server_cert( fn verify_server_cert(
&self, &self,
_end_entity: &CertificateDer<'_>, end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>], intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>, server_name: &ServerName<'_>,
_ocsp_response: &[u8], _ocsp_response: &[u8],
_now: UnixTime, now: UnixTime,
) -> Result<ServerCertVerified, TlsError> { ) -> 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()) Ok(ServerCertVerified::assertion())
} }

View File

@@ -291,14 +291,8 @@ impl Display for RcptParameter {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::collections::HashSet;
use super::*; use super::*;
use crate::transport::smtp::{ use crate::transport::smtp::response::{Category, Code, Detail, Severity};
authentication::Mechanism,
response::{Category, Code, Detail, Response, Severity},
};
#[test] #[test]
fn test_clientid_fmt() { fn test_clientid_fmt() {

View File

@@ -336,11 +336,11 @@ impl SmtpClient {
match &self.info.tls { match &self.info.tls {
Tls::Opportunistic(tls_parameters) => { Tls::Opportunistic(tls_parameters) => {
if conn.can_starttls() { if conn.can_starttls() {
conn = conn.starttls(tls_parameters, &self.info.hello_name)?; conn.starttls(tls_parameters, &self.info.hello_name)?;
} }
} }
Tls::Required(tls_parameters) => { Tls::Required(tls_parameters) => {
conn = conn.starttls(tls_parameters, &self.info.hello_name)?; conn.starttls(tls_parameters, &self.info.hello_name)?;
} }
_ => (), _ => (),
} }