Compare commits
76 Commits
smtp-error
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12dccd2bbe | ||
|
|
7899da9672 | ||
|
|
a85fdefe6f | ||
|
|
b73611c67f | ||
|
|
55cea7dbe6 | ||
|
|
d2b9d50000 | ||
|
|
3d16344d53 | ||
|
|
8873153178 | ||
|
|
b073df7666 | ||
|
|
cf6b767a9c | ||
|
|
d3d8e24824 | ||
|
|
c4df9730aa | ||
|
|
bfed19e6ad | ||
|
|
629967ac98 | ||
|
|
06e381ec9c | ||
|
|
d9ce9a6e47 | ||
|
|
e892b55b6b | ||
|
|
771d212198 | ||
|
|
83ba93944d | ||
|
|
de3ab006e2 | ||
|
|
9504b7f45c | ||
|
|
c91b356a96 | ||
|
|
118c1ad47f | ||
|
|
8bf4d3a9c1 | ||
|
|
1fcff673ba | ||
|
|
8c70c0cfb4 | ||
|
|
63d8d30088 | ||
|
|
6c0be84817 | ||
|
|
6059cb04d6 | ||
|
|
fdf0346556 | ||
|
|
0f9455715c | ||
|
|
0b3a1ed278 | ||
|
|
76bf68268f | ||
|
|
99a86c0fac | ||
|
|
f0de9ef02c | ||
|
|
b4ddcbdcfc | ||
|
|
1e22bcd527 | ||
|
|
75716ca269 | ||
|
|
8a6f1dab0e | ||
|
|
621853e2e3 | ||
|
|
5e4cb2d1b5 | ||
|
|
b4abd40698 | ||
|
|
2d1ccda2ef | ||
|
|
54934e1492 | ||
|
|
cfa29743a8 | ||
|
|
4a4a96d805 | ||
|
|
f0b8052a52 | ||
|
|
655cd8a140 | ||
|
|
dabc88a053 | ||
|
|
9cdefcea09 | ||
|
|
5748af4c98 | ||
|
|
3e9b1876d9 | ||
|
|
795bedae76 | ||
|
|
891dd521ab | ||
|
|
0fb89e23ad | ||
|
|
097f7d5aaa | ||
|
|
32e066464a | ||
|
|
55c7f57f25 | ||
|
|
3f7a57a417 | ||
|
|
bb64baec67 | ||
|
|
5f13636b49 | ||
|
|
4513e602d6 | ||
|
|
382e15013a | ||
|
|
3ce31c5a6a | ||
|
|
a48cf92a5b | ||
|
|
43f6f139d2 | ||
|
|
fd1425666d | ||
|
|
de075153b0 | ||
|
|
02dfc7dd4a | ||
|
|
83ce5872d7 | ||
|
|
272efeca74 | ||
|
|
ec6f5f3920 | ||
|
|
b62d23bd87 | ||
|
|
51794aa912 | ||
|
|
eb42651401 | ||
|
|
99c6dc2a87 |
16
.github/workflows/test.yml
vendored
16
.github/workflows/test.yml
vendored
@@ -57,12 +57,12 @@ jobs:
|
||||
|
||||
- name: Setup cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
|
||||
- name: Install cargo hack
|
||||
run: cargo install cargo-hack --debug
|
||||
|
||||
- name: Check with cargo hack
|
||||
run: cargo hack check --feature-powerset --depth 3
|
||||
run: cargo hack check --feature-powerset --depth 3 --at-least-one-of aws-lc-rs,ring --at-least-one-of rustls-native-certs,webpki-roots
|
||||
|
||||
test:
|
||||
name: test / ${{ matrix.name }}
|
||||
@@ -75,8 +75,8 @@ jobs:
|
||||
rust: stable
|
||||
- name: beta
|
||||
rust: beta
|
||||
- name: '1.70'
|
||||
rust: '1.70'
|
||||
- name: '1.74'
|
||||
rust: '1.74'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
- name: Setup cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install postfix
|
||||
- name: Install postfix
|
||||
run: |
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get -y install postfix
|
||||
@@ -119,10 +119,10 @@ jobs:
|
||||
run: cargo test
|
||||
|
||||
- name: Test with all features (-native-tls)
|
||||
run: cargo test --no-default-features --features async-std1,async-std1-rustls-tls,boring-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,pool,rustls-native-certs,rustls-tls,sendmail-transport,smtp-transport,tokio1,tokio1-boring-tls,tokio1-rustls-tls,tracing
|
||||
|
||||
run: cargo test --no-default-features --features async-std1,async-std1-rustls,aws-lc-rs,rustls-native-certs,boring-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,pool,rustls-native-certs,rustls,sendmail-transport,smtp-transport,tokio1,tokio1-boring-tls,tokio1-rustls,tracing
|
||||
|
||||
- name: Test with all features (-boring-tls)
|
||||
run: cargo test --no-default-features --features async-std1,async-std1-rustls-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,native-tls,pool,rustls-native-certs,rustls-tls,sendmail-transport,smtp-transport,tokio1,tokio1-native-tls,tokio1-rustls-tls,tracing
|
||||
run: cargo test --no-default-features --features async-std1,async-std1-rustls,aws-lc-rs,rustls-native-certs,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,native-tls,pool,rustls-native-certs,rustls,sendmail-transport,smtp-transport,tokio1,tokio1-native-tls,tokio1-rustls,tracing
|
||||
|
||||
# coverage:
|
||||
# name: Coverage
|
||||
|
||||
242
CHANGELOG.md
242
CHANGELOG.md
@@ -1,3 +1,243 @@
|
||||
<a name="v0.11.19"></a>
|
||||
### v0.11.19 (2025-10-08)
|
||||
|
||||
#### Features
|
||||
|
||||
* Add raw header setter to `MessageBuilder` ([#1108])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Fix README example ([#1114])
|
||||
* Replace custom `static_assert!` macro with `std::assert!` ([#1112])
|
||||
|
||||
[#1108]: https://github.com/lettre/lettre/pull/1108
|
||||
[#1112]: https://github.com/lettre/lettre/pull/1112
|
||||
[#1114]: https://github.com/lettre/lettre/pull/1114
|
||||
|
||||
<a name="v0.11.18"></a>
|
||||
### v0.11.18 (2025-07-28)
|
||||
|
||||
#### Features
|
||||
|
||||
* Allow inline attachments to be named ([#1101])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Upgrade `socket2` to v0.6 ([#1098])
|
||||
|
||||
[#1098]: https://github.com/lettre/lettre/pull/1098
|
||||
[#1101]: https://github.com/lettre/lettre/pull/1101
|
||||
|
||||
<a name="v0.11.17"></a>
|
||||
### v0.11.17 (2025-06-06)
|
||||
|
||||
#### Features
|
||||
|
||||
* Add support for `rustls-platform-verifier` ([#1081])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Change readme example to use `Mailbox::new` instead of string parsing ([#1090])
|
||||
* Replace futures-util `Mutex` with std `Mutex` in `AsyncStubTransport` ([#1091])
|
||||
* Avoid duplicate `abort_concurrent` implementation ([#1092])
|
||||
|
||||
[#1081]: https://github.com/lettre/lettre/pull/1081
|
||||
[#1090]: https://github.com/lettre/lettre/pull/1090
|
||||
[#1091]: https://github.com/lettre/lettre/pull/1091
|
||||
[#1092]: https://github.com/lettre/lettre/pull/1092
|
||||
|
||||
<a name="v0.11.16"></a>
|
||||
### v0.11.16 (2025-05-12)
|
||||
|
||||
#### Features
|
||||
|
||||
* Always implement `Clone` for `AsyncFileTransport` ([#1075])
|
||||
|
||||
#### Changes
|
||||
|
||||
* `Tls`, `CertificateStore`, `TlsParameters`, `TlsParametersBuilder`, `Certificate` and `Identity`
|
||||
are now marked as deprecated when no TLS backend is enabled. They will be properly feature gated
|
||||
in lettre v0.12 ([#1084])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Gate `web-time` behind `cfg(target_arch = "wasm32")]` ([#1086])
|
||||
* Add missing `#[doc(cfg(...))]` attributes ([#1086])
|
||||
* Upgrade `webpki-roots` to v1 ([#1088])
|
||||
* Cleanup internal `TlsParameters` and `(Async)NetworkStream` structures ([#1082])
|
||||
* Feature gate internal `TransportBuilder::tls` to avoid recursive call site warnings ([#1083])
|
||||
* Fix workaround for embedding cargo script in rustdoc output ([#1077])
|
||||
* Fix `clippy::io_other_error` warnings ([#1078])
|
||||
* Upgrade semver compatible dependencies ([#1076], [#1079], [#1080])
|
||||
|
||||
[#1075]: https://github.com/lettre/lettre/pull/1075
|
||||
[#1076]: https://github.com/lettre/lettre/pull/1076
|
||||
[#1077]: https://github.com/lettre/lettre/pull/1077
|
||||
[#1078]: https://github.com/lettre/lettre/pull/1078
|
||||
[#1079]: https://github.com/lettre/lettre/pull/1079
|
||||
[#1080]: https://github.com/lettre/lettre/pull/1080
|
||||
[#1082]: https://github.com/lettre/lettre/pull/1082
|
||||
[#1083]: https://github.com/lettre/lettre/pull/1083
|
||||
[#1084]: https://github.com/lettre/lettre/pull/1084
|
||||
[#1086]: https://github.com/lettre/lettre/pull/1086
|
||||
[#1088]: https://github.com/lettre/lettre/pull/1088
|
||||
|
||||
<a name="v0.11.15"></a>
|
||||
### v0.11.15 (2025-03-10)
|
||||
|
||||
#### Upgrade notes
|
||||
|
||||
* MSRV is now 1.74 ([#1060])
|
||||
|
||||
#### Features
|
||||
|
||||
* Add controlled shutdown methods ([#1045], [#1068])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Deny `unreachable_pub` lint ([#1058])
|
||||
* Bump minimum supported `rustls` ([#1063])
|
||||
* Bump minimum supported `serde` ([#1064])
|
||||
* Upgrade semver compatible dependencies ([#1067])
|
||||
* Upgrade `email-encoding` to v0.4 ([#1069])
|
||||
|
||||
[#1045]: https://github.com/lettre/lettre/pull/1045
|
||||
[#1058]: https://github.com/lettre/lettre/pull/1058
|
||||
[#1060]: https://github.com/lettre/lettre/pull/1060
|
||||
[#1063]: https://github.com/lettre/lettre/pull/1063
|
||||
[#1064]: https://github.com/lettre/lettre/pull/1064
|
||||
[#1067]: https://github.com/lettre/lettre/pull/1067
|
||||
[#1068]: https://github.com/lettre/lettre/pull/1068
|
||||
[#1069]: https://github.com/lettre/lettre/pull/1069
|
||||
|
||||
<a name="v0.11.14"></a>
|
||||
### v0.11.14 (2025-02-23)
|
||||
|
||||
This release deprecates the `rustls-tls`, `tokio1-rustls-tls` and `async-std1-rustls-tls`
|
||||
features, which will be removed in lettre v0.12.
|
||||
|
||||
rustls users should start migrating to the `rustls`, `tokio1-rustls` and
|
||||
`async-std1-rustls` features. Unlike the deprecated _*rustls-tls_ features,
|
||||
which automatically enabled the `ring` and `webpki-roots` backends, the new
|
||||
features do not. To complete the migration, users must either enable the
|
||||
`aws-lc-rs` or the `ring` feature. Additionally, those who rely on `webpki-roots`
|
||||
for TLS certificate verification must now explicitly enable its feature.
|
||||
Users of `rustls-native-certs` do not need to enable `webpki-roots`.
|
||||
|
||||
Find out more about the new features via the [lettre rustls docs].
|
||||
|
||||
#### Features
|
||||
|
||||
* Make it possible to use different `rustls` crypto providers and TLS verifiers ([#1054])
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Use the same `rustls` crypto provider everywhere ([#1055])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Deprecate `AsyncNetworkStream` being public ([#1059])
|
||||
* Upgrade `nom` to v8 ([#1048])
|
||||
* Drop `rustls-pemfile` in favor of `rustls-pki-types` APIs ([#1050])
|
||||
* Ban direct use of `std::time::SystemTime::now` via clippy ([#1043])
|
||||
* Drop direct dependency on `rustls-pki-types` ([#1051])
|
||||
* Remove artifact from `web-time` refactor ([#1049])
|
||||
* Fix warnings with `rustls-native-certs` when `tracing` is disabled ([#1053])
|
||||
* Bump license year ([#1057])
|
||||
* Cleanup `Cargo.toml` style ([#1047])
|
||||
|
||||
[lettre rustls docs]: https://docs.rs/lettre/0.11.14/lettre/index.html#smtp-over-tls-via-the-rustls-crate
|
||||
[#1043]: https://github.com/lettre/lettre/pull/1043
|
||||
[#1047]: https://github.com/lettre/lettre/pull/1047
|
||||
[#1048]: https://github.com/lettre/lettre/pull/1048
|
||||
[#1049]: https://github.com/lettre/lettre/pull/1049
|
||||
[#1050]: https://github.com/lettre/lettre/pull/1050
|
||||
[#1051]: https://github.com/lettre/lettre/pull/1051
|
||||
[#1053]: https://github.com/lettre/lettre/pull/1053
|
||||
[#1054]: https://github.com/lettre/lettre/pull/1054
|
||||
[#1055]: https://github.com/lettre/lettre/pull/1055
|
||||
[#1057]: https://github.com/lettre/lettre/pull/1057
|
||||
[#1059]: https://github.com/lettre/lettre/pull/1059
|
||||
|
||||
<a name="v0.11.13"></a>
|
||||
### v0.11.13 (2025-02-17)
|
||||
|
||||
#### Features
|
||||
|
||||
* Add WASM support ([#1037], [#1042])
|
||||
* Add method to get the TLS verify result with BoringSSL ([#1039])
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Synchronous pool shutdowns being arbitrarily delayed ([#1041])
|
||||
|
||||
[#1037]: https://github.com/lettre/lettre/pull/1037
|
||||
[#1039]: https://github.com/lettre/lettre/pull/1039
|
||||
[#1041]: https://github.com/lettre/lettre/pull/1041
|
||||
[#1042]: https://github.com/lettre/lettre/pull/1042
|
||||
|
||||
<a name="v0.11.12"></a>
|
||||
### v0.11.12 (2025-02-02)
|
||||
|
||||
#### Misc
|
||||
|
||||
* Warn against manually configuring `port` and `tls` on SMTP transport builder ([#1014])
|
||||
* Document variants of `Tls` enum ([#1015])
|
||||
* Fix rustdoc warnings ([#1016])
|
||||
* Add `ContentType::TEXT_PLAIN` to `Message` builder examples ([#1017])
|
||||
* Document `SmtpTransport` and `AsyncSmtpTransport` ([#1018])
|
||||
* Fix typo in transport builder `credentials` method ([#1019])
|
||||
* Document required system dependencies for OpenSSL ([#1030])
|
||||
* Improve docs for the `transport::smtp` module ([#1031])
|
||||
* Improve docs for smtp transport builder `from_url` ([#1032])
|
||||
* Replace `assert!` with `?` on `send` examples ([#1033])
|
||||
* Warn on more pedantic clippy lints and fix them ([#1035], [#1036])
|
||||
|
||||
[#1014]: https://github.com/lettre/lettre/pull/1014
|
||||
[#1015]: https://github.com/lettre/lettre/pull/1015
|
||||
[#1016]: https://github.com/lettre/lettre/pull/1016
|
||||
[#1017]: https://github.com/lettre/lettre/pull/1017
|
||||
[#1018]: https://github.com/lettre/lettre/pull/1018
|
||||
[#1019]: https://github.com/lettre/lettre/pull/1019
|
||||
[#1030]: https://github.com/lettre/lettre/pull/1030
|
||||
[#1031]: https://github.com/lettre/lettre/pull/1031
|
||||
[#1032]: https://github.com/lettre/lettre/pull/1032
|
||||
[#1033]: https://github.com/lettre/lettre/pull/1033
|
||||
[#1035]: https://github.com/lettre/lettre/pull/1035
|
||||
[#1036]: https://github.com/lettre/lettre/pull/1036
|
||||
|
||||
<a name="v0.11.11"></a>
|
||||
### v0.11.11 (2024-12-05)
|
||||
|
||||
#### Upgrade notes
|
||||
|
||||
* MSRV is now 1.71 ([#1008])
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Fix off-by-one error reaching the minimum number of configured pooled connections ([#1012])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Fix clippy warnings ([#1009])
|
||||
* Fix `-Zminimal-versions` build ([#1007])
|
||||
|
||||
[#1007]: https://github.com/lettre/lettre/pull/1007
|
||||
[#1008]: https://github.com/lettre/lettre/pull/1008
|
||||
[#1009]: https://github.com/lettre/lettre/pull/1009
|
||||
[#1012]: https://github.com/lettre/lettre/pull/1012
|
||||
|
||||
<a name="v0.11.10"></a>
|
||||
### v0.11.10 (2024-10-23)
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Ignore disconnect errors when `pool` feature of SMTP transport is disabled ([#999])
|
||||
* Use case insensitive comparisons for matching login challenge requests ([#1000])
|
||||
|
||||
[#999]: https://github.com/lettre/lettre/pull/999
|
||||
[#1000]: https://github.com/lettre/lettre/pull/1000
|
||||
|
||||
<a name="v0.11.9"></a>
|
||||
### v0.11.9 (2024-09-13)
|
||||
|
||||
@@ -508,6 +748,6 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
|
||||
|
||||
* multipart support
|
||||
* add non-consuming methods for Email builders
|
||||
* `add_header` does not return the builder anymore,
|
||||
* `add_header` does not return the builder anymore,
|
||||
for consistency with other methods. Use the `header`
|
||||
method instead
|
||||
|
||||
1228
Cargo.lock
generated
1228
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
56
Cargo.toml
56
Cargo.toml
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "lettre"
|
||||
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
|
||||
version = "0.11.9"
|
||||
version = "0.11.19"
|
||||
description = "Email client"
|
||||
readme = "README.md"
|
||||
homepage = "https://lettre.rs"
|
||||
@@ -11,7 +11,7 @@ authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo
|
||||
categories = ["email", "network-programming"]
|
||||
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
rust-version = "1.74"
|
||||
|
||||
[badges]
|
||||
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
||||
@@ -19,9 +19,12 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
email_address = { version = "0.2.1", default-features = false }
|
||||
chumsky = "0.9"
|
||||
idna = "1"
|
||||
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
|
||||
|
||||
## tracing support
|
||||
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true }
|
||||
|
||||
# builder
|
||||
httpdate = { version = "1", optional = true }
|
||||
@@ -29,27 +32,26 @@ mime = { version = "0.3.4", optional = true }
|
||||
fastrand = { version = "2.0", optional = true }
|
||||
quoted_printable = { version = "0.5", optional = true }
|
||||
base64 = { version = "0.22", optional = true }
|
||||
email-encoding = { version = "0.3", optional = true }
|
||||
email-encoding = { version = "0.4", optional = true }
|
||||
|
||||
# file transport
|
||||
uuid = { version = "1", features = ["v4"], optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
serde = { version = "1.0.110", features = ["derive"], optional = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
|
||||
# smtp-transport
|
||||
nom = { version = "7", optional = true }
|
||||
nom = { version = "8", optional = true }
|
||||
hostname = { version = "0.4", optional = true } # feature
|
||||
socket2 = { version = "0.5.1", optional = true }
|
||||
socket2 = { version = "0.6", optional = true }
|
||||
url = { version = "2.4", optional = true }
|
||||
percent-encoding = { version = "2.3", optional = true }
|
||||
|
||||
## tls
|
||||
native-tls = { version = "0.2.5", optional = true } # feature
|
||||
rustls = { version = "0.23.5", default-features = false, features = ["ring", "logging", "std", "tls12"], optional = true }
|
||||
rustls-pemfile = { version = "2", optional = true }
|
||||
native-tls = { version = "0.2.9", optional = true } # feature
|
||||
rustls = { version = "0.23.18", default-features = false, features = ["logging", "std", "tls12"], optional = true }
|
||||
rustls-platform-verifier = { version = "0.6.0", 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 = "1.0.0", optional = true }
|
||||
boring = { version = "4", optional = true }
|
||||
|
||||
# async
|
||||
@@ -59,21 +61,22 @@ async-trait = { version = "0.1", optional = true }
|
||||
|
||||
## async-std
|
||||
async-std = { version = "1.8", optional = true }
|
||||
futures-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12", "ring"], optional = true }
|
||||
futures-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12"], optional = true }
|
||||
|
||||
## tokio
|
||||
tokio1_crate = { package = "tokio", version = "1", optional = true }
|
||||
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
|
||||
tokio1_rustls = { package = "tokio-rustls", version = "0.26", default-features = false, features = ["logging", "tls12", "ring"], optional = true }
|
||||
tokio1_rustls = { package = "tokio-rustls", version = "0.26", default-features = false, features = ["logging", "tls12"], optional = true }
|
||||
tokio1_boring = { package = "tokio-boring", version = "4", optional = true }
|
||||
|
||||
## dkim
|
||||
sha2 = { version = "0.10", optional = true, features = ["oid"] }
|
||||
sha2 = { version = "0.10", features = ["oid"], optional = true }
|
||||
rsa = { version = "0.9", optional = true }
|
||||
ed25519-dalek = { version = "2", optional = true }
|
||||
|
||||
# email formats
|
||||
email_address = { version = "0.2.1", default-features = false }
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
## web-time for wasm support
|
||||
web-time = { version = "1.1.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
@@ -108,20 +111,33 @@ smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:url", "dep:percen
|
||||
|
||||
pool = ["dep:futures-util"]
|
||||
|
||||
rustls-tls = ["dep:webpki-roots", "dep:rustls", "dep:rustls-pemfile", "dep:rustls-pki-types"]
|
||||
rustls = ["dep:rustls"]
|
||||
aws-lc-rs = ["rustls?/aws-lc-rs"]
|
||||
fips = ["aws-lc-rs", "rustls?/fips"]
|
||||
ring = ["rustls?/ring"]
|
||||
webpki-roots = ["dep:webpki-roots"]
|
||||
# deprecated
|
||||
rustls-tls = ["webpki-roots", "rustls", "ring"]
|
||||
|
||||
boring-tls = ["dep:boring"]
|
||||
|
||||
# async
|
||||
async-std1 = ["dep:async-std", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
|
||||
async-std1-rustls-tls = ["async-std1", "rustls-tls", "dep:futures-rustls"]
|
||||
async-std1-rustls = ["async-std1", "rustls", "dep:futures-rustls"]
|
||||
# deprecated
|
||||
async-std1-rustls-tls = ["async-std1-rustls", "rustls-tls"]
|
||||
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-rustls-tls = ["tokio1", "rustls-tls", "dep:tokio1_rustls"]
|
||||
tokio1-rustls = ["tokio1", "rustls", "dep:tokio1_rustls"]
|
||||
# deprecated
|
||||
tokio1-rustls-tls = ["tokio1-rustls", "rustls-tls"]
|
||||
tokio1-boring-tls = ["tokio1", "boring-tls", "dep:tokio1_boring"]
|
||||
|
||||
dkim = ["dep:base64", "dep:sha2", "dep:rsa", "dep:ed25519-dalek"]
|
||||
|
||||
# wasm support
|
||||
web = ["dep:web-time"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(lettre_ignore_tls_mismatch)'] }
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,5 +1,5 @@
|
||||
Copyright (c) 2014-2024 Alexis Mousset <contact@amousset.me>
|
||||
Copyright (c) 2019-2024 Paolo Barbolini <paolo@paolo565.org>
|
||||
Copyright (c) 2019-2025 Paolo Barbolini <paolo@paolo565.org>
|
||||
Copyright (c) 2018 K. <kayo@illumium.org>
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
|
||||
18
README.md
18
README.md
@@ -28,8 +28,8 @@
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://deps.rs/crate/lettre/0.11.9">
|
||||
<img src="https://deps.rs/crate/lettre/0.11.9/status.svg"
|
||||
<a href="https://deps.rs/crate/lettre/0.11.19">
|
||||
<img src="https://deps.rs/crate/lettre/0.11.19/status.svg"
|
||||
alt="dependency status" />
|
||||
</a>
|
||||
</div>
|
||||
@@ -53,12 +53,12 @@ Lettre does not provide (for now):
|
||||
## Supported Rust Versions
|
||||
|
||||
Lettre supports all Rust versions released in the last 6 months. At the time of writing
|
||||
the minimum supported Rust version is 1.70, but this could change at any time either from
|
||||
the minimum supported Rust version is 1.74, but this could change at any time either from
|
||||
one of our dependencies bumping their MSRV or by a new patch release of lettre.
|
||||
|
||||
## Example
|
||||
|
||||
This library requires Rust 1.70 or newer.
|
||||
This library requires Rust 1.74 or newer.
|
||||
To use this library, add the following to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
@@ -67,15 +67,15 @@ lettre = "0.11"
|
||||
```
|
||||
|
||||
```rust,no_run
|
||||
use lettre::message::header::ContentType;
|
||||
use lettre::message::{Mailbox, header::ContentType};
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
|
||||
fn main() {
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.from(Mailbox::new(Some("NoBody".to_owned()), "nobody@domain.tld".parse().unwrap()))
|
||||
.reply_to(Mailbox::new(Some("Yuin".to_owned()), "yuin@domain.tld".parse().unwrap()))
|
||||
.to(Mailbox::new(Some("Hei".to_owned()), "hei@domain.tld".parse().unwrap()))
|
||||
.subject("Happy new year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy!"))
|
||||
@@ -107,7 +107,7 @@ cargo run --example autoconfigure SMTP_HOST
|
||||
|
||||
## Testing
|
||||
|
||||
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. If you have python installed
|
||||
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. If you have python installed
|
||||
such a server can be launched with `python -m smtpd -n -c DebuggingServer 127.0.0.1:2525`
|
||||
|
||||
Alternatively only unit tests can be run by doing `cargo test --lib`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
|
||||
|
||||
fn bench_simple_send(c: &mut Criterion) {
|
||||
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
@@ -13,6 +13,7 @@ fn bench_simple_send(c: &mut Criterion) {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
let result = black_box(sender.send(&email));
|
||||
@@ -32,6 +33,7 @@ fn bench_reuse_send(c: &mut Criterion) {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
let result = black_box(sender.send(&email));
|
||||
|
||||
3
clippy.toml
Normal file
3
clippy.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
disallowed-methods = [
|
||||
{ "path" = "std::time::SystemTime::now", reason = "does not work on WASM environments", replacement = "crate::time::now" }
|
||||
]
|
||||
@@ -29,7 +29,7 @@ pub struct Envelope {
|
||||
mod serde_forward_path {
|
||||
use super::Address;
|
||||
/// dummy type required for serde
|
||||
/// see example: https://serde.rs/deserialize-map.html
|
||||
/// see example: <https://serde.rs/deserialize-map.html>
|
||||
struct CustomVisitor;
|
||||
impl<'de> serde::de::Visitor<'de> for CustomVisitor {
|
||||
type Value = Vec<Address>;
|
||||
@@ -53,7 +53,7 @@ mod serde_forward_path {
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Address>, D::Error>
|
||||
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Address>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
@@ -163,6 +163,7 @@ impl Envelope {
|
||||
}
|
||||
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
impl TryFrom<&Headers> for Envelope {
|
||||
type Error = Error;
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ impl<'de> Deserialize<'de> for Address {
|
||||
{
|
||||
struct FieldVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for FieldVisitor {
|
||||
impl Visitor<'_> for FieldVisitor {
|
||||
type Value = Field;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||
|
||||
@@ -45,6 +45,7 @@ use crate::transport::smtp::Error;
|
||||
#[async_trait]
|
||||
pub trait Executor: Debug + Send + Sync + 'static + private::Sealed {
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
#[allow(private_bounds)]
|
||||
type Handle: SpawnHandle;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Sleep: Future<Output = ()> + Send + 'static;
|
||||
@@ -82,8 +83,8 @@ pub trait Executor: Debug + Send + Sync + 'static + private::Sealed {
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
#[async_trait]
|
||||
pub trait SpawnHandle: Debug + Send + Sync + 'static + private::Sealed {
|
||||
async fn shutdown(self);
|
||||
pub(crate) trait SpawnHandle: Debug + Send + Sync + 'static + private::Sealed {
|
||||
async fn shutdown(&self);
|
||||
}
|
||||
|
||||
/// Async [`Executor`] using `tokio` `1.x`
|
||||
@@ -133,7 +134,7 @@ impl Executor for Tokio1Executor {
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match tls {
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls"))]
|
||||
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
|
||||
_ => None,
|
||||
};
|
||||
@@ -147,7 +148,7 @@ impl Executor for Tokio1Executor {
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls"))]
|
||||
match tls {
|
||||
Tls::Opportunistic(tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
@@ -177,7 +178,7 @@ impl Executor for Tokio1Executor {
|
||||
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
|
||||
#[async_trait]
|
||||
impl SpawnHandle for tokio1_crate::task::JoinHandle<()> {
|
||||
async fn shutdown(self) {
|
||||
async fn shutdown(&self) {
|
||||
self.abort();
|
||||
}
|
||||
}
|
||||
@@ -201,7 +202,7 @@ pub struct AsyncStd1Executor;
|
||||
#[cfg(feature = "async-std1")]
|
||||
impl Executor for AsyncStd1Executor {
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Handle = async_std::task::JoinHandle<()>;
|
||||
type Handle = futures_util::future::AbortHandle;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Sleep = BoxFuture<'static, ()>;
|
||||
|
||||
@@ -211,7 +212,9 @@ impl Executor for AsyncStd1Executor {
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
async_std::task::spawn(fut)
|
||||
let (handle, registration) = futures_util::future::AbortHandle::new_pair();
|
||||
async_std::task::spawn(futures_util::future::Abortable::new(fut, registration));
|
||||
handle
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
@@ -230,7 +233,7 @@ impl Executor for AsyncStd1Executor {
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match tls {
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
|
||||
_ => None,
|
||||
};
|
||||
@@ -243,7 +246,7 @@ impl Executor for AsyncStd1Executor {
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
match tls {
|
||||
Tls::Opportunistic(tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
@@ -272,9 +275,9 @@ impl Executor for AsyncStd1Executor {
|
||||
|
||||
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
|
||||
#[async_trait]
|
||||
impl SpawnHandle for async_std::task::JoinHandle<()> {
|
||||
async fn shutdown(self) {
|
||||
self.cancel().await;
|
||||
impl SpawnHandle for futures_util::future::AbortHandle {
|
||||
async fn shutdown(&self) {
|
||||
self.abort();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,5 +294,5 @@ mod private {
|
||||
impl Sealed for tokio1_crate::task::JoinHandle<()> {}
|
||||
|
||||
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
|
||||
impl Sealed for async_std::task::JoinHandle<()> {}
|
||||
impl Sealed for futures_util::future::AbortHandle {}
|
||||
}
|
||||
|
||||
129
src/lib.rs
129
src/lib.rs
@@ -6,7 +6,7 @@
|
||||
//! * Secure defaults
|
||||
//! * Async support
|
||||
//!
|
||||
//! Lettre requires Rust 1.70 or newer.
|
||||
//! Lettre requires Rust 1.74 or newer.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
@@ -34,13 +34,25 @@
|
||||
//!
|
||||
//! _Secure SMTP connections using TLS from the `native-tls` crate_
|
||||
//!
|
||||
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL on Linux.
|
||||
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL
|
||||
//! on all other platforms.
|
||||
//!
|
||||
//! * **native-tls** 📫: TLS support for the synchronous version of the API
|
||||
//! * **tokio1-native-tls**: TLS support for the `tokio1` async version of the API
|
||||
//!
|
||||
//! NOTE: native-tls isn't supported with `async-std`
|
||||
//!
|
||||
//! ##### Building lettre with OpenSSL
|
||||
//!
|
||||
//! When building lettre with native-tls on a system that makes
|
||||
//! use of OpenSSL, the following packages will need to be installed
|
||||
//! in order for the build and the compiled program to run properly.
|
||||
//!
|
||||
//! | Distro | Build-time packages | Runtime packages |
|
||||
//! | ------------ | -------------------------- | ---------------------------- |
|
||||
//! | Debian | `pkg-config`, `libssl-dev` | `libssl3`, `ca-certificates` |
|
||||
//! | Alpine Linux | `pkgconf`, `openssl-dev` | `libssl3`, `ca-certificates` |
|
||||
//!
|
||||
//! #### SMTP over TLS via the boring crate (Boring TLS)
|
||||
//!
|
||||
//! _Secure SMTP connections using TLS from the `boring-tls` crate_
|
||||
@@ -52,13 +64,49 @@
|
||||
//!
|
||||
//! #### SMTP over TLS via the rustls crate
|
||||
//!
|
||||
//! _Secure SMTP connections using TLS from the `rustls-tls` crate_
|
||||
//! _Secure SMTP connections using TLS from the `rustls` crate_
|
||||
//!
|
||||
//! Rustls uses [ring] as the cryptography implementation. As a result, [not all Rust's targets are supported][ring-support].
|
||||
//! * **rustls**: TLS support for the synchronous version of the API
|
||||
//! * **tokio1-rustls**: TLS support for the `tokio1` async version of the API
|
||||
//! * **async-std1-rustls**: TLS support for the `async-std1` async version of the API
|
||||
//!
|
||||
//! * **rustls-tls**: TLS support for the synchronous version of the API
|
||||
//! * **tokio1-rustls-tls**: TLS support for the `tokio1` async version of the API
|
||||
//! * **async-std1-rustls-tls**: TLS support for the `async-std1` async version of the API
|
||||
//! ##### rustls crypto backends
|
||||
//!
|
||||
//! _The crypto implementation to use with rustls_
|
||||
//!
|
||||
//! When the `rustls` feature is enabled, one of the following crypto backends MUST also
|
||||
//! be enabled.
|
||||
//!
|
||||
//! * **aws-lc-rs**: use [AWS-LC] (via [`aws-lc-rs`]) as the `rustls` crypto backend
|
||||
//! * **ring**: use [`ring`] as the `rustls` crypto backend
|
||||
//!
|
||||
//! When enabling `aws-lc-rs`, the `fips` feature can also be enabled to have
|
||||
//! rustls use the FIPS certified module of AWS-LC.
|
||||
//!
|
||||
//! `aws-lc-rs` may require cmake on some platforms to compile.
|
||||
//! `fips` always requires cmake and the Go compiler to compile.
|
||||
//!
|
||||
//! ##### rustls certificate verification backend
|
||||
//!
|
||||
//! _The TLS certificate verification backend to use with rustls_
|
||||
//!
|
||||
//! When the `rustls` feature is enabled, one of the following verification backends
|
||||
//! MUST also be enabled.
|
||||
//!
|
||||
//! * **rustls-platform-verifier**: verify TLS certificate using the OS's native certificate store (see [`rustls-platform-verifier`])
|
||||
//! * **rustls-native-certs**: verify TLS certificates using the platform's native certificate store (see [`rustls-native-certs`]) - when in doubt use `rustls-platform-verifier`
|
||||
//! * **webpki-roots**: verify TLS certificates against Mozilla's root certificates (see [`webpki-roots`])
|
||||
//!
|
||||
//! The following packages will need to be installed in order for the build
|
||||
//! stage and the compiled program to run properly.
|
||||
//!
|
||||
//! | Verification backend | Distro | Build-time packages | Runtime packages |
|
||||
//! | --------------------- | ------------ | -------------------------- | ---------------------------- |
|
||||
//! | `rustls-platform-verifier` | Debian | none | `ca-certificates` |
|
||||
//! | `rustls-platform-verifier` | Alpine Linux | none | `ca-certificates` |
|
||||
//! | `rustls-native-certs` | Debian | none | `ca-certificates` |
|
||||
//! | `rustls-native-certs` | Alpine Linux | none | `ca-certificates` |
|
||||
//! | `webpki-roots` | any | none | none |
|
||||
//!
|
||||
//! ### Sendmail transport
|
||||
//!
|
||||
@@ -95,6 +143,7 @@
|
||||
//! * **tracing**: Logging using the `tracing` crate
|
||||
//! * **mime03**: Allow creating a [`ContentType`] from an existing [mime 0.3] `Mime` struct
|
||||
//! * **dkim**: Add support for signing email with DKIM
|
||||
//! * **web**: WebAssembly support using the `web-time` crate for time operations
|
||||
//!
|
||||
//! [`SMTP`]: crate::transport::smtp
|
||||
//! [`sendmail`]: crate::transport::sendmail
|
||||
@@ -102,18 +151,23 @@
|
||||
//! [`ContentType`]: crate::message::header::ContentType
|
||||
//! [tokio]: https://docs.rs/tokio/1
|
||||
//! [async-std]: https://docs.rs/async-std/1
|
||||
//! [ring]: https://github.com/briansmith/ring#ring
|
||||
//! [ring-support]: https://github.com/briansmith/ring#online-automated-testing
|
||||
//! [AWS-LC]: https://github.com/aws/aws-lc
|
||||
//! [`aws-lc-rs`]: https://crates.io/crates/aws-lc-rs
|
||||
//! [`ring`]: https://crates.io/crates/ring
|
||||
//! [`rustls-platform-verifier`]: https://crates.io/crates/rustls-platform-verifier
|
||||
//! [`rustls-native-certs`]: https://crates.io/crates/rustls-native-certs
|
||||
//! [`webpki-roots`]: https://crates.io/crates/webpki-roots
|
||||
//! [Tokio 1.x]: https://docs.rs/tokio/1
|
||||
//! [async-std 1.x]: https://docs.rs/async-std/1
|
||||
//! [mime 0.3]: https://docs.rs/mime/0.3
|
||||
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
|
||||
|
||||
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.9")]
|
||||
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.19")]
|
||||
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(
|
||||
unreachable_pub,
|
||||
missing_copy_implementations,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
@@ -137,12 +191,36 @@
|
||||
clippy::wildcard_imports,
|
||||
clippy::str_to_string,
|
||||
clippy::empty_structs_with_brackets,
|
||||
clippy::zero_sized_map_values
|
||||
clippy::zero_sized_map_values,
|
||||
clippy::manual_let_else,
|
||||
clippy::semicolon_if_nothing_returned,
|
||||
clippy::unnecessary_wraps,
|
||||
clippy::doc_markdown,
|
||||
clippy::explicit_iter_loop,
|
||||
clippy::redundant_closure_for_method_calls,
|
||||
// Rust 1.86: clippy::unnecessary_semicolon,
|
||||
)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
#[cfg(not(lettre_ignore_tls_mismatch))]
|
||||
mod compiletime_checks {
|
||||
#[cfg(all(feature = "rustls", not(feature = "aws-lc-rs"), not(feature = "ring")))]
|
||||
compile_error!(
|
||||
"feature `rustls` also requires either the `aws-lc-rs` or the `ring` feature to
|
||||
be enabled"
|
||||
);
|
||||
|
||||
#[cfg(all(
|
||||
feature = "rustls",
|
||||
not(feature = "rustls-platform-verifier"),
|
||||
not(feature = "rustls-native-certs"),
|
||||
not(feature = "webpki-roots")
|
||||
))]
|
||||
compile_error!(
|
||||
"feature `rustls` also requires either the `rustls-platform-verifier`, the `rustls-native-certs`
|
||||
or the `webpki-roots` feature to be enabled"
|
||||
);
|
||||
|
||||
#[cfg(all(feature = "native-tls", feature = "boring-tls"))]
|
||||
compile_error!("feature \"native-tls\" and feature \"boring-tls\" cannot be enabled at the same time, otherwise
|
||||
the executable will fail to link.");
|
||||
@@ -153,16 +231,12 @@ mod compiletime_checks {
|
||||
not(feature = "tokio1-native-tls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `tokio1` and the `native-tls` features, but the `tokio1-native-tls` feature hasn't been turned on.
|
||||
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` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
|
||||
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||
|
||||
#[cfg(all(
|
||||
feature = "tokio1",
|
||||
feature = "rustls-tls",
|
||||
not(feature = "tokio1-rustls-tls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `tokio1` and the `rustls-tls` features, but the `tokio1-rustls-tls` feature hasn't been turned on.
|
||||
If you'd like to use `native-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
|
||||
#[cfg(all(feature = "tokio1", feature = "rustls", not(feature = "tokio1-rustls")))]
|
||||
compile_error!("Lettre is being built with the `tokio1` and the `rustls` features, but the `tokio1-rustls` feature hasn't been turned on.
|
||||
If you'd like to use `native-tls` make sure that the `rustls` feature hasn't been enabled by mistake.
|
||||
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||
|
||||
#[cfg(all(
|
||||
@@ -171,22 +245,22 @@ mod compiletime_checks {
|
||||
not(feature = "tokio1-boring-tls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `tokio1` and the `boring-tls` features, but the `tokio1-boring-tls` feature hasn't been turned on.
|
||||
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` feature hasn't been enabled by mistake.
|
||||
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||
|
||||
#[cfg(all(feature = "async-std1", feature = "native-tls",))]
|
||||
#[cfg(all(feature = "async-std1", feature = "native-tls"))]
|
||||
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the async-std integration doesn't support native-tls yet.
|
||||
If you'd like to work on the issue please take a look at https://github.com/lettre/lettre/issues/576.
|
||||
If you were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
|
||||
If you were trying to opt into `rustls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
|
||||
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||
|
||||
#[cfg(all(
|
||||
feature = "async-std1",
|
||||
feature = "rustls-tls",
|
||||
not(feature = "async-std1-rustls-tls")
|
||||
feature = "rustls",
|
||||
not(feature = "async-std1-rustls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `async-std1` and the `rustls-tls` features, but the `async-std1-rustls-tls` feature hasn't been turned on.
|
||||
If you'd like to use `native-tls` make sure that the `rustls-tls` hasn't been enabled by mistake.
|
||||
compile_error!("Lettre is being built with the `async-std1` and the `rustls` features, but the `async-std1-rustls` feature hasn't been turned on.
|
||||
If you'd like to use `native-tls` make sure that the `rustls` hasn't been enabled by mistake.
|
||||
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||
}
|
||||
|
||||
@@ -199,6 +273,9 @@ mod executor;
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
pub mod message;
|
||||
#[cfg(feature = "rustls")]
|
||||
mod rustls_crypto;
|
||||
mod time;
|
||||
pub mod transport;
|
||||
|
||||
use std::error::Error as StdError;
|
||||
|
||||
@@ -16,7 +16,10 @@ enum Disposition {
|
||||
/// File name
|
||||
Attached(String),
|
||||
/// Content id
|
||||
Inline(String),
|
||||
Inline {
|
||||
content_id: String,
|
||||
name: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Attachment {
|
||||
@@ -81,7 +84,50 @@ impl Attachment {
|
||||
/// ```
|
||||
pub fn new_inline(content_id: String) -> Self {
|
||||
Attachment {
|
||||
disposition: Disposition::Inline(content_id),
|
||||
disposition: Disposition::Inline {
|
||||
content_id,
|
||||
name: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new inline attachment giving it a name
|
||||
///
|
||||
/// This attachment should be displayed inline into the message
|
||||
/// body:
|
||||
///
|
||||
/// ```html
|
||||
/// <img src="cid:123">
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::error::Error;
|
||||
/// use std::fs;
|
||||
///
|
||||
/// use lettre::message::{header::ContentType, Attachment};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let content_id = String::from("123");
|
||||
/// let file_name = String::from("image.jpg");
|
||||
/// # if false {
|
||||
/// let filebody = fs::read(&file_name)?;
|
||||
/// # }
|
||||
/// # let filebody = fs::read("docs/lettre.png")?;
|
||||
/// let content_type = ContentType::parse("image/jpeg").unwrap();
|
||||
/// let attachment =
|
||||
/// Attachment::new_inline_with_name(content_id, file_name).body(filebody, content_type);
|
||||
///
|
||||
/// // The image `attachment` will display inline into the email.
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new_inline_with_name(content_id: String, name: String) -> Self {
|
||||
Attachment {
|
||||
disposition: Disposition::Inline {
|
||||
content_id,
|
||||
name: Some(name),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,9 +141,18 @@ impl Attachment {
|
||||
Disposition::Attached(filename) => {
|
||||
builder.header(header::ContentDisposition::attachment(&filename))
|
||||
}
|
||||
Disposition::Inline(content_id) => builder
|
||||
Disposition::Inline {
|
||||
content_id,
|
||||
name: None,
|
||||
} => builder
|
||||
.header(header::ContentId::from(format!("<{content_id}>")))
|
||||
.header(header::ContentDisposition::inline()),
|
||||
Disposition::Inline {
|
||||
content_id,
|
||||
name: Some(name),
|
||||
} => builder
|
||||
.header(header::ContentId::from(format!("<{content_id}>")))
|
||||
.header(header::ContentDisposition::inline_with_name(&name)),
|
||||
};
|
||||
builder = builder.header(content_type);
|
||||
builder.body(content)
|
||||
@@ -142,4 +197,24 @@ mod tests {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_inline_with_name() {
|
||||
let id = String::from("id");
|
||||
let name = String::from("test");
|
||||
let part = super::Attachment::new_inline_with_name(id, name).body(
|
||||
String::from("Hello world!"),
|
||||
ContentType::parse("text/plain").unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
&String::from_utf8_lossy(&part.formatted()),
|
||||
concat!(
|
||||
"Content-ID: <id>\r\n",
|
||||
"Content-Disposition: inline; filename=\"test\"\r\n",
|
||||
"Content-Type: text/plain\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n\r\n",
|
||||
"Hello world!\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ impl Display for DkimSigningAlgorithm {
|
||||
}
|
||||
}
|
||||
|
||||
/// Describe DkimSigning key error
|
||||
/// Describe [`DkimSigningKey`] key error
|
||||
#[derive(Debug)]
|
||||
pub struct DkimSigningKeyError(InnerDkimSigningKeyError);
|
||||
|
||||
@@ -100,7 +100,7 @@ impl StdError for DkimSigningKeyError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Describe a signing key to be carried by DkimConfig struct
|
||||
/// Describe a signing key to be carried by [`DkimConfig`] struct
|
||||
#[derive(Debug)]
|
||||
pub struct DkimSigningKey(InnerDkimSigningKey);
|
||||
|
||||
@@ -183,7 +183,7 @@ impl DkimConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a DkimConfig
|
||||
/// Create a [`DkimConfig`]
|
||||
pub fn new(
|
||||
selector: String,
|
||||
domain: String,
|
||||
@@ -283,19 +283,19 @@ fn dkim_canonicalize_headers_relaxed(headers: &str) -> String {
|
||||
// End of header.
|
||||
[b'\r', b'\n', ..] => {
|
||||
*out += "\r\n";
|
||||
name(&h[2..], out)
|
||||
name(&h[2..], out);
|
||||
}
|
||||
// Sequential whitespace.
|
||||
[b' ' | b'\t', b' ' | b'\t' | b'\r', ..] => value(&h[1..], out),
|
||||
// All whitespace becomes spaces.
|
||||
[b'\t', ..] => {
|
||||
out.push(' ');
|
||||
value(&h[1..], out)
|
||||
value(&h[1..], out);
|
||||
}
|
||||
[_, ..] => {
|
||||
let mut chars = h.chars();
|
||||
out.push(chars.next().unwrap());
|
||||
value(chars.as_str(), out)
|
||||
value(chars.as_str(), out);
|
||||
}
|
||||
[] => {}
|
||||
}
|
||||
@@ -317,7 +317,7 @@ fn dkim_canonicalize_header_tag(
|
||||
}
|
||||
}
|
||||
|
||||
/// Canonicalize signed headers passed as headers_list among mail_headers using canonicalization
|
||||
/// Canonicalize signed headers passed as `headers_list` among `mail_headers` using canonicalization
|
||||
fn dkim_canonicalize_headers<'a>(
|
||||
headers_list: impl IntoIterator<Item = &'a str>,
|
||||
mail_headers: &Headers,
|
||||
@@ -344,10 +344,9 @@ fn dkim_canonicalize_headers<'a>(
|
||||
}
|
||||
|
||||
/// Sign with Dkim a message by adding Dkim-Signature header created with configuration expressed by
|
||||
/// dkim_config
|
||||
|
||||
/// `dkim_config`
|
||||
pub fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
|
||||
dkim_sign_fixed_time(message, dkim_config, SystemTime::now())
|
||||
dkim_sign_fixed_time(message, dkim_config, crate::time::now());
|
||||
}
|
||||
|
||||
fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timestamp: SystemTime) {
|
||||
@@ -378,7 +377,7 @@ fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timesta
|
||||
}
|
||||
let dkim_header = dkim_header_format(dkim_config, timestamp, &signed_headers_list, &bh, "");
|
||||
let signed_headers = dkim_canonicalize_headers(
|
||||
dkim_config.headers.iter().map(|h| h.as_ref()),
|
||||
dkim_config.headers.iter().map(AsRef::as_ref),
|
||||
headers,
|
||||
dkim_config.canonicalization.header,
|
||||
);
|
||||
@@ -488,14 +487,14 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
|
||||
fn test_headers_simple_canonicalize() {
|
||||
let message = test_message();
|
||||
dbg!(message.headers.to_string());
|
||||
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple), "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\nTest: test test very very long with spaces and extra spaces \twill be\r\n folded to several lines \r\n")
|
||||
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple), "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\nTest: test test very very long with spaces and extra spaces \twill be\r\n folded to several lines \r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_headers_relaxed_canonicalize() {
|
||||
let message = test_message();
|
||||
dbg!(message.headers.to_string());
|
||||
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Relaxed),"from:=?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\ntest:test test very very long with spaces and extra spaces will be folded to several lines\r\n")
|
||||
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Relaxed),"from:=?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\ntest:test test very very long with spaces and extra spaces will be folded to several lines\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -119,7 +119,7 @@ mod serde {
|
||||
{
|
||||
struct ContentTypeVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ContentTypeVisitor {
|
||||
impl Visitor<'_> for ContentTypeVisitor {
|
||||
type Value = ContentType;
|
||||
|
||||
// The error message which states what the Visitor expects to
|
||||
|
||||
@@ -21,7 +21,7 @@ impl Date {
|
||||
///
|
||||
/// Shortcut for `Date::new(SystemTime::now())`
|
||||
pub fn now() -> Self {
|
||||
Self::new(SystemTime::now())
|
||||
Self::new(crate::time::now())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ mailbox_header! {
|
||||
|
||||
`Sender` header
|
||||
|
||||
This header contains [`Mailbox`][self::Mailbox] associated with sender.
|
||||
This header contains [`Mailbox`] associated with sender.
|
||||
|
||||
```no_test
|
||||
header::Sender("Mr. Sender <sender@example.com>".parse().unwrap())
|
||||
@@ -124,7 +124,7 @@ mailboxes_header! {
|
||||
|
||||
`From` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
This header contains [`Mailboxes`].
|
||||
|
||||
*/
|
||||
(From, "From")
|
||||
@@ -135,7 +135,7 @@ mailboxes_header! {
|
||||
|
||||
`Reply-To` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
This header contains [`Mailboxes`].
|
||||
|
||||
*/
|
||||
(ReplyTo, "Reply-To")
|
||||
@@ -146,7 +146,7 @@ mailboxes_header! {
|
||||
|
||||
`To` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
This header contains [`Mailboxes`].
|
||||
|
||||
*/
|
||||
(To, "To")
|
||||
@@ -157,7 +157,7 @@ mailboxes_header! {
|
||||
|
||||
`Cc` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
This header contains [`Mailboxes`].
|
||||
|
||||
*/
|
||||
(Cc, "Cc")
|
||||
@@ -168,7 +168,7 @@ mailboxes_header! {
|
||||
|
||||
`Bcc` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
This header contains [`Mailboxes`].
|
||||
|
||||
*/
|
||||
(Bcc, "Bcc")
|
||||
|
||||
@@ -186,21 +186,15 @@ impl HeaderName {
|
||||
|
||||
/// Creates a new header name, panics on invalid name
|
||||
pub const fn new_from_ascii_str(ascii: &'static str) -> Self {
|
||||
macro_rules! static_assert {
|
||||
($condition:expr) => {
|
||||
let _ = [()][(!($condition)) as usize];
|
||||
};
|
||||
}
|
||||
|
||||
static_assert!(!ascii.is_empty());
|
||||
static_assert!(ascii.len() <= 76);
|
||||
assert!(!ascii.is_empty());
|
||||
assert!(ascii.len() <= 76);
|
||||
assert!(ascii.is_ascii());
|
||||
|
||||
let bytes = ascii.as_bytes();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
static_assert!(bytes[i].is_ascii());
|
||||
static_assert!(bytes[i] != b' ');
|
||||
static_assert!(bytes[i] != b':');
|
||||
assert!(bytes[i] != b' ');
|
||||
assert!(bytes[i] != b':');
|
||||
|
||||
i += 1;
|
||||
}
|
||||
@@ -415,7 +409,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn empty_headername() {
|
||||
assert!(HeaderName::new_from_ascii(String::from("")).is_err());
|
||||
assert!(HeaderName::new_from_ascii("".to_owned()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -41,14 +41,14 @@ fn quoted_pair() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
|
||||
// FWS = ([*WSP CRLF] 1*WSP) / ; Folding white space
|
||||
// obs-FWS
|
||||
pub fn fws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
|
||||
pub(super) fn fws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
|
||||
rfc2234::wsp()
|
||||
.or_not()
|
||||
.then_ignore(rfc2234::wsp().ignored().repeated())
|
||||
}
|
||||
|
||||
// CFWS = *([FWS] comment) (([FWS] comment) / FWS)
|
||||
pub fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
|
||||
pub(super) fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
|
||||
// TODO: comment are not currently supported, so for now a cfws is
|
||||
// the same as a fws.
|
||||
fws()
|
||||
@@ -106,12 +106,12 @@ pub(super) fn atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
}
|
||||
|
||||
// dot-atom = [CFWS] dot-atom-text [CFWS]
|
||||
pub fn dot_atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
pub(super) fn dot_atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
cfws().chain(dot_atom_text())
|
||||
}
|
||||
|
||||
// dot-atom-text = 1*atext *("." 1*atext)
|
||||
pub fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
pub(super) fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
atext().repeated().at_least(1).chain(
|
||||
just('.')
|
||||
.chain(atext().repeated().at_least(1))
|
||||
@@ -204,7 +204,7 @@ pub(crate) fn mailbox_list(
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1
|
||||
|
||||
// addr-spec = local-part "@" domain
|
||||
pub fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<char>> {
|
||||
pub(super) fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<char>> {
|
||||
local_part()
|
||||
.collect()
|
||||
.then_ignore(just('@'))
|
||||
@@ -212,12 +212,12 @@ pub fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<char>> {
|
||||
}
|
||||
|
||||
// local-part = dot-atom / quoted-string / obs-local-part
|
||||
pub fn local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
pub(super) fn local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
choice((dot_atom(), quoted_string(), obs_local_part()))
|
||||
}
|
||||
|
||||
// domain = dot-atom / domain-literal / obs-domain
|
||||
pub fn domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
pub(super) fn domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
// NOTE: omitting domain-literal since it may never be used
|
||||
choice((dot_atom(), obs_domain()))
|
||||
}
|
||||
@@ -240,11 +240,11 @@ fn obs_phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.4
|
||||
|
||||
// obs-local-part = word *("." word)
|
||||
pub fn obs_local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
pub(super) fn obs_local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
word().chain(just('.').chain(word()).repeated().flatten())
|
||||
}
|
||||
|
||||
// obs-domain = atom *("." atom)
|
||||
pub fn obs_domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
pub(super) fn obs_domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
atom().chain(just('.').chain(atom()).repeated().flatten())
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ impl<'de> Deserialize<'de> for Mailbox {
|
||||
{
|
||||
struct FieldVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for FieldVisitor {
|
||||
impl Visitor<'_> for FieldVisitor {
|
||||
type Value = Field;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||
|
||||
@@ -174,7 +174,7 @@ impl Mailboxes {
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a new [`Mailbox`] to the list, in a Vec::push style pattern.
|
||||
/// Adds a new [`Mailbox`] to the list, in a `Vec::push` style pattern.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -351,7 +351,7 @@ impl FromStr for Mailboxes {
|
||||
})?;
|
||||
|
||||
for (name, (user, domain)) in parsed_mailboxes {
|
||||
mailboxes.push(Mailbox::new(name, Address::new(user, domain)?))
|
||||
mailboxes.push(Mailbox::new(name, Address::new(user, domain)?));
|
||||
}
|
||||
|
||||
Ok(Mailboxes(mailboxes))
|
||||
@@ -531,7 +531,7 @@ mod test {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(Some("".into()), "kayo@example.com".parse().unwrap())
|
||||
Mailbox::new(Some("".to_owned()), "kayo@example.com".parse().unwrap())
|
||||
),
|
||||
"kayo@example.com"
|
||||
);
|
||||
|
||||
@@ -217,7 +217,7 @@ mod mimebody;
|
||||
|
||||
use crate::{
|
||||
address::Envelope,
|
||||
message::header::{ContentTransferEncoding, Header, Headers, MailboxesHeader},
|
||||
message::header::{ContentTransferEncoding, Header, HeaderValue, Headers, MailboxesHeader},
|
||||
Error as EmailError,
|
||||
};
|
||||
|
||||
@@ -277,7 +277,7 @@ impl MessageBuilder {
|
||||
/// Shortcut for `self.date(SystemTime::now())`, it is automatically inserted
|
||||
/// if no date has been provided.
|
||||
pub fn date_now(self) -> Self {
|
||||
self.date(SystemTime::now())
|
||||
self.date(crate::time::now())
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `ReplyTo` header
|
||||
@@ -345,7 +345,7 @@ impl MessageBuilder {
|
||||
let hostname = hostname::get()
|
||||
.map_err(|_| ())
|
||||
.and_then(|s| s.into_string().map_err(|_| ()))
|
||||
.unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_owned());
|
||||
.unwrap_or_else(|()| DEFAULT_MESSAGE_ID_DOMAIN.to_owned());
|
||||
#[cfg(not(feature = "hostname"))]
|
||||
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_owned();
|
||||
|
||||
@@ -369,6 +369,12 @@ impl MessageBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set raw custom header to message
|
||||
pub fn raw_header(mut self, raw_header: HeaderValue) -> Self {
|
||||
self.headers.insert_raw(raw_header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add mailbox to header
|
||||
pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self {
|
||||
match self.headers.get::<H>() {
|
||||
@@ -457,12 +463,12 @@ impl MessageBuilder {
|
||||
self.build(MessageBody::Raw(body.into_vec()))
|
||||
}
|
||||
|
||||
/// Create message using mime body ([`MultiPart`][self::MultiPart])
|
||||
/// Create message using mime body ([`MultiPart`])
|
||||
pub fn multipart(self, part: MultiPart) -> Result<Message, EmailError> {
|
||||
self.mime_1_0().build(MessageBody::Mime(Part::Multi(part)))
|
||||
}
|
||||
|
||||
/// Create message using mime body ([`SinglePart`][self::SinglePart])
|
||||
/// Create message using mime body ([`SinglePart`])
|
||||
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
|
||||
self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
|
||||
}
|
||||
@@ -527,7 +533,7 @@ impl Message {
|
||||
match &self.body {
|
||||
MessageBody::Mime(p) => p.format_body(&mut out),
|
||||
MessageBody::Raw(r) => out.extend_from_slice(r),
|
||||
};
|
||||
}
|
||||
out.extend_from_slice(b"\r\n");
|
||||
out
|
||||
}
|
||||
@@ -537,7 +543,10 @@ impl Message {
|
||||
/// Example:
|
||||
/// ```rust
|
||||
/// use lettre::{
|
||||
/// message::dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
|
||||
/// message::{
|
||||
/// dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
|
||||
/// header::ContentType,
|
||||
/// },
|
||||
/// Message,
|
||||
/// };
|
||||
///
|
||||
@@ -546,6 +555,7 @@ impl Message {
|
||||
/// .reply_to("Bob <bob@example.org>".parse().unwrap())
|
||||
/// .to("Carla <carla@example.net>".parse().unwrap())
|
||||
/// .subject("Hello")
|
||||
/// .header(ContentType::TEXT_PLAIN)
|
||||
/// .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_owned())
|
||||
/// .unwrap();
|
||||
/// let key = "-----BEGIN RSA PRIVATE KEY-----
|
||||
@@ -601,7 +611,7 @@ impl EmailFormat for Message {
|
||||
MessageBody::Mime(p) => p.format(out),
|
||||
MessageBody::Raw(r) => {
|
||||
out.extend_from_slice(b"\r\n");
|
||||
out.extend_from_slice(r)
|
||||
out.extend_from_slice(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -707,7 +717,10 @@ mod test {
|
||||
.header(header::To(
|
||||
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
|
||||
))
|
||||
.header(header::Subject::from(String::from("яңа ел белән!")))
|
||||
.raw_header(header::HeaderValue::new(
|
||||
header::HeaderName::new_from_ascii_str("Subject"),
|
||||
"яңа ел белән!".to_owned(),
|
||||
))
|
||||
.body(String::from("Happy new year!"))
|
||||
.unwrap();
|
||||
|
||||
@@ -765,7 +778,7 @@ mod test {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert_eq!(line.0, line.1)
|
||||
assert_eq!(line.0, line.1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
14
src/rustls_crypto.rs
Normal file
14
src/rustls_crypto.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustls::crypto::CryptoProvider;
|
||||
|
||||
pub(crate) fn crypto_provider() -> Arc<CryptoProvider> {
|
||||
CryptoProvider::get_default().cloned().unwrap_or_else(|| {
|
||||
#[cfg(feature = "aws-lc-rs")]
|
||||
let provider = rustls::crypto::aws_lc_rs::default_provider();
|
||||
#[cfg(not(feature = "aws-lc-rs"))]
|
||||
let provider = rustls::crypto::ring::default_provider();
|
||||
|
||||
Arc::new(provider)
|
||||
})
|
||||
}
|
||||
26
src/time.rs
Normal file
26
src/time.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
#[cfg(all(feature = "web", target_arch = "wasm32"))]
|
||||
pub(crate) fn now() -> SystemTime {
|
||||
fn to_std_systemtime(time: web_time::SystemTime) -> std::time::SystemTime {
|
||||
let duration = time
|
||||
.duration_since(web_time::SystemTime::UNIX_EPOCH)
|
||||
.unwrap();
|
||||
SystemTime::UNIX_EPOCH + duration
|
||||
}
|
||||
|
||||
// FIXME: change to:
|
||||
// #[allow(
|
||||
// clippy::disallowed_methods,
|
||||
// reason = "`web-time` aliases `std::time::SystemTime::now` on non-WASM platforms"
|
||||
// )]
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
to_std_systemtime(web_time::SystemTime::now())
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "web", target_arch = "wasm32")))]
|
||||
pub(crate) fn now() -> SystemTime {
|
||||
// FIXME: change to #[expect(clippy::disallowed_methods, reason = "the `web` feature is disabled")]
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
SystemTime::now()
|
||||
}
|
||||
@@ -34,6 +34,7 @@ impl Error {
|
||||
|
||||
/// Returns true if the error is an envelope serialization or deserialization error
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
|
||||
pub fn is_envelope(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Envelope)
|
||||
}
|
||||
@@ -68,7 +69,7 @@ impl fmt::Display for Error {
|
||||
Kind::Io => f.write_str("response error")?,
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
Kind::Envelope => f.write_str("internal client error")?,
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(e) = &self.inner.source {
|
||||
write!(f, ": {e}")?;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//!
|
||||
//! use lettre::{FileTransport, Message, Transport};
|
||||
//! use lettre::{message::header::ContentType, FileTransport, Message, Transport};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = FileTransport::new(temp_dir());
|
||||
@@ -20,10 +20,10 @@
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(&email)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//!
|
||||
@@ -44,7 +44,7 @@
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//!
|
||||
//! use lettre::{FileTransport, Message, Transport};
|
||||
//! use lettre::{message::header::ContentType, FileTransport, Message, Transport};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = FileTransport::with_envelope(temp_dir());
|
||||
@@ -53,10 +53,10 @@
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(&email)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//!
|
||||
@@ -73,7 +73,9 @@
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//!
|
||||
//! use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType, AsyncFileTransport, AsyncTransport, Message, Tokio1Executor,
|
||||
//! };
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
|
||||
@@ -82,10 +84,10 @@
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(email).await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
@@ -99,7 +101,10 @@
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//!
|
||||
//! use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType, AsyncFileTransport, AsyncStd1Executor, AsyncTransport,
|
||||
//! Message,
|
||||
//! };
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
|
||||
@@ -108,10 +113,10 @@
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(email).await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
@@ -125,6 +130,7 @@
|
||||
//! Reply-To: Yuin <yuin@domain.tld>
|
||||
//! To: Hei <hei@domain.tld>
|
||||
//! Subject: Happy new year
|
||||
//! Content-Type: text/plain; charset=utf-8
|
||||
//! Date: Tue, 18 Aug 2020 22:50:17 GMT
|
||||
//!
|
||||
//! Be happy!
|
||||
@@ -167,7 +173,7 @@ pub struct FileTransport {
|
||||
}
|
||||
|
||||
/// Asynchronously writes the content and the envelope information to a file
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
@@ -193,6 +199,7 @@ impl FileTransport {
|
||||
/// Writes the email content in eml format and the envelope
|
||||
/// in json format.
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
|
||||
pub fn with_envelope<P: AsRef<Path>>(path: P) -> FileTransport {
|
||||
FileTransport {
|
||||
path: PathBuf::from(path.as_ref()),
|
||||
@@ -205,6 +212,7 @@ impl FileTransport {
|
||||
///
|
||||
/// Reads the envelope and the raw message content.
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
|
||||
pub fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
|
||||
use std::fs;
|
||||
|
||||
@@ -243,6 +251,7 @@ where
|
||||
/// Writes the email content in eml format and the envelope
|
||||
/// in json format.
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
|
||||
pub fn with_envelope<P: AsRef<Path>>(path: P) -> Self {
|
||||
Self {
|
||||
inner: FileTransport::with_envelope(path),
|
||||
@@ -254,6 +263,7 @@ where
|
||||
///
|
||||
/// Reads the envelope and the raw message content.
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
|
||||
pub async fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
|
||||
let eml_file = self.inner.path.join(format!("{email_id}.eml"));
|
||||
let eml = E::fs_read(&eml_file).await.map_err(error::io)?;
|
||||
@@ -266,6 +276,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
impl<E: Executor> Clone for AsyncFileTransport<E> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
marker_: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for FileTransport {
|
||||
type Ok = Id;
|
||||
type Error = Error;
|
||||
|
||||
@@ -56,13 +56,17 @@
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "builder", feature = "smtp-transport"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType, transport::smtp::authentication::Credentials, Message,
|
||||
//! SmtpTransport, Transport,
|
||||
//! };
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||
@@ -136,6 +140,10 @@ pub trait Transport {
|
||||
}
|
||||
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
|
||||
|
||||
/// Shuts down the transport. Future calls to [`Self::send`] and
|
||||
/// [`Self::send_raw`] might fail.
|
||||
fn shutdown(&self) {}
|
||||
}
|
||||
|
||||
/// Async Transport method for emails
|
||||
@@ -162,4 +170,8 @@ pub trait AsyncTransport {
|
||||
}
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
|
||||
|
||||
/// Shuts down the transport. Future calls to [`Self::send`] and
|
||||
/// [`Self::send_raw`] might fail.
|
||||
async fn shutdown(&self) {}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ impl fmt::Display for Error {
|
||||
match self.inner.kind {
|
||||
Kind::Response => f.write_str("response error")?,
|
||||
Kind::Client => f.write_str("internal client error")?,
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(e) = &self.inner.source {
|
||||
write!(f, ": {e}")?;
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{Message, SendmailTransport, Transport};
|
||||
//! use lettre::{message::header::ContentType, Message, SendmailTransport, Transport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let sender = SendmailTransport::new();
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(&email)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//!
|
||||
@@ -34,7 +34,8 @@
|
||||
//! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{
|
||||
//! AsyncSendmailTransport, AsyncTransport, Message, SendmailTransport, Tokio1Executor,
|
||||
//! message::header::ContentType, AsyncSendmailTransport, AsyncTransport, Message,
|
||||
//! SendmailTransport, Tokio1Executor,
|
||||
//! };
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
@@ -42,11 +43,11 @@
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let sender = AsyncSendmailTransport::<Tokio1Executor>::new();
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(email).await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
@@ -58,18 +59,17 @@
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "async-std1", feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{Message, AsyncTransport, AsyncStd1Executor, AsyncSendmailTransport};
|
||||
//! use lettre::{Message, AsyncTransport, AsyncStd1Executor,message::header::ContentType, AsyncSendmailTransport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .subject("Happy new year").header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let sender = AsyncSendmailTransport::<AsyncStd1Executor>::new();
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(email).await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
@@ -120,7 +120,7 @@ impl SendmailTransport {
|
||||
/// Creates a new transport with the `sendmail` command
|
||||
///
|
||||
/// Note: This uses the `sendmail` command in the current `PATH`. To use another command,
|
||||
/// use [SendmailTransport::new_with_command].
|
||||
/// use [`SendmailTransport::new_with_command`].
|
||||
pub fn new() -> SendmailTransport {
|
||||
SendmailTransport {
|
||||
command: DEFAULT_SENDMAIL.into(),
|
||||
@@ -157,7 +157,7 @@ where
|
||||
/// Creates a new transport with the `sendmail` command
|
||||
///
|
||||
/// Note: This uses the `sendmail` command in the current `PATH`. To use another command,
|
||||
/// use [AsyncSendmailTransport::new_with_command].
|
||||
/// use [`AsyncSendmailTransport::new_with_command`].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: SendmailTransport::new(),
|
||||
|
||||
@@ -12,6 +12,12 @@ use async_trait::async_trait;
|
||||
use super::pool::async_impl::Pool;
|
||||
#[cfg(feature = "pool")]
|
||||
use super::PoolConfig;
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls",
|
||||
feature = "async-std1-rustls"
|
||||
))]
|
||||
use super::Tls;
|
||||
use super::{
|
||||
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
|
||||
};
|
||||
@@ -24,6 +30,30 @@ use crate::Tokio1Executor;
|
||||
use crate::{Envelope, Executor};
|
||||
|
||||
/// Asynchronously sends emails using the SMTP protocol
|
||||
///
|
||||
/// `AsyncSmtpTransport` is the primary way for communicating
|
||||
/// with SMTP relay servers to send email messages. It holds the
|
||||
/// client connect configuration and creates new connections
|
||||
/// as necessary.
|
||||
///
|
||||
/// # Connection pool
|
||||
///
|
||||
/// When the `pool` feature is enabled (default), `AsyncSmtpTransport` maintains a
|
||||
/// connection pool to manage SMTP connections. The pool:
|
||||
///
|
||||
/// - Establishes a new connection when sending a message.
|
||||
/// - Recycles connections internally after a message is sent.
|
||||
/// - Reuses connections for subsequent messages, reducing connection setup overhead.
|
||||
///
|
||||
/// The connection pool can grow to hold multiple SMTP connections if multiple
|
||||
/// emails are sent concurrently, as SMTP does not support multiplexing within a
|
||||
/// single connection.
|
||||
///
|
||||
/// However, **connection reuse is not possible** if the `SyncSmtpTransport` instance
|
||||
/// is dropped after every email send operation. You must reuse the instance
|
||||
/// of this struct for the connection pool to be of any use.
|
||||
///
|
||||
/// To customize connection pool settings, use [`AsyncSmtpTransportBuilder::pool_config`].
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
pub struct AsyncSmtpTransport<E: Executor> {
|
||||
#[cfg(feature = "pool")]
|
||||
@@ -45,10 +75,15 @@ impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
|
||||
let result = conn.send(envelope, email).await?;
|
||||
|
||||
#[cfg(not(feature = "pool"))]
|
||||
conn.quit().await?;
|
||||
conn.abort().await;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn shutdown(&self) {
|
||||
#[cfg(feature = "pool")]
|
||||
self.inner.shutdown().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
@@ -67,6 +102,11 @@ impl AsyncTransport for AsyncSmtpTransport<AsyncStd1Executor> {
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn shutdown(&self) {
|
||||
#[cfg(feature = "pool")]
|
||||
self.inner.shutdown().await;
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> AsyncSmtpTransport<E>
|
||||
@@ -81,15 +121,15 @@ where
|
||||
/// to validate TLS certificates.
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
feature = "tokio1-rustls",
|
||||
feature = "async-std1-rustls"
|
||||
))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
feature = "tokio1-rustls",
|
||||
feature = "async-std1-rustls"
|
||||
)))
|
||||
)]
|
||||
pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||
@@ -115,15 +155,15 @@ where
|
||||
/// or emails will be sent to the server, protecting from downgrade attacks.
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
feature = "tokio1-rustls",
|
||||
feature = "async-std1-rustls"
|
||||
))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
feature = "tokio1-rustls",
|
||||
feature = "async-std1-rustls"
|
||||
)))
|
||||
)]
|
||||
pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||
@@ -161,54 +201,72 @@ where
|
||||
|
||||
/// Creates a `AsyncSmtpTransportBuilder` from a connection URL
|
||||
///
|
||||
/// The protocol, credentials, host and port can be provided in a single URL.
|
||||
/// Use the scheme `smtp` for an unencrypted relay (optionally in combination with the
|
||||
/// `tls` parameter to allow/require STARTTLS) or `smtps` for SMTP over TLS.
|
||||
/// The path section of the url can be used to set an alternative name for
|
||||
/// the HELO / EHLO command.
|
||||
/// For example `smtps://username:password@smtp.example.com/client.example.com:465`
|
||||
/// will set the HELO / EHLO name `client.example.com`.
|
||||
/// The protocol, credentials, host, port and EHLO name can be provided
|
||||
/// in a single URL. This may be simpler than having to configure SMTP
|
||||
/// through multiple configuration parameters and then having to pass
|
||||
/// those options to lettre.
|
||||
///
|
||||
/// <table>
|
||||
/// <thead>
|
||||
/// <tr>
|
||||
/// <th>scheme</th>
|
||||
/// <th>tls parameter</th>
|
||||
/// <th>example</th>
|
||||
/// <th>remarks</th>
|
||||
/// </tr>
|
||||
/// </thead>
|
||||
/// <tbody>
|
||||
/// <tr>
|
||||
/// <td>smtps</td>
|
||||
/// <td>-</td>
|
||||
/// <td>smtps://smtp.example.com</td>
|
||||
/// <td>SMTP over TLS, recommended method</td>
|
||||
/// </tr>
|
||||
/// <tr>
|
||||
/// <td>smtp</td>
|
||||
/// <td>required</td>
|
||||
/// <td>smtp://smtp.example.com?tls=required</td>
|
||||
/// <td>SMTP with STARTTLS required, when SMTP over TLS is not available</td>
|
||||
/// </tr>
|
||||
/// <tr>
|
||||
/// <td>smtp</td>
|
||||
/// <td>opportunistic</td>
|
||||
/// <td>smtp://smtp.example.com?tls=opportunistic</td>
|
||||
/// <td>
|
||||
/// SMTP with optionally STARTTLS when supported by the server.
|
||||
/// Caution: this method is vulnerable to a man-in-the-middle attack.
|
||||
/// Not recommended for production use.
|
||||
/// </td>
|
||||
/// </tr>
|
||||
/// <tr>
|
||||
/// <td>smtp</td>
|
||||
/// <td>-</td>
|
||||
/// <td>smtp://smtp.example.com</td>
|
||||
/// <td>Unencrypted SMTP, not recommended for production use.</td>
|
||||
/// </tr>
|
||||
/// </tbody>
|
||||
/// </table>
|
||||
/// The URL is created in the following way:
|
||||
/// `scheme://user:pass@hostname:port/ehlo-name?tls=TLS`.
|
||||
///
|
||||
/// `user` (Username) and `pass` (Password) are optional in case the
|
||||
/// SMTP relay doesn't require authentication. When `port` is not
|
||||
/// configured it is automatically determined based on the `scheme`.
|
||||
/// `ehlo-name` optionally overwrites the hostname sent for the EHLO
|
||||
/// command. `TLS` controls whether STARTTLS is simply enabled
|
||||
/// (`opportunistic` - not enough to prevent man-in-the-middle attacks)
|
||||
/// or `required` (require the server to upgrade the connection to
|
||||
/// STARTTLS, otherwise fail on suspicion of main-in-the-middle attempt).
|
||||
///
|
||||
/// Use the following table to construct your SMTP url:
|
||||
///
|
||||
/// | scheme | `tls` query parameter | example | default port | remarks |
|
||||
/// | ------- | --------------------- | -------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
/// | `smtps` | unset | `smtps://user:pass@hostname:port` | 465 | SMTP over TLS, recommended method |
|
||||
/// | `smtp` | `required` | `smtp://user:pass@hostname:port?tls=required` | 587 | SMTP with STARTTLS required, when SMTP over TLS is not available |
|
||||
/// | `smtp` | `opportunistic` | `smtp://user:pass@hostname:port?tls=opportunistic` | 587 | SMTP with optionally STARTTLS when supported by the server. Not suitable for production use: vulnerable to a man-in-the-middle attack |
|
||||
/// | `smtp` | unset | `smtp://user:pass@hostname:port` | 587 | Always unencrypted SMTP. Not suitable for production use: sends all data unencrypted |
|
||||
///
|
||||
/// IMPORTANT: some parameters like `user` and `pass` cannot simply
|
||||
/// be concatenated to construct the final URL because special characters
|
||||
/// contained within the parameter may confuse the URL decoder.
|
||||
/// Manually URL encode the parameters before concatenating them or use
|
||||
/// a proper URL encoder, like the following cargo script:
|
||||
///
|
||||
/// ```rust
|
||||
/// # const TOML: &str = r#"
|
||||
/// #!/usr/bin/env cargo
|
||||
///
|
||||
/// //! ```cargo
|
||||
/// //! [dependencies]
|
||||
/// //! url = "2"
|
||||
/// //! ```
|
||||
/// # "#;
|
||||
///
|
||||
/// use url::Url;
|
||||
///
|
||||
/// fn main() {
|
||||
/// // don't touch this line
|
||||
/// let mut url = Url::parse("foo://bar").unwrap();
|
||||
///
|
||||
/// // configure the scheme (`smtp` or `smtps`) here.
|
||||
/// url.set_scheme("smtps").unwrap();
|
||||
/// // configure the username and password.
|
||||
/// // remove the following two lines if unauthenticated.
|
||||
/// url.set_username("username").unwrap();
|
||||
/// url.set_password(Some("password")).unwrap();
|
||||
/// // configure the hostname
|
||||
/// url.set_host(Some("smtp.example.com")).unwrap();
|
||||
/// // configure the port - only necessary if using a non-default port
|
||||
/// url.set_port(Some(465)).unwrap();
|
||||
/// // configure the EHLO name
|
||||
/// url.set_path("ehlo-name");
|
||||
///
|
||||
/// println!("{url}");
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The connection URL can then be used in the following way:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use lettre::{
|
||||
@@ -232,22 +290,18 @@ where
|
||||
/// let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
||||
/// AsyncSmtpTransport::<Tokio1Executor>::from_url(
|
||||
/// "smtps://username:password@smtp.example.com:465",
|
||||
/// )
|
||||
/// .unwrap()
|
||||
/// )?
|
||||
/// .build();
|
||||
///
|
||||
/// // Send the email
|
||||
/// match mailer.send(email).await {
|
||||
/// Ok(_) => println!("Email sent successfully!"),
|
||||
/// Err(e) => panic!("Could not send email: {e:?}"),
|
||||
/// }
|
||||
/// mailer.send(email).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn from_url(connection_url: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||
super::connection_url::from_connection_url(connection_url)
|
||||
@@ -323,7 +377,7 @@ impl AsyncSmtpTransportBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
/// Set the authentication credentials to use
|
||||
pub fn credentials(mut self, credentials: Credentials) -> Self {
|
||||
self.info.credentials = Some(credentials);
|
||||
self
|
||||
@@ -336,6 +390,17 @@ impl AsyncSmtpTransportBuilder {
|
||||
}
|
||||
|
||||
/// Set the port to use
|
||||
///
|
||||
/// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
|
||||
///
|
||||
/// lettre usually picks the correct `port` when building
|
||||
/// [`AsyncSmtpTransport`] using [`AsyncSmtpTransport::relay`] or
|
||||
/// [`AsyncSmtpTransport::starttls_relay`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Using the incorrect `port` and [`Self::tls`] combination may
|
||||
/// lead to hard to debug IO errors coming from the TLS library.
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.info.port = port;
|
||||
self
|
||||
@@ -348,20 +413,31 @@ impl AsyncSmtpTransportBuilder {
|
||||
}
|
||||
|
||||
/// Set the TLS settings to use
|
||||
///
|
||||
/// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
|
||||
///
|
||||
/// By default lettre chooses the correct `tls` configuration when
|
||||
/// building [`AsyncSmtpTransport`] using [`AsyncSmtpTransport::relay`] or
|
||||
/// [`AsyncSmtpTransport::starttls_relay`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Using the incorrect [`Tls`] and [`Self::port`] combination may
|
||||
/// lead to hard to debug IO errors coming from the TLS library.
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
feature = "tokio1-rustls",
|
||||
feature = "async-std1-rustls"
|
||||
))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
feature = "tokio1-rustls",
|
||||
feature = "async-std1-rustls"
|
||||
)))
|
||||
)]
|
||||
pub fn tls(mut self, tls: super::Tls) -> Self {
|
||||
pub fn tls(mut self, tls: Tls) -> Self {
|
||||
self.info.tls = tls;
|
||||
self
|
||||
}
|
||||
@@ -394,7 +470,7 @@ impl AsyncSmtpTransportBuilder {
|
||||
}
|
||||
|
||||
/// Build client
|
||||
pub struct AsyncSmtpClient<E> {
|
||||
pub(super) struct AsyncSmtpClient<E> {
|
||||
info: SmtpInfo,
|
||||
marker_: PhantomData<E>,
|
||||
}
|
||||
@@ -406,7 +482,7 @@ where
|
||||
/// Creates a new connection directly usable to send emails
|
||||
///
|
||||
/// Handles encryption and authentication
|
||||
pub async fn connection(&self) -> Result<AsyncSmtpConnection, Error> {
|
||||
pub(super) async fn connection(&self) -> Result<AsyncSmtpConnection, Error> {
|
||||
let mut conn = E::connect(
|
||||
&self.info.server,
|
||||
self.info.port,
|
||||
|
||||
@@ -98,13 +98,17 @@ impl Mechanism {
|
||||
let decoded_challenge = challenge
|
||||
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
|
||||
|
||||
if ["User Name", "Username:", "Username", "User Name\0"]
|
||||
.contains(&decoded_challenge)
|
||||
{
|
||||
if contains_ignore_ascii_case(
|
||||
decoded_challenge,
|
||||
["User Name", "Username:", "Username", "User Name\0"],
|
||||
) {
|
||||
return Ok(credentials.authentication_identity.clone());
|
||||
}
|
||||
|
||||
if ["Password", "Password:", "Password\0"].contains(&decoded_challenge) {
|
||||
if contains_ignore_ascii_case(
|
||||
decoded_challenge,
|
||||
["Password", "Password:", "Password\0"],
|
||||
) {
|
||||
return Ok(credentials.secret.clone());
|
||||
}
|
||||
|
||||
@@ -121,6 +125,15 @@ impl Mechanism {
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_ignore_ascii_case<'a>(
|
||||
haystack: &str,
|
||||
needles: impl IntoIterator<Item = &'a str>,
|
||||
) -> bool {
|
||||
needles
|
||||
.into_iter()
|
||||
.any(|item| item.eq_ignore_ascii_case(haystack))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Credentials, Mechanism};
|
||||
@@ -155,6 +168,23 @@ mod test {
|
||||
assert!(mechanism.response(&credentials, None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_login_case_insensitive() {
|
||||
let mechanism = Mechanism::Login;
|
||||
|
||||
let credentials = Credentials::new("alice".to_owned(), "wonderland".to_owned());
|
||||
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, Some("username")).unwrap(),
|
||||
"alice"
|
||||
);
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, Some("password")).unwrap(),
|
||||
"wonderland"
|
||||
);
|
||||
assert!(mechanism.response(&credentials, None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xoauth2() {
|
||||
let mechanism = Mechanism::Xoauth2;
|
||||
|
||||
@@ -6,7 +6,8 @@ use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use super::async_net::AsyncTokioStream;
|
||||
#[cfg(feature = "tracing")]
|
||||
use super::escape_crlf;
|
||||
use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
|
||||
#[allow(deprecated)]
|
||||
use super::{async_net::AsyncNetworkStream, ClientCodec, TlsParameters};
|
||||
use crate::{
|
||||
transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
@@ -35,6 +36,7 @@ macro_rules! try_smtp (
|
||||
pub struct AsyncSmtpConnection {
|
||||
/// TCP stream between client and server
|
||||
/// Value is None before connection
|
||||
#[allow(deprecated)]
|
||||
stream: BufReader<AsyncNetworkStream>,
|
||||
/// Panic state
|
||||
panic: bool,
|
||||
@@ -52,10 +54,12 @@ impl AsyncSmtpConnection {
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
|
||||
pub async fn connect_with_transport(
|
||||
stream: Box<dyn AsyncTokioStream>,
|
||||
hello_name: &ClientId,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(deprecated)]
|
||||
let stream = AsyncNetworkStream::use_existing_tokio1(stream);
|
||||
Self::connect_impl(stream, hello_name).await
|
||||
}
|
||||
@@ -86,12 +90,12 @@ impl AsyncSmtpConnection {
|
||||
/// Some(TlsParameters::new("example.com".to_owned())?),
|
||||
/// None,
|
||||
/// )
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// .await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
|
||||
pub async fn connect_tokio1<T: tokio1_crate::net::ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
@@ -99,6 +103,7 @@ impl AsyncSmtpConnection {
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
local_address: Option<IpAddr>,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(deprecated)]
|
||||
let stream =
|
||||
AsyncNetworkStream::connect_tokio1(server, timeout, tls_parameters, local_address)
|
||||
.await?;
|
||||
@@ -109,16 +114,19 @@ impl AsyncSmtpConnection {
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
|
||||
pub async fn connect_asyncstd1<T: async_std::net::ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(deprecated)]
|
||||
let stream = AsyncNetworkStream::connect_asyncstd1(server, timeout, tls_parameters).await?;
|
||||
Self::connect_impl(stream, hello_name).await
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
async fn connect_impl(
|
||||
stream: AsyncNetworkStream,
|
||||
hello_name: &ClientId,
|
||||
@@ -246,6 +254,7 @@ impl AsyncSmtpConnection {
|
||||
}
|
||||
|
||||
/// Sets the underlying stream
|
||||
#[allow(deprecated)]
|
||||
pub fn set_stream(&mut self, stream: AsyncNetworkStream) {
|
||||
self.stream = BufReader::new(stream);
|
||||
}
|
||||
@@ -369,13 +378,35 @@ impl AsyncSmtpConnection {
|
||||
}
|
||||
|
||||
/// The X509 certificate of the server (DER encoded)
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
|
||||
self.stream.get_ref().peer_certificate()
|
||||
}
|
||||
|
||||
/// Currently this is only avaialable when using Boring TLS and
|
||||
/// returns the result of the verification of the TLS certificate
|
||||
/// presented by the peer, if any. Only the last error encountered
|
||||
/// during verification is presented.
|
||||
/// It can be useful when you don't want to fail outright the TLS
|
||||
/// negotiation, for example when a self-signed certificate is
|
||||
/// encountered, but still want to record metrics or log the fact.
|
||||
/// When using DANE verification, the PKI root of trust moves from
|
||||
/// the CAs to DNS, so self-signed certificates are permitted as long
|
||||
/// as the TLSA records match the leaf or issuer certificates.
|
||||
/// It cannot be called on non Boring TLS streams.
|
||||
#[cfg(feature = "boring-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
|
||||
pub fn tls_verify_result(&self) -> Result<(), Error> {
|
||||
self.stream.get_ref().tls_verify_result()
|
||||
}
|
||||
|
||||
/// All the X509 certificates of the chain (DER encoded)
|
||||
#[cfg(any(feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "boring-tls"))))]
|
||||
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
|
||||
self.stream.get_ref().certificate_chain()
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ use std::{
|
||||
#[cfg(feature = "async-std1")]
|
||||
use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs};
|
||||
use futures_io::{
|
||||
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind,
|
||||
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError,
|
||||
Result as IoResult,
|
||||
};
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
|
||||
#[cfg(any(feature = "tokio1-rustls-tls", feature = "async-std1-rustls-tls"))]
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
use futures_rustls::client::TlsStream as AsyncStd1RustlsStream;
|
||||
#[cfg(any(feature = "tokio1-rustls", feature = "async-std1-rustls"))]
|
||||
use rustls::pki_types::ServerName;
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
use tokio1_boring::SslStream as Tokio1SslStream;
|
||||
@@ -27,14 +27,14 @@ use tokio1_crate::net::{
|
||||
};
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream;
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
use tokio1_rustls::client::TlsStream as Tokio1RustlsStream;
|
||||
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "tokio1-rustls",
|
||||
feature = "tokio1-boring-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
feature = "async-std1-rustls"
|
||||
))]
|
||||
use super::InnerTlsParameters;
|
||||
use super::TlsParameters;
|
||||
@@ -44,6 +44,10 @@ use crate::transport::smtp::{error, Error};
|
||||
|
||||
/// A network stream
|
||||
#[derive(Debug)]
|
||||
#[deprecated(
|
||||
since = "0.11.14",
|
||||
note = "This struct was not meant to be made public"
|
||||
)]
|
||||
pub struct AsyncNetworkStream {
|
||||
inner: InnerAsyncNetworkStream,
|
||||
}
|
||||
@@ -74,8 +78,8 @@ enum InnerAsyncNetworkStream {
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
Tokio1NativeTls(Tokio1TlsStream<Box<dyn AsyncTokioStream>>),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
Tokio1RustlsTls(Tokio1RustlsTlsStream<Box<dyn AsyncTokioStream>>),
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
Tokio1Rustls(Tokio1RustlsStream<Box<dyn AsyncTokioStream>>),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
Tokio1BoringTls(Tokio1SslStream<Box<dyn AsyncTokioStream>>),
|
||||
@@ -83,12 +87,13 @@ enum InnerAsyncNetworkStream {
|
||||
#[cfg(feature = "async-std1")]
|
||||
AsyncStd1Tcp(AsyncStd1TcpStream),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>),
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
AsyncStd1Rustls(AsyncStd1RustlsStream<AsyncStd1TcpStream>),
|
||||
/// Can't be built
|
||||
None,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl AsyncNetworkStream {
|
||||
fn new(inner: InnerAsyncNetworkStream) -> Self {
|
||||
if let InnerAsyncNetworkStream::None = inner {
|
||||
@@ -107,18 +112,17 @@ impl AsyncNetworkStream {
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => {
|
||||
s.get_ref().get_ref().get_ref().peer_addr()
|
||||
}
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => s.get_ref().0.peer_addr(),
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(s) => s.get_ref().0.peer_addr(),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => s.peer_addr(),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => s.get_ref().0.peer_addr(),
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => s.get_ref().0.peer_addr(),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Err(IoError::new(
|
||||
ErrorKind::Other,
|
||||
Err(IoError::other(
|
||||
"InnerAsyncNetworkStream::None must never be built",
|
||||
))
|
||||
}
|
||||
@@ -126,11 +130,13 @@ impl AsyncNetworkStream {
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
|
||||
pub fn use_existing_tokio1(stream: Box<dyn AsyncTokioStream>) -> AsyncNetworkStream {
|
||||
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(stream))
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
|
||||
pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
@@ -170,7 +176,7 @@ impl AsyncNetworkStream {
|
||||
last_err = Some(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"connection timed out",
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -197,6 +203,7 @@ impl AsyncNetworkStream {
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
|
||||
pub async fn connect_asyncstd1<T: AsyncStd1ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
@@ -222,7 +229,7 @@ impl AsyncNetworkStream {
|
||||
last_err = Some(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"connection timed out",
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,26 +260,25 @@ impl AsyncNetworkStream {
|
||||
feature = "tokio1",
|
||||
not(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "tokio1-rustls",
|
||||
feature = "tokio1-boring-tls"
|
||||
))
|
||||
))]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||
let _ = tls_parameters;
|
||||
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls-tls feature");
|
||||
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls feature");
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "tokio1-rustls",
|
||||
feature = "tokio1-boring-tls"
|
||||
))]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||
// 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!(),
|
||||
let InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) = tcp_stream else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
|
||||
@@ -280,19 +286,18 @@ impl AsyncNetworkStream {
|
||||
.map_err(error::connection)?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(all(feature = "async-std1", not(feature = "async-std1-rustls-tls")))]
|
||||
#[cfg(all(feature = "async-std1", not(feature = "async-std1-rustls")))]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
let _ = tls_parameters;
|
||||
panic!("Trying to upgrade an AsyncNetworkStream without having enabled the async-std1-rustls-tls feature");
|
||||
panic!("Trying to upgrade an AsyncNetworkStream without having enabled the async-std1-rustls feature");
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
// 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!(),
|
||||
let InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) = tcp_stream else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
|
||||
@@ -307,7 +312,7 @@ impl AsyncNetworkStream {
|
||||
#[allow(unused_variables)]
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "tokio1-rustls",
|
||||
feature = "tokio1-boring-tls"
|
||||
))]
|
||||
async fn upgrade_tokio1_tls(
|
||||
@@ -318,7 +323,7 @@ impl AsyncNetworkStream {
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(connector) => {
|
||||
InnerTlsParameters::NativeTls { connector } => {
|
||||
#[cfg(not(feature = "tokio1-native-tls"))]
|
||||
panic!("built without the tokio1-native-tls feature");
|
||||
|
||||
@@ -334,12 +339,12 @@ impl AsyncNetworkStream {
|
||||
Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerTlsParameters::RustlsTls(config) => {
|
||||
#[cfg(not(feature = "tokio1-rustls-tls"))]
|
||||
panic!("built without the tokio1-rustls-tls feature");
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerTlsParameters::Rustls { config } => {
|
||||
#[cfg(not(feature = "tokio1-rustls"))]
|
||||
panic!("built without the tokio1-rustls feature");
|
||||
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
return {
|
||||
use tokio1_rustls::TlsConnector;
|
||||
|
||||
@@ -351,18 +356,21 @@ impl AsyncNetworkStream {
|
||||
.connect(domain.to_owned(), tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
|
||||
Ok(InnerAsyncNetworkStream::Tokio1Rustls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerTlsParameters::BoringTls(connector) => {
|
||||
InnerTlsParameters::BoringTls {
|
||||
connector,
|
||||
accept_invalid_hostnames,
|
||||
} => {
|
||||
#[cfg(not(feature = "tokio1-boring-tls"))]
|
||||
panic!("built without the tokio1-boring-tls feature");
|
||||
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
return {
|
||||
let mut config = connector.configure().map_err(error::connection)?;
|
||||
config.set_verify_hostname(tls_parameters.accept_invalid_hostnames);
|
||||
config.set_verify_hostname(accept_invalid_hostnames);
|
||||
|
||||
let stream = tokio1_boring::connect(config, &domain, tcp_stream)
|
||||
.await
|
||||
@@ -374,7 +382,7 @@ impl AsyncNetworkStream {
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
async fn upgrade_asyncstd1_tls(
|
||||
tcp_stream: AsyncStd1TcpStream,
|
||||
mut tls_parameters: TlsParameters,
|
||||
@@ -383,15 +391,15 @@ impl AsyncNetworkStream {
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[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");
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerTlsParameters::RustlsTls(config) => {
|
||||
#[cfg(not(feature = "async-std1-rustls-tls"))]
|
||||
panic!("built without the async-std1-rustls-tls feature");
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerTlsParameters::Rustls { config } => {
|
||||
#[cfg(not(feature = "async-std1-rustls"))]
|
||||
panic!("built without the async-std1-rustls feature");
|
||||
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
return {
|
||||
use futures_rustls::TlsConnector;
|
||||
|
||||
@@ -403,11 +411,11 @@ impl AsyncNetworkStream {
|
||||
.connect(domain.to_owned(), tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
|
||||
Ok(InnerAsyncNetworkStream::AsyncStd1Rustls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerTlsParameters::BoringTls(connector) => {
|
||||
InnerTlsParameters::BoringTls { .. } => {
|
||||
panic!("boring-tls isn't supported with async-std yet.");
|
||||
}
|
||||
}
|
||||
@@ -419,18 +427,44 @@ impl AsyncNetworkStream {
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => false,
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(_) => true,
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(_) => true,
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(_) => true,
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(_) => true,
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false,
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => true,
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(_) => true,
|
||||
InnerAsyncNetworkStream::None => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "boring-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
|
||||
pub fn tls_verify_result(&self) -> Result<(), 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")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(_) => panic!("Unsupported"),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => {
|
||||
stream.ssl().verify_result().map_err(error::tls)
|
||||
}
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
Err(error::client("Connection is not encrypted"))
|
||||
}
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(_) => panic!("Unsupported"),
|
||||
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
|
||||
match &self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
@@ -439,8 +473,8 @@ impl AsyncNetworkStream {
|
||||
}
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(_) => panic!("Unsupported"),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(stream) => Ok(stream
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(stream) => Ok(stream
|
||||
.get_ref()
|
||||
.1
|
||||
.peer_certificates()
|
||||
@@ -460,8 +494,8 @@ impl AsyncNetworkStream {
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
Err(error::client("Connection is not encrypted"))
|
||||
}
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(stream) => Ok(stream
|
||||
.get_ref()
|
||||
.1
|
||||
.peer_certificates()
|
||||
@@ -487,8 +521,8 @@ impl AsyncNetworkStream {
|
||||
.unwrap()
|
||||
.to_der()
|
||||
.map_err(error::tls)?),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(stream) => Ok(stream
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(stream) => Ok(stream
|
||||
.get_ref()
|
||||
.1
|
||||
.peer_certificates()
|
||||
@@ -507,8 +541,8 @@ impl AsyncNetworkStream {
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
Err(error::client("Connection is not encrypted"))
|
||||
}
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(stream) => Ok(stream
|
||||
.get_ref()
|
||||
.1
|
||||
.peer_certificates()
|
||||
@@ -521,6 +555,7 @@ impl AsyncNetworkStream {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
@@ -546,8 +581,8 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => {
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(s) => {
|
||||
let mut b = Tokio1ReadBuf::new(buf);
|
||||
match Pin::new(s).poll_read(cx, &mut b) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||
@@ -566,8 +601,8 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
}
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_read(cx, buf),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(0))
|
||||
@@ -576,6 +611,7 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl FuturesAsyncWrite for AsyncNetworkStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
@@ -587,14 +623,14 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(0))
|
||||
@@ -608,14 +644,14 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(()))
|
||||
@@ -629,14 +665,14 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_close(cx),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_close(cx),
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_close(cx),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(()))
|
||||
|
||||
@@ -143,7 +143,7 @@ impl SmtpConnection {
|
||||
hello_name: &ClientId,
|
||||
) -> Result<(), Error> {
|
||||
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", feature = "boring-tls"))]
|
||||
{
|
||||
try_smtp!(self.command(Starttls), self);
|
||||
self.stream.get_mut().upgrade_tls(tls_parameters)?;
|
||||
@@ -153,11 +153,7 @@ impl SmtpConnection {
|
||||
try_smtp!(self.ehlo(hello_name), self);
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(any(
|
||||
feature = "native-tls",
|
||||
feature = "rustls-tls",
|
||||
feature = "boring-tls"
|
||||
)))]
|
||||
#[cfg(not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))]
|
||||
// This should never happen as `Tls` can only be created
|
||||
// when a TLS library is enabled
|
||||
unreachable!("TLS support required but not supported");
|
||||
@@ -303,13 +299,35 @@ impl SmtpConnection {
|
||||
}
|
||||
|
||||
/// The X509 certificate of the server (DER encoded)
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
|
||||
self.stream.get_ref().peer_certificate()
|
||||
}
|
||||
|
||||
/// Currently this is only avaialable when using Boring TLS and
|
||||
/// returns the result of the verification of the TLS certificate
|
||||
/// presented by the peer, if any. Only the last error encountered
|
||||
/// during verification is presented.
|
||||
/// It can be useful when you don't want to fail outright the TLS
|
||||
/// negotiation, for example when a self-signed certificate is
|
||||
/// encountered, but still want to record metrics or log the fact.
|
||||
/// When using DANE verification, the PKI root of trust moves from
|
||||
/// the CAs to DNS, so self-signed certificates are permitted as long
|
||||
/// as the TLSA records match the leaf or issuer certificates.
|
||||
/// It cannot be called on non Boring TLS streams.
|
||||
#[cfg(feature = "boring-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
|
||||
pub fn tls_verify_result(&self) -> Result<(), Error> {
|
||||
self.stream.get_ref().tls_verify_result()
|
||||
}
|
||||
|
||||
/// All the X509 certificates of the chain (DER encoded)
|
||||
#[cfg(any(feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "boring-tls"))))]
|
||||
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
|
||||
self.stream.get_ref().certificate_chain()
|
||||
}
|
||||
|
||||
@@ -28,13 +28,14 @@ use std::fmt::Debug;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub use self::async_connection::AsyncSmtpConnection;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
#[allow(deprecated)]
|
||||
pub use self::async_net::AsyncNetworkStream;
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub use self::async_net::AsyncTokioStream;
|
||||
use self::net::NetworkStream;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
pub(super) use self::tls::InnerTlsParameters;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
pub use self::tls::TlsVersion;
|
||||
pub use self::{
|
||||
connection::SmtpConnection,
|
||||
@@ -57,7 +58,7 @@ struct ClientCodec {
|
||||
|
||||
impl ClientCodec {
|
||||
/// Creates a new client codec
|
||||
pub fn new() -> Self {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
status: CodecStatus::StartOfNewLine,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(feature = "rustls")]
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
@@ -11,11 +11,11 @@ use std::{
|
||||
use boring::ssl::SslStream;
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::TlsStream;
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(feature = "rustls")]
|
||||
use rustls::{pki_types::ServerName, ClientConnection, StreamOwned};
|
||||
use socket2::{Domain, Protocol, Type};
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
use super::InnerTlsParameters;
|
||||
use super::TlsParameters;
|
||||
use crate::transport::smtp::{error, Error};
|
||||
@@ -36,8 +36,8 @@ enum InnerNetworkStream {
|
||||
#[cfg(feature = "native-tls")]
|
||||
NativeTls(TlsStream<TcpStream>),
|
||||
/// Encrypted TCP stream
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
RustlsTls(StreamOwned<ClientConnection, TcpStream>),
|
||||
#[cfg(feature = "rustls")]
|
||||
Rustls(StreamOwned<ClientConnection, TcpStream>),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
BoringTls(SslStream<TcpStream>),
|
||||
/// Can't be built
|
||||
@@ -59,8 +59,8 @@ impl NetworkStream {
|
||||
InnerNetworkStream::Tcp(s) => s.peer_addr(),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(s) => s.get_ref().peer_addr(),
|
||||
InnerNetworkStream::None => {
|
||||
@@ -79,8 +79,8 @@ impl NetworkStream {
|
||||
InnerNetworkStream::Tcp(s) => s.shutdown(how),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(s) => s.get_ref().shutdown(how),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(s) => s.get_ref().shutdown(how),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(s) => s.get_ref().shutdown(how),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(s) => s.get_ref().shutdown(how),
|
||||
InnerNetworkStream::None => {
|
||||
@@ -119,12 +119,12 @@ impl NetworkStream {
|
||||
|
||||
if let Some(timeout) = timeout {
|
||||
match socket.connect_timeout(&addr.into(), timeout) {
|
||||
Ok(_) => return Ok(socket.into()),
|
||||
Ok(()) => return Ok(socket.into()),
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
} else {
|
||||
match socket.connect(&addr.into()) {
|
||||
Ok(_) => return Ok(socket.into()),
|
||||
Ok(()) => return Ok(socket.into()),
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
}
|
||||
@@ -146,23 +146,18 @@ impl NetworkStream {
|
||||
|
||||
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
|
||||
match &self.inner {
|
||||
#[cfg(not(any(
|
||||
feature = "native-tls",
|
||||
feature = "rustls-tls",
|
||||
feature = "boring-tls"
|
||||
)))]
|
||||
#[cfg(not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))]
|
||||
InnerNetworkStream::Tcp(_) => {
|
||||
let _ = tls_parameters;
|
||||
panic!("Trying to upgrade an NetworkStream without having enabled either the native-tls or the rustls-tls feature");
|
||||
panic!("Trying to upgrade an NetworkStream without having enabled either the `native-tls` or the `rustls` feature");
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
InnerNetworkStream::Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerNetworkStream::Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
let InnerNetworkStream::Tcp(tcp_stream) = tcp_stream else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
|
||||
@@ -172,34 +167,37 @@ impl NetworkStream {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
fn upgrade_tls_impl(
|
||||
tcp_stream: TcpStream,
|
||||
tls_parameters: &TlsParameters,
|
||||
) -> Result<InnerNetworkStream, Error> {
|
||||
Ok(match &tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(connector) => {
|
||||
InnerTlsParameters::NativeTls { connector } => {
|
||||
let stream = connector
|
||||
.connect(tls_parameters.domain(), tcp_stream)
|
||||
.map_err(error::connection)?;
|
||||
InnerNetworkStream::NativeTls(stream)
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerTlsParameters::RustlsTls(connector) => {
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerTlsParameters::Rustls { config } => {
|
||||
let domain = ServerName::try_from(tls_parameters.domain())
|
||||
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
|
||||
let connection = ClientConnection::new(Arc::clone(connector), domain.to_owned())
|
||||
let connection = ClientConnection::new(Arc::clone(config), domain.to_owned())
|
||||
.map_err(error::connection)?;
|
||||
let stream = StreamOwned::new(connection, tcp_stream);
|
||||
InnerNetworkStream::RustlsTls(stream)
|
||||
InnerNetworkStream::Rustls(stream)
|
||||
}
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerTlsParameters::BoringTls(connector) => {
|
||||
InnerTlsParameters::BoringTls {
|
||||
connector,
|
||||
accept_invalid_hostnames,
|
||||
} => {
|
||||
let stream = connector
|
||||
.configure()
|
||||
.map_err(error::connection)?
|
||||
.verify_hostname(tls_parameters.accept_invalid_hostnames)
|
||||
.verify_hostname(*accept_invalid_hostnames)
|
||||
.connect(tls_parameters.domain(), tcp_stream)
|
||||
.map_err(error::connection)?;
|
||||
InnerNetworkStream::BoringTls(stream)
|
||||
@@ -212,8 +210,8 @@ impl NetworkStream {
|
||||
InnerNetworkStream::Tcp(_) => false,
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(_) => true,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(_) => true,
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(_) => true,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(_) => true,
|
||||
InnerNetworkStream::None => {
|
||||
@@ -223,14 +221,32 @@ impl NetworkStream {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(feature = "boring-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
|
||||
pub fn tls_verify_result(&self) -> Result<(), Error> {
|
||||
match &self.inner {
|
||||
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(_) => panic!("Unsupported"),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(_) => panic!("Unsupported"),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(stream) => {
|
||||
stream.ssl().verify_result().map_err(error::tls)
|
||||
}
|
||||
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", 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
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(stream) => Ok(stream
|
||||
.conn
|
||||
.peer_certificates()
|
||||
.unwrap()
|
||||
@@ -249,7 +265,11 @@ impl NetworkStream {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
|
||||
match &self.inner {
|
||||
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
|
||||
@@ -260,8 +280,8 @@ impl NetworkStream {
|
||||
.unwrap()
|
||||
.to_der()
|
||||
.map_err(error::tls)?),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(stream) => Ok(stream
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(stream) => Ok(stream
|
||||
.conn
|
||||
.peer_certificates()
|
||||
.unwrap()
|
||||
@@ -284,8 +304,8 @@ impl NetworkStream {
|
||||
InnerNetworkStream::Tcp(stream) => stream.set_read_timeout(duration),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_read_timeout(duration),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_read_timeout(duration),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(stream) => stream.get_ref().set_read_timeout(duration),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_read_timeout(duration),
|
||||
InnerNetworkStream::None => {
|
||||
@@ -302,8 +322,8 @@ impl NetworkStream {
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_write_timeout(duration),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_write_timeout(duration),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(stream) => stream.get_ref().set_write_timeout(duration),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_write_timeout(duration),
|
||||
InnerNetworkStream::None => {
|
||||
@@ -320,8 +340,8 @@ impl Read for NetworkStream {
|
||||
InnerNetworkStream::Tcp(s) => s.read(buf),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(s) => s.read(buf),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(s) => s.read(buf),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(s) => s.read(buf),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(s) => s.read(buf),
|
||||
InnerNetworkStream::None => {
|
||||
@@ -338,8 +358,8 @@ impl Write for NetworkStream {
|
||||
InnerNetworkStream::Tcp(s) => s.write(buf),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(s) => s.write(buf),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(s) => s.write(buf),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(s) => s.write(buf),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(s) => s.write(buf),
|
||||
InnerNetworkStream::None => {
|
||||
@@ -354,8 +374,8 @@ impl Write for NetworkStream {
|
||||
InnerNetworkStream::Tcp(s) => s.flush(),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(s) => s.flush(),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(s) => s.flush(),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(s) => s.flush(),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(s) => s.flush(),
|
||||
InnerNetworkStream::None => {
|
||||
@@ -369,7 +389,7 @@ impl Write for NetworkStream {
|
||||
/// If the local address is set, binds the socket to this address.
|
||||
/// If local address is not set, then destination address is required to determine the default
|
||||
/// local address on some platforms.
|
||||
/// See: https://github.com/hyperium/hyper/blob/faf24c6ad8eee1c3d5ccc9a4d4835717b8e2903f/src/client/connect/http.rs#L560
|
||||
/// See: <https://github.com/hyperium/hyper/blob/faf24c6ad8eee1c3d5ccc9a4d4835717b8e2903f/src/client/connect/http.rs#L560>
|
||||
fn bind_local_address(
|
||||
socket: &socket2::Socket,
|
||||
dst_addr: &SocketAddr,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::fmt::{self, Debug};
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use std::{io, sync::Arc};
|
||||
#[cfg(feature = "rustls")]
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "boring-tls")]
|
||||
use boring::{
|
||||
@@ -10,23 +10,22 @@ use boring::{
|
||||
};
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::{Protocol, TlsConnector};
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(feature = "rustls")]
|
||||
use rustls::{
|
||||
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
|
||||
crypto::WebPkiSupportedAlgorithms,
|
||||
crypto::{verify_tls12_signature, verify_tls13_signature},
|
||||
pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime},
|
||||
crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider},
|
||||
pki_types::{self, pem::PemObject, CertificateDer, PrivateKeyDer, ServerName, UnixTime},
|
||||
server::ParsedCertificate,
|
||||
ClientConfig, DigitallySignedStruct, Error as TlsError, RootCertStore, SignatureScheme,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
use crate::transport::smtp::{error, Error};
|
||||
|
||||
/// TLS protocol versions.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[non_exhaustive]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
pub enum TlsVersion {
|
||||
/// TLS 1.0
|
||||
///
|
||||
@@ -59,31 +58,75 @@ pub enum TlsVersion {
|
||||
Tlsv13,
|
||||
}
|
||||
|
||||
/// How to apply TLS to a client connection
|
||||
/// Specifies how to establish a TLS connection
|
||||
///
|
||||
/// TLDR: Use [`Tls::Wrapper`] or [`Tls::Required`] when
|
||||
/// connecting to a remote server, [`Tls::None`] when
|
||||
/// connecting to a local server.
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[cfg_attr(
|
||||
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
|
||||
deprecated(
|
||||
note = "starting from lettre v0.12 `Tls` won't be available when none of the TLS backends are enabled"
|
||||
)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub enum Tls {
|
||||
/// Insecure connection only (for testing purposes)
|
||||
/// Insecure (plaintext) connection only.
|
||||
///
|
||||
/// This option **always** uses a plaintext connection and should only
|
||||
/// be used for trusted local relays. It is **highly discouraged**
|
||||
/// for remote servers, as it exposes credentials and emails to potential
|
||||
/// interception.
|
||||
///
|
||||
/// Note: Servers requiring credentials or emails to be sent over TLS
|
||||
/// may reject connections when this option is used.
|
||||
None,
|
||||
/// Start with insecure connection and use `STARTTLS` when available
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
/// Begin with a plaintext connection and attempt to use `STARTTLS` if available.
|
||||
///
|
||||
/// lettre will try to upgrade to a TLS-secured connection but will fall back
|
||||
/// to plaintext if the server does not support TLS. This option is provided for
|
||||
/// compatibility but is **strongly discouraged**, as it exposes connections to
|
||||
/// potential MITM (man-in-the-middle) attacks.
|
||||
///
|
||||
/// Warning: A malicious intermediary could intercept the `STARTTLS` flag,
|
||||
/// causing lettre to believe the server only supports plaintext connections.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
Opportunistic(TlsParameters),
|
||||
/// Start with insecure connection and require `STARTTLS`
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
/// Begin with a plaintext connection and require `STARTTLS` for security.
|
||||
///
|
||||
/// lettre will upgrade plaintext TCP connections to TLS before transmitting
|
||||
/// any sensitive data. If the server does not support TLS, the connection
|
||||
/// attempt will fail, ensuring no credentials or emails are sent in plaintext.
|
||||
///
|
||||
/// Unlike [`Tls::Opportunistic`], this option is secure against MITM attacks.
|
||||
/// For optimal security and performance, consider using [`Tls::Wrapper`] instead,
|
||||
/// as it requires fewer roundtrips to establish a secure connection.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
Required(TlsParameters),
|
||||
/// Use TLS wrapped connection
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
/// Establish a connection wrapped in TLS from the start.
|
||||
///
|
||||
/// lettre connects to the server and immediately performs a TLS handshake.
|
||||
/// If the handshake fails, the connection attempt is aborted without
|
||||
/// transmitting any sensitive data.
|
||||
///
|
||||
/// This is the fastest and most secure option for establishing a connection.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
Wrapper(TlsParameters),
|
||||
}
|
||||
@@ -92,11 +135,11 @@ impl Debug for Tls {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self {
|
||||
Self::None => f.pad("None"),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
Self::Opportunistic(_) => f.pad("Opportunistic"),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
Self::Required(_) => f.pad("Required"),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
Self::Wrapper(_) => f.pad("Wrapper"),
|
||||
}
|
||||
}
|
||||
@@ -105,14 +148,25 @@ impl Debug for Tls {
|
||||
/// Source for the base set of root certificates to trust.
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[cfg_attr(
|
||||
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
|
||||
deprecated(
|
||||
note = "starting from lettre v0.12 `CertificateStore` won't be available when none of the TLS backends are enabled"
|
||||
)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub enum CertificateStore {
|
||||
/// Use the default for the TLS backend.
|
||||
///
|
||||
/// For native-tls, this will use the system certificate store on Windows, the keychain on
|
||||
/// macOS, and OpenSSL directories on Linux (usually `/etc/ssl`).
|
||||
///
|
||||
/// For rustls, this will also use the system store if the `rustls-native-certs` feature is
|
||||
/// enabled, or will fall back to `webpki-roots`.
|
||||
/// For rustls, this will use the system certificate verifier if the `rustls-platform-verifier`
|
||||
/// feature is enabled. If the `rustls-native-certs` feature is enabled, system certificate
|
||||
/// store will be used. Otherwise, it will fall back to `webpki-roots`.
|
||||
///
|
||||
/// The boring-tls backend uses the same logic as OpenSSL on all platforms.
|
||||
#[default]
|
||||
@@ -120,7 +174,7 @@ pub enum CertificateStore {
|
||||
/// Use a hardcoded set of Mozilla roots via the `webpki-roots` crate.
|
||||
///
|
||||
/// This option is only available in the rustls backend.
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(all(feature = "rustls", feature = "webpki-roots"))]
|
||||
WebpkiRoots,
|
||||
/// Don't use any system certificates.
|
||||
None,
|
||||
@@ -128,16 +182,34 @@ pub enum CertificateStore {
|
||||
|
||||
/// Parameters to use for secure clients
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(
|
||||
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
|
||||
deprecated(
|
||||
note = "starting from lettre v0.12 `TlsParameters` won't be available when none of the TLS backends are enabled"
|
||||
)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub struct TlsParameters {
|
||||
pub(crate) connector: InnerTlsParameters,
|
||||
/// The domain name which is expected in the TLS certificate from the server
|
||||
pub(super) domain: String,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
pub(super) accept_invalid_hostnames: bool,
|
||||
}
|
||||
|
||||
/// Builder for `TlsParameters`
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(
|
||||
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
|
||||
deprecated(
|
||||
note = "starting from lettre v0.12 `TlsParametersBuilder` won't be available when none of the TLS backends are enabled"
|
||||
)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub struct TlsParametersBuilder {
|
||||
domain: String,
|
||||
cert_store: CertificateStore,
|
||||
@@ -145,7 +217,7 @@ pub struct TlsParametersBuilder {
|
||||
identity: Option<Identity>,
|
||||
accept_invalid_hostnames: bool,
|
||||
accept_invalid_certs: bool,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
min_tls_version: TlsVersion,
|
||||
}
|
||||
|
||||
@@ -159,7 +231,7 @@ impl TlsParametersBuilder {
|
||||
identity: None,
|
||||
accept_invalid_hostnames: false,
|
||||
accept_invalid_certs: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
min_tls_version: TlsVersion::Tlsv12,
|
||||
}
|
||||
}
|
||||
@@ -188,6 +260,8 @@ impl TlsParametersBuilder {
|
||||
|
||||
/// Controls whether certificates with an invalid hostname are accepted
|
||||
///
|
||||
/// This option is silently disabled when using `rustls-platform-verifier`.
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
///
|
||||
/// # Warning
|
||||
@@ -197,10 +271,10 @@ impl TlsParametersBuilder {
|
||||
/// including those from other sites, are trusted.
|
||||
///
|
||||
/// This method introduces significant vulnerabilities to man-in-the-middle attacks.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
|
||||
self.accept_invalid_hostnames = accept_invalid_hostnames;
|
||||
@@ -210,7 +284,11 @@ impl TlsParametersBuilder {
|
||||
/// Controls which minimum TLS version is allowed
|
||||
///
|
||||
/// Defaults to [`Tlsv12`][TlsVersion::Tlsv12].
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn set_min_tls_version(mut self, min_tls_version: TlsVersion) -> Self {
|
||||
self.min_tls_version = min_tls_version;
|
||||
self
|
||||
@@ -239,17 +317,17 @@ impl TlsParametersBuilder {
|
||||
|
||||
/// Creates a new `TlsParameters` using native-tls, boring-tls or rustls
|
||||
/// depending on which one is available
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn build(self) -> Result<TlsParameters, Error> {
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(feature = "rustls")]
|
||||
return self.build_rustls();
|
||||
#[cfg(all(not(feature = "rustls-tls"), feature = "native-tls"))]
|
||||
#[cfg(all(not(feature = "rustls"), feature = "native-tls"))]
|
||||
return self.build_native();
|
||||
#[cfg(all(not(feature = "rustls-tls"), feature = "boring-tls"))]
|
||||
#[cfg(all(not(feature = "rustls"), feature = "boring-tls"))]
|
||||
return self.build_boring();
|
||||
}
|
||||
|
||||
@@ -295,10 +373,8 @@ impl TlsParametersBuilder {
|
||||
|
||||
let connector = tls_builder.build().map_err(error::tls)?;
|
||||
Ok(TlsParameters {
|
||||
connector: InnerTlsParameters::NativeTls(connector),
|
||||
connector: InnerTlsParameters::NativeTls { connector },
|
||||
domain: self.domain,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
accept_invalid_hostnames: self.accept_invalid_hostnames,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -356,15 +432,17 @@ impl TlsParametersBuilder {
|
||||
.map_err(error::tls)?;
|
||||
let connector = tls_builder.build();
|
||||
Ok(TlsParameters {
|
||||
connector: InnerTlsParameters::BoringTls(connector),
|
||||
connector: InnerTlsParameters::BoringTls {
|
||||
connector,
|
||||
accept_invalid_hostnames: self.accept_invalid_hostnames,
|
||||
},
|
||||
domain: self.domain,
|
||||
accept_invalid_hostnames: self.accept_invalid_hostnames,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using rustls with the provided configuration
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
||||
#[cfg(feature = "rustls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
|
||||
let just_version3 = &[&rustls::version::TLS13];
|
||||
let supported_versions = match self.min_tls_version {
|
||||
@@ -378,18 +456,19 @@ impl TlsParametersBuilder {
|
||||
TlsVersion::Tlsv13 => just_version3,
|
||||
};
|
||||
|
||||
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 crypto_provider = crate::rustls_crypto::crypto_provider();
|
||||
let tls = ClientConfig::builder_with_provider(Arc::clone(&crypto_provider))
|
||||
.with_protocol_versions(supported_versions)
|
||||
.map_err(error::tls)?;
|
||||
|
||||
// Build TLS config
|
||||
let signature_algorithms = provider.signature_verification_algorithms;
|
||||
|
||||
let mut root_cert_store = RootCertStore::empty();
|
||||
|
||||
#[cfg(feature = "rustls-native-certs")]
|
||||
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
|
||||
#[cfg(all(
|
||||
not(feature = "rustls-platform-verifier"),
|
||||
feature = "rustls-native-certs"
|
||||
))]
|
||||
fn load_native_roots(store: &mut RootCertStore) {
|
||||
let rustls_native_certs::CertificateResult { certs, errors, .. } =
|
||||
rustls_native_certs::load_native_certs();
|
||||
let errors_len = errors.len();
|
||||
@@ -399,22 +478,38 @@ impl TlsParametersBuilder {
|
||||
tracing::debug!(
|
||||
"loaded platform certs with {errors_len} failing to load, {added} valid and {ignored} ignored (invalid) certs"
|
||||
);
|
||||
Ok(())
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
let _ = (errors_len, added, ignored);
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(all(feature = "rustls", feature = "webpki-roots"))]
|
||||
fn load_webpki_roots(store: &mut RootCertStore) {
|
||||
store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "rustls-platform-verifier"), allow(unused_mut))]
|
||||
let mut extra_roots = None::<Vec<CertificateDer<'static>>>;
|
||||
match self.cert_store {
|
||||
CertificateStore::Default => {
|
||||
#[cfg(feature = "rustls-native-certs")]
|
||||
load_native_roots(&mut root_cert_store)?;
|
||||
#[cfg(not(feature = "rustls-native-certs"))]
|
||||
#[cfg(feature = "rustls-platform-verifier")]
|
||||
{
|
||||
extra_roots = Some(Vec::new());
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
not(feature = "rustls-platform-verifier"),
|
||||
feature = "rustls-native-certs"
|
||||
))]
|
||||
load_native_roots(&mut root_cert_store);
|
||||
|
||||
#[cfg(all(
|
||||
not(feature = "rustls-platform-verifier"),
|
||||
not(feature = "rustls-native-certs"),
|
||||
feature = "webpki-roots"
|
||||
))]
|
||||
load_webpki_roots(&mut root_cert_store);
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(all(feature = "rustls", feature = "webpki-roots"))]
|
||||
CertificateStore::WebpkiRoots => {
|
||||
load_webpki_roots(&mut root_cert_store);
|
||||
}
|
||||
@@ -422,21 +517,43 @@ impl TlsParametersBuilder {
|
||||
}
|
||||
for cert in self.root_certs {
|
||||
for rustls_cert in cert.rustls {
|
||||
#[cfg(feature = "rustls-platform-verifier")]
|
||||
if let Some(extra_roots) = &mut extra_roots {
|
||||
extra_roots.push(rustls_cert.clone());
|
||||
}
|
||||
root_cert_store.add(rustls_cert).map_err(error::tls)?;
|
||||
}
|
||||
}
|
||||
|
||||
let tls = if self.accept_invalid_certs || self.accept_invalid_hostnames {
|
||||
let tls = if self.accept_invalid_certs
|
||||
|| (extra_roots.is_none() && self.accept_invalid_hostnames)
|
||||
{
|
||||
let verifier = InvalidCertsVerifier {
|
||||
ignore_invalid_hostnames: self.accept_invalid_hostnames,
|
||||
ignore_invalid_certs: self.accept_invalid_certs,
|
||||
roots: root_cert_store,
|
||||
signature_algorithms,
|
||||
crypto_provider,
|
||||
};
|
||||
tls.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(verifier))
|
||||
} else {
|
||||
tls.with_root_certificates(root_cert_store)
|
||||
#[cfg(feature = "rustls-platform-verifier")]
|
||||
if let Some(extra_roots) = extra_roots {
|
||||
tls.dangerous().with_custom_certificate_verifier(Arc::new(
|
||||
rustls_platform_verifier::Verifier::new_with_extra_roots(
|
||||
extra_roots,
|
||||
crypto_provider,
|
||||
)
|
||||
.map_err(error::tls)?,
|
||||
))
|
||||
} else {
|
||||
tls.with_root_certificates(root_cert_store)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "rustls-platform-verifier"))]
|
||||
{
|
||||
tls.with_root_certificates(root_cert_store)
|
||||
}
|
||||
};
|
||||
|
||||
let tls = if let Some(identity) = self.identity {
|
||||
@@ -448,32 +565,35 @@ impl TlsParametersBuilder {
|
||||
};
|
||||
|
||||
Ok(TlsParameters {
|
||||
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
|
||||
connector: InnerTlsParameters::Rustls {
|
||||
config: Arc::new(tls),
|
||||
},
|
||||
domain: self.domain,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
accept_invalid_hostnames: self.accept_invalid_hostnames,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum InnerTlsParameters {
|
||||
pub(crate) enum InnerTlsParameters {
|
||||
#[cfg(feature = "native-tls")]
|
||||
NativeTls(TlsConnector),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
RustlsTls(Arc<ClientConfig>),
|
||||
NativeTls { connector: TlsConnector },
|
||||
#[cfg(feature = "rustls")]
|
||||
Rustls { config: Arc<ClientConfig> },
|
||||
#[cfg(feature = "boring-tls")]
|
||||
BoringTls(SslConnector),
|
||||
BoringTls {
|
||||
connector: SslConnector,
|
||||
accept_invalid_hostnames: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl TlsParameters {
|
||||
/// Creates a new `TlsParameters` using native-tls or rustls
|
||||
/// depending on which one is available
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn new(domain: String) -> Result<Self, Error> {
|
||||
TlsParametersBuilder::new(domain).build()
|
||||
@@ -492,8 +612,8 @@ impl TlsParameters {
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using rustls
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
||||
#[cfg(feature = "rustls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
pub fn new_rustls(domain: String) -> Result<Self, Error> {
|
||||
TlsParametersBuilder::new(domain).build_rustls()
|
||||
}
|
||||
@@ -513,16 +633,26 @@ impl TlsParameters {
|
||||
/// A certificate that can be used with [`TlsParametersBuilder::add_root_certificate`]
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[cfg_attr(
|
||||
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
|
||||
deprecated(
|
||||
note = "starting from lettre v0.12 `Certificate` won't be available when none of the TLS backends are enabled"
|
||||
)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub struct Certificate {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: native_tls::Certificate,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(feature = "rustls")]
|
||||
rustls: Vec<CertificateDer<'static>>,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: boring::x509::X509,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
impl Certificate {
|
||||
/// Create a `Certificate` from a DER encoded certificate
|
||||
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
|
||||
@@ -535,7 +665,7 @@ impl Certificate {
|
||||
Ok(Self {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: native_tls_cert,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(feature = "rustls")]
|
||||
rustls: vec![der.into()],
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: boring_tls_cert,
|
||||
@@ -550,20 +680,17 @@ impl Certificate {
|
||||
#[cfg(feature = "boring-tls")]
|
||||
let boring_tls_cert = boring::x509::X509::from_pem(pem).map_err(error::tls)?;
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(feature = "rustls")]
|
||||
let rustls_cert = {
|
||||
use std::io::Cursor;
|
||||
|
||||
let mut pem = Cursor::new(pem);
|
||||
rustls_pemfile::certs(&mut pem)
|
||||
.collect::<io::Result<Vec<_>>>()
|
||||
CertificateDer::pem_slice_iter(pem)
|
||||
.collect::<Result<Vec<_>, pki_types::pem::Error>>()
|
||||
.map_err(|_| error::tls("invalid certificates"))?
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: native_tls_cert,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(feature = "rustls")]
|
||||
rustls: rustls_cert,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: boring_tls_cert,
|
||||
@@ -579,10 +706,20 @@ impl Debug for Certificate {
|
||||
|
||||
/// An identity that can be used with [`TlsParametersBuilder::identify_with`]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[cfg_attr(
|
||||
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
|
||||
deprecated(
|
||||
note = "starting from lettre v0.12 `Identity` won't be available when none of the TLS backends are enabled"
|
||||
)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub struct Identity {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: native_tls::Identity,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(feature = "rustls")]
|
||||
rustls_tls: (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: (boring::x509::X509, PKey<boring::pkey::Private>),
|
||||
@@ -599,7 +736,7 @@ impl Clone for Identity {
|
||||
Identity {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: self.native_tls.clone(),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(feature = "rustls")]
|
||||
rustls_tls: (self.rustls_tls.0.clone(), self.rustls_tls.1.clone_key()),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: (self.boring_tls.0.clone(), self.boring_tls.1.clone()),
|
||||
@@ -607,13 +744,13 @@ impl Clone for Identity {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
impl Identity {
|
||||
pub fn from_pem(pem: &[u8], key: &[u8]) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: Identity::from_pem_native_tls(pem, key)?,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(feature = "rustls")]
|
||||
rustls_tls: Identity::from_pem_rustls_tls(pem, key)?,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: Identity::from_pem_boring_tls(pem, key)?,
|
||||
@@ -625,13 +762,19 @@ impl Identity {
|
||||
native_tls::Identity::from_pkcs8(pem, key).map_err(error::tls)
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(feature = "rustls")]
|
||||
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();
|
||||
let key = match PrivateKeyDer::from_pem_slice(key) {
|
||||
Ok(key) => key,
|
||||
Err(pki_types::pem::Error::NoItemsFound) => {
|
||||
return Err(error::tls("no private key found"))
|
||||
}
|
||||
Err(err) => return Err(error::tls(err)),
|
||||
};
|
||||
|
||||
Ok((vec![pem.to_owned().into()], key))
|
||||
}
|
||||
|
||||
@@ -646,16 +789,16 @@ impl Identity {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(feature = "rustls")]
|
||||
#[derive(Debug)]
|
||||
struct InvalidCertsVerifier {
|
||||
ignore_invalid_hostnames: bool,
|
||||
ignore_invalid_certs: bool,
|
||||
roots: RootCertStore,
|
||||
signature_algorithms: WebPkiSupportedAlgorithms,
|
||||
crypto_provider: Arc<CryptoProvider>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg(feature = "rustls")]
|
||||
impl ServerCertVerifier for InvalidCertsVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
@@ -673,7 +816,7 @@ impl ServerCertVerifier for InvalidCertsVerifier {
|
||||
&self.roots,
|
||||
intermediates,
|
||||
now,
|
||||
self.signature_algorithms.all,
|
||||
self.crypto_provider.signature_verification_algorithms.all,
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -693,7 +836,7 @@ impl ServerCertVerifier for InvalidCertsVerifier {
|
||||
message,
|
||||
cert,
|
||||
dss,
|
||||
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
|
||||
&self.crypto_provider.signature_verification_algorithms,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -707,12 +850,12 @@ impl ServerCertVerifier for InvalidCertsVerifier {
|
||||
message,
|
||||
cert,
|
||||
dss,
|
||||
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
|
||||
&self.crypto_provider.signature_verification_algorithms,
|
||||
)
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
rustls::crypto::ring::default_provider()
|
||||
self.crypto_provider
|
||||
.signature_verification_algorithms
|
||||
.supported_schemes()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use url::Url;
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
use super::client::{Tls, TlsParameters};
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use super::AsyncSmtpTransportBuilder;
|
||||
@@ -11,6 +13,7 @@ use super::{
|
||||
|
||||
pub(crate) trait TransportBuilder {
|
||||
fn new<T: Into<String>>(server: T) -> Self;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
fn tls(self, tls: super::Tls) -> Self;
|
||||
fn port(self, port: u16) -> Self;
|
||||
fn credentials(self, credentials: Credentials) -> Self;
|
||||
@@ -22,6 +25,7 @@ impl TransportBuilder for SmtpTransportBuilder {
|
||||
Self::new(server)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
fn tls(self, tls: super::Tls) -> Self {
|
||||
self.tls(tls)
|
||||
}
|
||||
@@ -45,6 +49,7 @@ impl TransportBuilder for AsyncSmtpTransportBuilder {
|
||||
Self::new(server)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
fn tls(self, tls: super::Tls) -> Self {
|
||||
self.tls(tls)
|
||||
}
|
||||
@@ -62,7 +67,7 @@ impl TransportBuilder for AsyncSmtpTransportBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new SmtpTransportBuilder or AsyncSmtpTransportBuilder from a connection URL
|
||||
/// Create a new `SmtpTransportBuilder` or `AsyncSmtpTransportBuilder` from a connection URL
|
||||
pub(crate) fn from_connection_url<B: TransportBuilder>(connection_url: &str) -> Result<B, Error> {
|
||||
let connection_url = Url::parse(connection_url).map_err(error::connection)?;
|
||||
let tls: Option<String> = connection_url
|
||||
@@ -80,30 +85,30 @@ pub(crate) fn from_connection_url<B: TransportBuilder>(connection_url: &str) ->
|
||||
("smtp", None) => {
|
||||
builder = builder.port(connection_url.port().unwrap_or(SMTP_PORT));
|
||||
}
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
("smtp", Some("required")) => {
|
||||
builder = builder
|
||||
.port(connection_url.port().unwrap_or(SUBMISSION_PORT))
|
||||
.tls(Tls::Required(TlsParameters::new(host.into())?))
|
||||
.tls(Tls::Required(TlsParameters::new(host.into())?));
|
||||
}
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
("smtp", Some("opportunistic")) => {
|
||||
builder = builder
|
||||
.port(connection_url.port().unwrap_or(SUBMISSION_PORT))
|
||||
.tls(Tls::Opportunistic(TlsParameters::new(host.into())?))
|
||||
.tls(Tls::Opportunistic(TlsParameters::new(host.into())?));
|
||||
}
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
("smtps", _) => {
|
||||
builder = builder
|
||||
.port(connection_url.port().unwrap_or(SUBMISSIONS_PORT))
|
||||
.tls(Tls::Wrapper(TlsParameters::new(host.into())?))
|
||||
.tls(Tls::Wrapper(TlsParameters::new(host.into())?));
|
||||
}
|
||||
(scheme, tls) => {
|
||||
return Err(error::connection(format!(
|
||||
"Unknown scheme '{scheme}' or tls parameter '{tls:?}', note that a transport with TLS requires one of the TLS features"
|
||||
)))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// use the path segment of the URL as name in the name in the HELO / EHLO command
|
||||
if connection_url.path().len() > 1 {
|
||||
@@ -115,7 +120,7 @@ pub(crate) fn from_connection_url<B: TransportBuilder>(connection_url: &str) ->
|
||||
let percent_decode = |s: &str| {
|
||||
percent_encoding::percent_decode_str(s)
|
||||
.decode_utf8()
|
||||
.map(|cow| cow.into_owned())
|
||||
.map(Cow::into_owned)
|
||||
.map_err(error::connection)
|
||||
};
|
||||
let credentials = Credentials::new(
|
||||
|
||||
@@ -68,15 +68,20 @@ impl Error {
|
||||
}
|
||||
|
||||
/// Returns true if the error is from TLS
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn is_tls(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Tls)
|
||||
}
|
||||
|
||||
/// Returns true if the error is because the transport was shut down
|
||||
pub fn is_transport_shutdown(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::TransportShutdown)
|
||||
}
|
||||
|
||||
/// Returns the status code, if the error was generated from a response.
|
||||
pub fn status(&self) -> Option<Code> {
|
||||
match self.inner.kind {
|
||||
@@ -107,10 +112,12 @@ pub(crate) enum Kind {
|
||||
/// TLS error
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
Tls,
|
||||
/// Transport shutdown error
|
||||
TransportShutdown,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
@@ -134,15 +141,16 @@ impl fmt::Display for Error {
|
||||
Kind::Client => f.write_str("internal client error")?,
|
||||
Kind::Network => f.write_str("network error")?,
|
||||
Kind::Connection => f.write_str("Connection error")?,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
Kind::Tls => f.write_str("tls error")?,
|
||||
Kind::TransportShutdown => f.write_str("transport has been shut down")?,
|
||||
Kind::Transient(code) => {
|
||||
write!(f, "transient error ({code})")?;
|
||||
}
|
||||
Kind::Permanent(code) => {
|
||||
write!(f, "permanent error ({code})")?;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(e) = &self.inner.source {
|
||||
write!(f, ": {e}")?;
|
||||
@@ -185,7 +193,11 @@ pub(crate) fn connection<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Connection, Some(e))
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Tls, Some(e))
|
||||
}
|
||||
|
||||
pub(crate) fn transport_shutdown() -> Error {
|
||||
Error::new::<BoxError>(Kind::TransportShutdown, None)
|
||||
}
|
||||
|
||||
@@ -129,9 +129,8 @@ impl Display for ServerInfo {
|
||||
impl ServerInfo {
|
||||
/// Parses a EHLO response to create a `ServerInfo`
|
||||
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
|
||||
let name = match response.first_word() {
|
||||
Some(name) => name,
|
||||
None => return Err(error::response("Could not read server name")),
|
||||
let Some(name) = response.first_word() else {
|
||||
return Err(error::response("Could not read server name"));
|
||||
};
|
||||
|
||||
let mut features: HashSet<Extension> = HashSet::new();
|
||||
@@ -169,7 +168,7 @@ impl ServerInfo {
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ServerInfo {
|
||||
|
||||
@@ -26,43 +26,17 @@
|
||||
//!
|
||||
//! The relay server can be the local email server, a specific host or a third-party service.
|
||||
//!
|
||||
//! #### Simple example
|
||||
//! #### Simple example with authentication
|
||||
//!
|
||||
//! This is the most basic example of usage:
|
||||
//! A good starting point for sending emails via SMTP relay is to
|
||||
//! do the following:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use lettre::{Message, SmtpTransport, Transport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! // Create TLS transport on port 465
|
||||
//! let sender = SmtpTransport::relay("smtp.example.com")?.build();
|
||||
//! // Send the email via remote relay
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! #### Authentication
|
||||
//!
|
||||
//! Example with authentication and connection pool:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls")))]
|
||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use lettre::{
|
||||
//! transport::smtp::{
|
||||
//! authentication::{Credentials, Mechanism},
|
||||
//! PoolConfig,
|
||||
//! },
|
||||
//! message::header::ContentType,
|
||||
//! transport::smtp::authentication::{Credentials, Mechanism},
|
||||
//! Message, SmtpTransport, Transport,
|
||||
//! };
|
||||
//!
|
||||
@@ -71,35 +45,39 @@
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! // Create TLS transport on port 587 with STARTTLS
|
||||
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
|
||||
//! // Create the SMTPS transport
|
||||
//! let sender = SmtpTransport::relay("smtp.example.com")?
|
||||
//! // Add credentials for authentication
|
||||
//! .credentials(Credentials::new(
|
||||
//! "username".to_owned(),
|
||||
//! "password".to_owned(),
|
||||
//! ))
|
||||
//! // Configure expected authentication mechanism
|
||||
//! // Optionally configure expected authentication mechanism
|
||||
//! .authentication(vec![Mechanism::Plain])
|
||||
//! // Connection pool settings
|
||||
//! .pool_config(PoolConfig::new().max_size(20))
|
||||
//! .build();
|
||||
//!
|
||||
//! // Send the email via remote relay
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(&email)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! You can specify custom TLS settings:
|
||||
//! #### Shortening configuration
|
||||
//!
|
||||
//! It can be very repetitive to ask the user for every SMTP connection parameter.
|
||||
//! In some cases this can be simplified by using a connection URI instead.
|
||||
//!
|
||||
//! For more information take a look at [`SmtpTransport::from_url`] or [`AsyncSmtpTransport::from_url`].
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls")))]
|
||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use lettre::{
|
||||
//! transport::smtp::client::{Tls, TlsParameters},
|
||||
//! message::header::ContentType,
|
||||
//! transport::smtp::authentication::{Credentials, Mechanism},
|
||||
//! Message, SmtpTransport, Transport,
|
||||
//! };
|
||||
//!
|
||||
@@ -108,25 +86,106 @@
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! // Custom TLS configuration
|
||||
//! let tls = TlsParameters::builder("smtp.example.com".to_owned())
|
||||
//! .dangerous_accept_invalid_certs(true)
|
||||
//! // Create the SMTPS transport
|
||||
//! let sender = SmtpTransport::from_url("smtps://username:password@smtp.example.com")?.build();
|
||||
//!
|
||||
//! // Send the email via remote relay
|
||||
//! sender.send(&email)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! #### Advanced configuration with custom TLS settings
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls")))]
|
||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use std::fs;
|
||||
//!
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType,
|
||||
//! transport::smtp::client::{Certificate, Tls, TlsParameters},
|
||||
//! Message, SmtpTransport, Transport,
|
||||
//! };
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! // Custom TLS configuration - Use a self signed certificate
|
||||
//! let cert = fs::read("self-signed.crt")?;
|
||||
//! let cert = Certificate::from_pem(&cert)?;
|
||||
//! let tls = TlsParameters::builder(/* TLS SNI value */ "smtp.example.com".to_owned())
|
||||
//! .add_root_certificate(cert)
|
||||
//! .build()?;
|
||||
//!
|
||||
//! // Create TLS transport on port 465
|
||||
//! // Create the SMTPS transport
|
||||
//! let sender = SmtpTransport::relay("smtp.example.com")?
|
||||
//! // Custom TLS configuration
|
||||
//! .tls(Tls::Required(tls))
|
||||
//! .tls(Tls::Wrapper(tls))
|
||||
//! .build();
|
||||
//!
|
||||
//! // Send the email via remote relay
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(&email)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! #### Connection pooling
|
||||
//!
|
||||
//! [`SmtpTransport`] and [`AsyncSmtpTransport`] store connections in
|
||||
//! a connection pool by default. This avoids connecting and disconnecting
|
||||
//! from the relay server for every message the application tries to send. For the connection pool
|
||||
//! to work the instance of the transport **must** be reused.
|
||||
//! In a webserver context it may go about this:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls")))]
|
||||
//! # fn test() {
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType,
|
||||
//! transport::smtp::{authentication::Credentials, PoolConfig},
|
||||
//! Message, SmtpTransport, Transport,
|
||||
//! };
|
||||
//! #
|
||||
//! # type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
//!
|
||||
//! /// The global application state
|
||||
//! #[derive(Debug)]
|
||||
//! struct AppState {
|
||||
//! smtp: SmtpTransport,
|
||||
//! // ... other global application parameters
|
||||
//! }
|
||||
//!
|
||||
//! impl AppState {
|
||||
//! pub fn new(smtp_url: &str) -> Result<Self> {
|
||||
//! let smtp = SmtpTransport::from_url(smtp_url)?.build();
|
||||
//! Ok(Self { smtp })
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! fn handle_request(app_state: &AppState) -> Result<String> {
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! // Send the email via remote relay
|
||||
//! app_state.smtp.send(&email)?;
|
||||
//!
|
||||
//! Ok("The email has successfully been sent!".to_owned())
|
||||
//! }
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -140,7 +199,7 @@ pub use self::{
|
||||
error::Error,
|
||||
transport::{SmtpTransport, SmtpTransportBuilder},
|
||||
};
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
use crate::transport::smtp::client::TlsParameters;
|
||||
use crate::transport::smtp::{
|
||||
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
sync::{Arc, OnceLock},
|
||||
time::{Duration, Instant},
|
||||
@@ -15,11 +14,15 @@ use super::{
|
||||
super::{client::AsyncSmtpConnection, Error},
|
||||
PoolConfig,
|
||||
};
|
||||
use crate::{executor::SpawnHandle, transport::smtp::async_transport::AsyncSmtpClient, Executor};
|
||||
use crate::{
|
||||
executor::SpawnHandle,
|
||||
transport::smtp::{async_transport::AsyncSmtpClient, error},
|
||||
Executor,
|
||||
};
|
||||
|
||||
pub struct Pool<E: Executor> {
|
||||
pub(crate) struct Pool<E: Executor> {
|
||||
config: PoolConfig,
|
||||
connections: Mutex<Vec<ParkedConnection>>,
|
||||
connections: Mutex<Option<Vec<ParkedConnection>>>,
|
||||
client: AsyncSmtpClient<E>,
|
||||
handle: OnceLock<E::Handle>,
|
||||
}
|
||||
@@ -29,16 +32,16 @@ struct ParkedConnection {
|
||||
since: Instant,
|
||||
}
|
||||
|
||||
pub struct PooledConnection<E: Executor> {
|
||||
pub(crate) struct PooledConnection<E: Executor> {
|
||||
conn: Option<AsyncSmtpConnection>,
|
||||
pool: Arc<Pool<E>>,
|
||||
}
|
||||
|
||||
impl<E: Executor> Pool<E> {
|
||||
pub fn new(config: PoolConfig, client: AsyncSmtpClient<E>) -> Arc<Self> {
|
||||
pub(crate) fn new(config: PoolConfig, client: AsyncSmtpClient<E>) -> Arc<Self> {
|
||||
let pool = Arc::new(Self {
|
||||
config,
|
||||
connections: Mutex::new(Vec::new()),
|
||||
connections: Mutex::new(Some(Vec::new())),
|
||||
client,
|
||||
handle: OnceLock::new(),
|
||||
});
|
||||
@@ -60,6 +63,10 @@ impl<E: Executor> Pool<E> {
|
||||
#[allow(clippy::needless_collect)]
|
||||
let (count, dropped) = {
|
||||
let mut connections = pool.connections.lock().await;
|
||||
let Some(connections) = connections.as_mut() else {
|
||||
// The transport was shut down
|
||||
return;
|
||||
};
|
||||
|
||||
let to_drop = connections
|
||||
.iter()
|
||||
@@ -78,7 +85,7 @@ impl<E: Executor> Pool<E> {
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let mut created = 0;
|
||||
for _ in count..=(min_idle as usize) {
|
||||
for _ in count..(min_idle as usize) {
|
||||
let conn = match pool.client.connection().await {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
@@ -92,6 +99,11 @@ impl<E: Executor> Pool<E> {
|
||||
};
|
||||
|
||||
let mut connections = pool.connections.lock().await;
|
||||
let Some(connections) = connections.as_mut() else {
|
||||
// The transport was shut down
|
||||
return;
|
||||
};
|
||||
|
||||
connections.push(ParkedConnection::park(conn));
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
@@ -109,7 +121,7 @@ impl<E: Executor> Pool<E> {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropped {} idle connections", dropped.len());
|
||||
|
||||
abort_concurrent(dropped.into_iter().map(|conn| conn.unpark()))
|
||||
abort_concurrent(dropped.into_iter().map(ParkedConnection::unpark))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
@@ -134,10 +146,25 @@ impl<E: Executor> Pool<E> {
|
||||
pool
|
||||
}
|
||||
|
||||
pub async fn connection(self: &Arc<Self>) -> Result<PooledConnection<E>, Error> {
|
||||
pub(crate) async fn shutdown(&self) {
|
||||
let connections = { self.connections.lock().await.take() };
|
||||
if let Some(connections) = connections {
|
||||
abort_concurrent(connections.into_iter().map(ParkedConnection::unpark)).await;
|
||||
}
|
||||
|
||||
if let Some(handle) = self.handle.get() {
|
||||
handle.shutdown().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn connection(self: &Arc<Self>) -> Result<PooledConnection<E>, Error> {
|
||||
loop {
|
||||
let conn = {
|
||||
let mut connections = self.connections.lock().await;
|
||||
let Some(connections) = connections.as_mut() else {
|
||||
// The transport was shut down
|
||||
return Err(error::transport_shutdown());
|
||||
};
|
||||
connections.pop()
|
||||
};
|
||||
|
||||
@@ -181,13 +208,20 @@ impl<E: Executor> Pool<E> {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("recycling connection");
|
||||
|
||||
let mut connections = self.connections.lock().await;
|
||||
if connections.len() >= self.config.max_size as usize {
|
||||
drop(connections);
|
||||
conn.abort().await;
|
||||
let mut connections_guard = self.connections.lock().await;
|
||||
|
||||
if let Some(connections) = connections_guard.as_mut() {
|
||||
if connections.len() >= self.config.max_size as usize {
|
||||
drop(connections_guard);
|
||||
conn.abort().await;
|
||||
} else {
|
||||
let conn = ParkedConnection::park(conn);
|
||||
connections.push(conn);
|
||||
}
|
||||
} else {
|
||||
let conn = ParkedConnection::park(conn);
|
||||
connections.push(conn);
|
||||
// The pool has already been shut down
|
||||
drop(connections_guard);
|
||||
conn.abort().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,7 +234,13 @@ impl<E: Executor> Debug for Pool<E> {
|
||||
.field(
|
||||
"connections",
|
||||
&match self.connections.try_lock() {
|
||||
Some(connections) => format!("{} connections", connections.len()),
|
||||
Some(connections) => {
|
||||
if let Some(connections) = connections.as_ref() {
|
||||
format!("{} connections", connections.len())
|
||||
} else {
|
||||
"SHUT DOWN".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
None => "LOCKED".to_owned(),
|
||||
},
|
||||
@@ -222,14 +262,16 @@ impl<E: Executor> Drop for Pool<E> {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping Pool");
|
||||
|
||||
let connections = mem::take(self.connections.get_mut());
|
||||
let connections = self.connections.get_mut().take();
|
||||
let handle = self.handle.take();
|
||||
E::spawn(async move {
|
||||
if let Some(handle) = handle {
|
||||
handle.shutdown().await;
|
||||
}
|
||||
|
||||
abort_concurrent(connections.into_iter().map(|conn| conn.unpark())).await;
|
||||
if let Some(connections) = connections {
|
||||
abort_concurrent(connections.into_iter().map(ParkedConnection::unpark)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub mod async_impl;
|
||||
pub mod sync_impl;
|
||||
pub(super) mod async_impl;
|
||||
pub(super) mod sync_impl;
|
||||
|
||||
/// Configuration for a connection pool
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
sync::{Arc, Mutex, TryLockError},
|
||||
sync::{mpsc, Arc, Mutex, TryLockError},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -11,11 +10,12 @@ use super::{
|
||||
super::{client::SmtpConnection, Error},
|
||||
PoolConfig,
|
||||
};
|
||||
use crate::transport::smtp::transport::SmtpClient;
|
||||
use crate::transport::smtp::{error, transport::SmtpClient};
|
||||
|
||||
pub struct Pool {
|
||||
pub(crate) struct Pool {
|
||||
config: PoolConfig,
|
||||
connections: Mutex<Vec<ParkedConnection>>,
|
||||
connections: Mutex<Option<Vec<ParkedConnection>>>,
|
||||
thread_terminator: mpsc::SyncSender<()>,
|
||||
client: SmtpClient,
|
||||
}
|
||||
|
||||
@@ -24,16 +24,19 @@ struct ParkedConnection {
|
||||
since: Instant,
|
||||
}
|
||||
|
||||
pub struct PooledConnection {
|
||||
pub(crate) struct PooledConnection {
|
||||
conn: Option<SmtpConnection>,
|
||||
pool: Arc<Pool>,
|
||||
}
|
||||
|
||||
impl Pool {
|
||||
pub fn new(config: PoolConfig, client: SmtpClient) -> Arc<Self> {
|
||||
pub(crate) fn new(config: PoolConfig, client: SmtpClient) -> Arc<Self> {
|
||||
let (thread_tx, thread_rx) = mpsc::sync_channel(1);
|
||||
|
||||
let pool = Arc::new(Self {
|
||||
config,
|
||||
connections: Mutex::new(Vec::new()),
|
||||
connections: Mutex::new(Some(Vec::new())),
|
||||
thread_terminator: thread_tx,
|
||||
client,
|
||||
});
|
||||
|
||||
@@ -54,6 +57,10 @@ impl Pool {
|
||||
#[allow(clippy::needless_collect)]
|
||||
let (count, dropped) = {
|
||||
let mut connections = pool.connections.lock().unwrap();
|
||||
let Some(connections) = connections.as_mut() else {
|
||||
// The transport was shut down
|
||||
return;
|
||||
};
|
||||
|
||||
let to_drop = connections
|
||||
.iter()
|
||||
@@ -72,7 +79,7 @@ impl Pool {
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let mut created = 0;
|
||||
for _ in count..=(min_idle as usize) {
|
||||
for _ in count..(min_idle as usize) {
|
||||
let conn = match pool.client.connection() {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
@@ -86,6 +93,11 @@ impl Pool {
|
||||
};
|
||||
|
||||
let mut connections = pool.connections.lock().unwrap();
|
||||
let Some(connections) = connections.as_mut() else {
|
||||
// The transport was shut down
|
||||
return;
|
||||
};
|
||||
|
||||
connections.push(ParkedConnection::park(conn));
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
@@ -109,7 +121,15 @@ impl Pool {
|
||||
}
|
||||
}
|
||||
|
||||
thread::sleep(idle_timeout);
|
||||
drop(pool);
|
||||
|
||||
match thread_rx.recv_timeout(idle_timeout) {
|
||||
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||
// The transport was shut down
|
||||
return;
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("couldn't spawn the Pool thread");
|
||||
@@ -118,10 +138,25 @@ impl Pool {
|
||||
pool
|
||||
}
|
||||
|
||||
pub fn connection(self: &Arc<Self>) -> Result<PooledConnection, Error> {
|
||||
pub(crate) fn shutdown(&self) {
|
||||
let connections = { self.connections.lock().unwrap().take() };
|
||||
if let Some(connections) = connections {
|
||||
for conn in connections {
|
||||
conn.unpark().abort();
|
||||
}
|
||||
}
|
||||
|
||||
_ = self.thread_terminator.try_send(());
|
||||
}
|
||||
|
||||
pub(crate) fn connection(self: &Arc<Self>) -> Result<PooledConnection, Error> {
|
||||
loop {
|
||||
let conn = {
|
||||
let mut connections = self.connections.lock().unwrap();
|
||||
let Some(connections) = connections.as_mut() else {
|
||||
// The transport was shut down
|
||||
return Err(error::transport_shutdown());
|
||||
};
|
||||
connections.pop()
|
||||
};
|
||||
|
||||
@@ -165,13 +200,20 @@ impl Pool {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("recycling connection");
|
||||
|
||||
let mut connections = self.connections.lock().unwrap();
|
||||
if connections.len() >= self.config.max_size as usize {
|
||||
drop(connections);
|
||||
conn.abort();
|
||||
let mut connections_guard = self.connections.lock().unwrap();
|
||||
|
||||
if let Some(connections) = connections_guard.as_mut() {
|
||||
if connections.len() >= self.config.max_size as usize {
|
||||
drop(connections_guard);
|
||||
conn.abort();
|
||||
} else {
|
||||
let conn = ParkedConnection::park(conn);
|
||||
connections.push(conn);
|
||||
}
|
||||
} else {
|
||||
let conn = ParkedConnection::park(conn);
|
||||
connections.push(conn);
|
||||
// The pool has already been shut down
|
||||
drop(connections_guard);
|
||||
conn.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,7 +226,13 @@ impl Debug for Pool {
|
||||
.field(
|
||||
"connections",
|
||||
&match self.connections.try_lock() {
|
||||
Ok(connections) => format!("{} connections", connections.len()),
|
||||
Ok(connections) => {
|
||||
if let Some(connections) = connections.as_ref() {
|
||||
format!("{} connections", connections.len())
|
||||
} else {
|
||||
"SHUT DOWN".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
Err(TryLockError::WouldBlock) => "LOCKED".to_owned(),
|
||||
Err(TryLockError::Poisoned(_)) => "POISONED".to_owned(),
|
||||
@@ -200,10 +248,11 @@ impl Drop for Pool {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping Pool");
|
||||
|
||||
let connections = mem::take(&mut *self.connections.get_mut().unwrap());
|
||||
for conn in connections {
|
||||
let mut conn = conn.unpark();
|
||||
conn.abort();
|
||||
if let Some(connections) = self.connections.get_mut().unwrap().take() {
|
||||
for conn in connections {
|
||||
let mut conn = conn.unpark();
|
||||
conn.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ use nom::{
|
||||
bytes::streaming::{tag, take_until},
|
||||
combinator::{complete, map},
|
||||
multi::many0,
|
||||
sequence::{preceded, tuple},
|
||||
IResult,
|
||||
sequence::preceded,
|
||||
IResult, Parser,
|
||||
};
|
||||
|
||||
use crate::transport::smtp::{error, Error};
|
||||
@@ -221,7 +221,8 @@ fn parse_severity(i: &str) -> IResult<&str, Severity> {
|
||||
map(tag("3"), |_| Severity::PositiveIntermediate),
|
||||
map(tag("4"), |_| Severity::TransientNegativeCompletion),
|
||||
map(tag("5"), |_| Severity::PermanentNegativeCompletion),
|
||||
))(i)
|
||||
))
|
||||
.parse(i)
|
||||
}
|
||||
|
||||
fn parse_category(i: &str) -> IResult<&str, Category> {
|
||||
@@ -232,7 +233,8 @@ fn parse_category(i: &str) -> IResult<&str, Category> {
|
||||
map(tag("3"), |_| Category::Unspecified3),
|
||||
map(tag("4"), |_| Category::Unspecified4),
|
||||
map(tag("5"), |_| Category::MailSystem),
|
||||
))(i)
|
||||
))
|
||||
.parse(i)
|
||||
}
|
||||
|
||||
fn parse_detail(i: &str) -> IResult<&str, Detail> {
|
||||
@@ -247,18 +249,20 @@ fn parse_detail(i: &str) -> IResult<&str, Detail> {
|
||||
map(tag("7"), |_| Detail::Seven),
|
||||
map(tag("8"), |_| Detail::Eight),
|
||||
map(tag("9"), |_| Detail::Nine),
|
||||
))(i)
|
||||
))
|
||||
.parse(i)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_response(i: &str) -> IResult<&str, Response> {
|
||||
let (i, lines) = many0(tuple((
|
||||
let (i, lines) = many0((
|
||||
parse_code,
|
||||
preceded(tag("-"), take_until("\r\n")),
|
||||
tag("\r\n"),
|
||||
)))(i)?;
|
||||
))
|
||||
.parse(i)?;
|
||||
let (i, (last_code, last_line)) =
|
||||
tuple((parse_code, preceded(tag(" "), take_until("\r\n"))))(i)?;
|
||||
let (i, _) = complete(tag("\r\n"))(i)?;
|
||||
(parse_code, preceded(tag(" "), take_until("\r\n"))).parse(i)?;
|
||||
let (i, _) = complete(tag("\r\n")).parse(i)?;
|
||||
|
||||
// Check that all codes are equal.
|
||||
if !lines.iter().all(|&(code, _, _)| code == last_code) {
|
||||
|
||||
@@ -7,11 +7,35 @@ use super::pool::sync_impl::Pool;
|
||||
#[cfg(feature = "pool")]
|
||||
use super::PoolConfig;
|
||||
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
||||
use crate::{address::Envelope, Transport};
|
||||
|
||||
/// Sends emails using the SMTP protocol
|
||||
/// Synchronously send emails using the SMTP protocol
|
||||
///
|
||||
/// `SmtpTransport` is the primary way for communicating
|
||||
/// with SMTP relay servers to send email messages. It holds the
|
||||
/// client connect configuration and creates new connections
|
||||
/// as necessary.
|
||||
///
|
||||
/// # Connection pool
|
||||
///
|
||||
/// When the `pool` feature is enabled (default), `SmtpTransport` maintains a
|
||||
/// connection pool to manage SMTP connections. The pool:
|
||||
///
|
||||
/// - Establishes a new connection when sending a message.
|
||||
/// - Recycles connections internally after a message is sent.
|
||||
/// - Reuses connections for subsequent messages, reducing connection setup overhead.
|
||||
///
|
||||
/// The connection pool can grow to hold multiple SMTP connections if multiple
|
||||
/// emails are sent concurrently, as SMTP does not support multiplexing within a
|
||||
/// single connection.
|
||||
///
|
||||
/// However, **connection reuse is not possible** if the `SmtpTransport` instance
|
||||
/// is dropped after every email send operation. You must reuse the instance
|
||||
/// of this struct for the connection pool to be of any use.
|
||||
///
|
||||
/// To customize connection pool settings, use [`SmtpTransportBuilder::pool_config`].
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpTransport {
|
||||
@@ -32,10 +56,15 @@ impl Transport for SmtpTransport {
|
||||
let result = conn.send(envelope, email)?;
|
||||
|
||||
#[cfg(not(feature = "pool"))]
|
||||
conn.quit()?;
|
||||
conn.abort();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn shutdown(&self) {
|
||||
#[cfg(feature = "pool")]
|
||||
self.inner.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SmtpTransport {
|
||||
@@ -53,10 +82,10 @@ impl SmtpTransport {
|
||||
///
|
||||
/// Creates an encrypted transport over submissions port, using the provided domain
|
||||
/// to validate TLS certificates.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
@@ -77,10 +106,10 @@ impl SmtpTransport {
|
||||
///
|
||||
/// An error is returned if the connection can't be upgraded. No credentials
|
||||
/// or emails will be sent to the server, protecting from downgrade attacks.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
@@ -115,54 +144,72 @@ impl SmtpTransport {
|
||||
|
||||
/// Creates a `SmtpTransportBuilder` from a connection URL
|
||||
///
|
||||
/// The protocol, credentials, host and port can be provided in a single URL.
|
||||
/// Use the scheme `smtp` for an unencrypted relay (optionally in combination with the
|
||||
/// `tls` parameter to allow/require STARTTLS) or `smtps` for SMTP over TLS.
|
||||
/// The path section of the url can be used to set an alternative name for
|
||||
/// the HELO / EHLO command.
|
||||
/// For example `smtps://username:password@smtp.example.com/client.example.com:465`
|
||||
/// will set the HELO / EHLO name `client.example.com`.
|
||||
/// The protocol, credentials, host, port and EHLO name can be provided
|
||||
/// in a single URL. This may be simpler than having to configure SMTP
|
||||
/// through multiple configuration parameters and then having to pass
|
||||
/// those options to lettre.
|
||||
///
|
||||
/// <table>
|
||||
/// <thead>
|
||||
/// <tr>
|
||||
/// <th>scheme</th>
|
||||
/// <th>tls parameter</th>
|
||||
/// <th>example</th>
|
||||
/// <th>remarks</th>
|
||||
/// </tr>
|
||||
/// </thead>
|
||||
/// <tbody>
|
||||
/// <tr>
|
||||
/// <td>smtps</td>
|
||||
/// <td>-</td>
|
||||
/// <td>smtps://smtp.example.com</td>
|
||||
/// <td>SMTP over TLS, recommended method</td>
|
||||
/// </tr>
|
||||
/// <tr>
|
||||
/// <td>smtp</td>
|
||||
/// <td>required</td>
|
||||
/// <td>smtp://smtp.example.com?tls=required</td>
|
||||
/// <td>SMTP with STARTTLS required, when SMTP over TLS is not available</td>
|
||||
/// </tr>
|
||||
/// <tr>
|
||||
/// <td>smtp</td>
|
||||
/// <td>opportunistic</td>
|
||||
/// <td>smtp://smtp.example.com?tls=opportunistic</td>
|
||||
/// <td>
|
||||
/// SMTP with optionally STARTTLS when supported by the server.
|
||||
/// Caution: this method is vulnerable to a man-in-the-middle attack.
|
||||
/// Not recommended for production use.
|
||||
/// </td>
|
||||
/// </tr>
|
||||
/// <tr>
|
||||
/// <td>smtp</td>
|
||||
/// <td>-</td>
|
||||
/// <td>smtp://smtp.example.com</td>
|
||||
/// <td>Unencrypted SMTP, not recommended for production use.</td>
|
||||
/// </tr>
|
||||
/// </tbody>
|
||||
/// </table>
|
||||
/// The URL is created in the following way:
|
||||
/// `scheme://user:pass@hostname:port/ehlo-name?tls=TLS`.
|
||||
///
|
||||
/// `user` (Username) and `pass` (Password) are optional in case the
|
||||
/// SMTP relay doesn't require authentication. When `port` is not
|
||||
/// configured it is automatically determined based on the `scheme`.
|
||||
/// `ehlo-name` optionally overwrites the hostname sent for the EHLO
|
||||
/// command. `TLS` controls whether STARTTLS is simply enabled
|
||||
/// (`opportunistic` - not enough to prevent man-in-the-middle attacks)
|
||||
/// or `required` (require the server to upgrade the connection to
|
||||
/// STARTTLS, otherwise fail on suspicion of main-in-the-middle attempt).
|
||||
///
|
||||
/// Use the following table to construct your SMTP url:
|
||||
///
|
||||
/// | scheme | `tls` query parameter | example | default port | remarks |
|
||||
/// | ------- | --------------------- | -------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
/// | `smtps` | unset | `smtps://user:pass@hostname:port` | 465 | SMTP over TLS, recommended method |
|
||||
/// | `smtp` | `required` | `smtp://user:pass@hostname:port?tls=required` | 587 | SMTP with STARTTLS required, when SMTP over TLS is not available |
|
||||
/// | `smtp` | `opportunistic` | `smtp://user:pass@hostname:port?tls=opportunistic` | 587 | SMTP with optionally STARTTLS when supported by the server. Not suitable for production use: vulnerable to a man-in-the-middle attack |
|
||||
/// | `smtp` | unset | `smtp://user:pass@hostname:port` | 587 | Always unencrypted SMTP. Not suitable for production use: sends all data unencrypted |
|
||||
///
|
||||
/// IMPORTANT: some parameters like `user` and `pass` cannot simply
|
||||
/// be concatenated to construct the final URL because special characters
|
||||
/// contained within the parameter may confuse the URL decoder.
|
||||
/// Manually URL encode the parameters before concatenating them or use
|
||||
/// a proper URL encoder, like the following cargo script:
|
||||
///
|
||||
/// ```rust
|
||||
/// # const TOML: &str = r#"
|
||||
/// #!/usr/bin/env cargo
|
||||
///
|
||||
/// //! ```cargo
|
||||
/// //! [dependencies]
|
||||
/// //! url = "2"
|
||||
/// //! ```
|
||||
/// # "#;
|
||||
///
|
||||
/// use url::Url;
|
||||
///
|
||||
/// fn main() {
|
||||
/// // don't touch this line
|
||||
/// let mut url = Url::parse("foo://bar").unwrap();
|
||||
///
|
||||
/// // configure the scheme (`smtp` or `smtps`) here.
|
||||
/// url.set_scheme("smtps").unwrap();
|
||||
/// // configure the username and password.
|
||||
/// // remove the following two lines if unauthenticated.
|
||||
/// url.set_username("username").unwrap();
|
||||
/// url.set_password(Some("password")).unwrap();
|
||||
/// // configure the hostname
|
||||
/// url.set_host(Some("smtp.example.com")).unwrap();
|
||||
/// // configure the port - only necessary if using a non-default port
|
||||
/// url.set_port(Some(465)).unwrap();
|
||||
/// // configure the EHLO name
|
||||
/// url.set_path("ehlo-name");
|
||||
///
|
||||
/// println!("{url}");
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The connection URL can then be used in the following way:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use lettre::{
|
||||
@@ -170,6 +217,7 @@ impl SmtpTransport {
|
||||
/// SmtpTransport, Transport,
|
||||
/// };
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let email = Message::builder()
|
||||
/// .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
/// .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
@@ -180,20 +228,17 @@ impl SmtpTransport {
|
||||
/// .unwrap();
|
||||
///
|
||||
/// // Open a remote connection to example
|
||||
/// let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com:465")
|
||||
/// .unwrap()
|
||||
/// .build();
|
||||
/// let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com")?.build();
|
||||
///
|
||||
/// // Send the email
|
||||
/// match mailer.send(&email) {
|
||||
/// Ok(_) => println!("Email sent successfully!"),
|
||||
/// Err(e) => panic!("Could not send email: {e:?}"),
|
||||
/// }
|
||||
/// mailer.send(&email)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn from_url(connection_url: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||
super::connection_url::from_connection_url(connection_url)
|
||||
@@ -246,7 +291,7 @@ impl SmtpTransportBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
/// Set the authentication credentials to use
|
||||
pub fn credentials(mut self, credentials: Credentials) -> Self {
|
||||
self.info.credentials = Some(credentials);
|
||||
self
|
||||
@@ -265,16 +310,38 @@ impl SmtpTransportBuilder {
|
||||
}
|
||||
|
||||
/// Set the port to use
|
||||
///
|
||||
/// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
|
||||
///
|
||||
/// lettre usually picks the correct `port` when building
|
||||
/// [`SmtpTransport`] using [`SmtpTransport::relay`] or
|
||||
/// [`SmtpTransport::starttls_relay`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Using the incorrect `port` and [`Self::tls`] combination may
|
||||
/// lead to hard to debug IO errors coming from the TLS library.
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.info.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the TLS settings to use
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
///
|
||||
/// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
|
||||
///
|
||||
/// By default lettre chooses the correct `tls` configuration when
|
||||
/// building [`SmtpTransport`] using [`SmtpTransport::relay`] or
|
||||
/// [`SmtpTransport::starttls_relay`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Using the wrong [`Tls`] and [`Self::port`] combination may
|
||||
/// lead to hard to debug IO errors coming from the TLS library.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn tls(mut self, tls: Tls) -> Self {
|
||||
self.info.tls = tls;
|
||||
@@ -307,7 +374,7 @@ impl SmtpTransportBuilder {
|
||||
|
||||
/// Build client
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmtpClient {
|
||||
pub(super) struct SmtpClient {
|
||||
info: SmtpInfo,
|
||||
}
|
||||
|
||||
@@ -315,10 +382,10 @@ impl SmtpClient {
|
||||
/// Creates a new connection directly usable to send emails
|
||||
///
|
||||
/// Handles encryption and authentication
|
||||
pub fn connection(&self) -> Result<SmtpConnection, Error> {
|
||||
pub(super) fn connection(&self) -> Result<SmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match &self.info.tls {
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
Tls::Wrapper(tls_parameters) => Some(tls_parameters),
|
||||
_ => None,
|
||||
};
|
||||
@@ -332,7 +399,7 @@ impl SmtpClient {
|
||||
None,
|
||||
)?;
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
match &self.info.tls {
|
||||
Tls::Opportunistic(tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
|
||||
@@ -4,9 +4,9 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||
|
||||
/// Encode a string as xtext
|
||||
#[derive(Debug)]
|
||||
pub struct XText<'a>(pub &'a str);
|
||||
pub(crate) struct XText<'a>(pub(crate) &'a str);
|
||||
|
||||
impl<'a> Display for XText<'a> {
|
||||
impl Display for XText<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
let mut rest = self.0;
|
||||
while let Some(idx) = rest.find(|c| c < '!' || c == '+' || c == '=') {
|
||||
@@ -38,9 +38,7 @@ mod tests {
|
||||
("bjørn", "bjørn"),
|
||||
("Ø+= ❤️‰", "Ø+2B+3D+20❤️‰"),
|
||||
("+", "+2B"),
|
||||
]
|
||||
.iter()
|
||||
{
|
||||
] {
|
||||
assert_eq!(format!("{}", XText(input)), (*expect).to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
//! ```rust
|
||||
//! # #[cfg(feature = "builder")]
|
||||
//! # {
|
||||
//! use lettre::{transport::stub::StubTransport, Message, Transport};
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType, transport::stub::StubTransport, Message, Transport,
|
||||
//! };
|
||||
//!
|
||||
//! # use std::error::Error;
|
||||
//! # fn try_main() -> Result<(), Box<dyn Error>> {
|
||||
@@ -20,11 +22,11 @@
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let mut sender = StubTransport::new_ok();
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! sender.send(&email)?;
|
||||
//! assert_eq!(
|
||||
//! sender.messages(),
|
||||
//! vec![(
|
||||
@@ -41,13 +43,11 @@
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt,
|
||||
sync::{Arc, Mutex as StdMutex},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use futures_util::lock::Mutex as FuturesMutex;
|
||||
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use crate::AsyncTransport;
|
||||
@@ -70,7 +70,7 @@ impl StdError for Error {}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StubTransport {
|
||||
response: Result<(), Error>,
|
||||
message_log: Arc<StdMutex<Vec<(Envelope, String)>>>,
|
||||
message_log: Arc<Mutex<Vec<(Envelope, String)>>>,
|
||||
}
|
||||
|
||||
/// This transport logs messages and always returns the given response
|
||||
@@ -79,7 +79,7 @@ pub struct StubTransport {
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
pub struct AsyncStubTransport {
|
||||
response: Result<(), Error>,
|
||||
message_log: Arc<FuturesMutex<Vec<(Envelope, String)>>>,
|
||||
message_log: Arc<Mutex<Vec<(Envelope, String)>>>,
|
||||
}
|
||||
|
||||
impl StubTransport {
|
||||
@@ -87,7 +87,7 @@ impl StubTransport {
|
||||
pub fn new(response: Result<(), Error>) -> Self {
|
||||
Self {
|
||||
response,
|
||||
message_log: Arc::new(StdMutex::new(vec![])),
|
||||
message_log: Arc::new(Mutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ impl StubTransport {
|
||||
pub fn new_ok() -> Self {
|
||||
Self {
|
||||
response: Ok(()),
|
||||
message_log: Arc::new(StdMutex::new(vec![])),
|
||||
message_log: Arc::new(Mutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ impl StubTransport {
|
||||
pub fn new_error() -> Self {
|
||||
Self {
|
||||
response: Err(Error),
|
||||
message_log: Arc::new(StdMutex::new(vec![])),
|
||||
message_log: Arc::new(Mutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ impl AsyncStubTransport {
|
||||
pub fn new(response: Result<(), Error>) -> Self {
|
||||
Self {
|
||||
response,
|
||||
message_log: Arc::new(FuturesMutex::new(vec![])),
|
||||
message_log: Arc::new(Mutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ impl AsyncStubTransport {
|
||||
pub fn new_ok() -> Self {
|
||||
Self {
|
||||
response: Ok(()),
|
||||
message_log: Arc::new(FuturesMutex::new(vec![])),
|
||||
message_log: Arc::new(Mutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,14 +138,14 @@ impl AsyncStubTransport {
|
||||
pub fn new_error() -> Self {
|
||||
Self {
|
||||
response: Err(Error),
|
||||
message_log: Arc::new(FuturesMutex::new(vec![])),
|
||||
message_log: Arc::new(Mutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return all logged messages sent using [`AsyncTransport::send_raw`]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub async fn messages(&self) -> Vec<(Envelope, String)> {
|
||||
self.message_log.lock().await.clone()
|
||||
self.message_log.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ impl AsyncTransport for AsyncStubTransport {
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
self.message_log
|
||||
.lock()
|
||||
.await
|
||||
.unwrap()
|
||||
.push((envelope.clone(), String::from_utf8_lossy(email).into()));
|
||||
self.response
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user