Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6babbce00 | ||
|
|
c9895c52de | ||
|
|
575492b9ed | ||
|
|
ad665cd01e | ||
|
|
e2ac5dadfb | ||
|
|
1c6a348eb8 | ||
|
|
e8b2498ad7 | ||
|
|
bf48bd6b96 | ||
|
|
fa6191983a | ||
|
|
ca405040ae | ||
|
|
f7a1b790df | ||
|
|
caff354cbf | ||
|
|
a81401c4cb | ||
|
|
54df594d6c | ||
|
|
cada01d039 | ||
|
|
0132bee59d | ||
|
|
acdf189717 | ||
|
|
3aea65315f | ||
|
|
9d3ebfab1a | ||
|
|
6fb69086fb | ||
|
|
dfdf3a61d2 | ||
|
|
e30ac2dbff | ||
|
|
22dca340a7 | ||
|
|
c7d1f35676 | ||
|
|
eebea56f16 | ||
|
|
851d6ae164 | ||
|
|
6f38e6b9a9 | ||
|
|
c40af78809 | ||
|
|
6d2e0d5046 |
20
.github/workflows/test.yml
vendored
20
.github/workflows/test.yml
vendored
@@ -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
|
||||||
|
|||||||
94
CHANGELOG.md
94
CHANGELOG.md
@@ -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
1314
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
24
Cargo.toml
24
Cargo.toml
@@ -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"]
|
||||||
|
|||||||
4
LICENSE
4
LICENSE
@@ -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
|
||||||
|
|||||||
42
README.md
42
README.md
@@ -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:?}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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?;
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/lib.rs
18
src/lib.rs
@@ -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.
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>");
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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)?;
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user